From eaaeaeac80feeab5223a2badbc7224e6a1608e77 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Tue, 12 Sep 2023 22:32:39 +0000 Subject: [PATCH 001/131] rtk install --- package-lock.json | 38 ++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 39 insertions(+) diff --git a/package-lock.json b/package-lock.json index 40abd2a70..df101d435 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.8.0", "license": "MPL-2.0", "dependencies": { + "@reduxjs/toolkit": "~1.9.5", "@wojtekmaj/react-daterange-picker": "~5.2.0", "axios": "~0.24.0", "bcryptjs": "~2.4.3", @@ -2535,6 +2536,29 @@ "redux": "^3.1.0 || ^4.0.0" } }, + "node_modules/@reduxjs/toolkit": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", + "integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==", + "dependencies": { + "immer": "^9.0.21", + "redux": "^4.2.1", + "redux-thunk": "^2.4.2", + "reselect": "^4.1.8" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.0.2" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@sinonjs/commons": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", @@ -7320,6 +7344,15 @@ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "dev": true }, + "node_modules/immer": { + "version": "9.0.21", + "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", + "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/immutable": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.2.tgz", @@ -10735,6 +10768,11 @@ "integrity": "sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==", "peer": true }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "node_modules/resolve": { "version": "1.22.4", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", diff --git a/package.json b/package.json index 057ea0d52..0f1d6db8d 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "babel-plugin-lodash": "~3.3.4" }, "dependencies": { + "@reduxjs/toolkit": "~1.9.5", "@wojtekmaj/react-daterange-picker": "~5.2.0", "axios": "~0.24.0", "bcryptjs": "~2.4.3", From c0bebe1f29dab2df782503b938344b6d5f07fcb9 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Mon, 18 Sep 2023 01:53:52 +0000 Subject: [PATCH 002/131] RTK Query Sample Usage --- src/client/app/actions/graph.ts | 9 +- src/client/app/actions/threeDReadings.ts | 156 ------------------ src/client/app/components/HomeComponent.tsx | 8 +- .../ReadingsPerDaySelectComponent.tsx | 47 +++--- src/client/app/components/ThreeDComponent.tsx | 67 ++------ src/client/app/index.tsx | 8 +- src/client/app/reducers/index.ts | 10 +- src/client/app/reducers/threeDReadings.ts | 119 ------------- src/client/app/redux/api/baseApi.ts | 9 + src/client/app/redux/api/groupsApi.ts | 10 ++ src/client/app/redux/api/metersApi.ts | 20 +++ src/client/app/redux/api/readingsApi.ts | 25 +++ src/client/app/redux/hooks.ts | 7 + .../app/redux/selectors/threeDSelectors.ts | 71 ++++++++ src/client/app/store.ts | 21 +++ src/client/app/types/redux/state.ts | 2 - 16 files changed, 214 insertions(+), 375 deletions(-) delete mode 100644 src/client/app/actions/threeDReadings.ts delete mode 100644 src/client/app/reducers/threeDReadings.ts create mode 100644 src/client/app/redux/api/baseApi.ts create mode 100644 src/client/app/redux/api/groupsApi.ts create mode 100644 src/client/app/redux/api/metersApi.ts create mode 100644 src/client/app/redux/api/readingsApi.ts create mode 100644 src/client/app/redux/hooks.ts create mode 100644 src/client/app/redux/selectors/threeDSelectors.ts create mode 100644 src/client/app/store.ts diff --git a/src/client/app/actions/graph.ts b/src/client/app/actions/graph.ts index b5ffae801..bc4ad23e1 100644 --- a/src/client/app/actions/graph.ts +++ b/src/client/app/actions/graph.ts @@ -18,7 +18,6 @@ import { fetchNeededMapReadings } from './mapReadings'; import { changeSelectedMap, fetchMapsDetails } from './map'; import { fetchUnitsDetailsIfNeeded } from './units'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; -import { fetchNeededThreeDReadings } from './threeDReadings'; export function changeRenderOnce() { return { type: ActionType.ConfirmGraphRenderOnce }; @@ -125,7 +124,6 @@ export function changeSelectedMeters(meterIDs: number[]): Thunk { dispatch2(fetchNeededBarReadings(getState().graph.timeInterval, getState().graph.selectedUnit)); dispatch2(fetchNeededCompareReadings(getState().graph.comparePeriod, getState().graph.selectedUnit)); dispatch2(fetchNeededMapReadings(getState().graph.timeInterval, getState().graph.selectedUnit)); - dispatch2(fetchNeededThreeDReadings()); }); return Promise.resolve(); }; @@ -140,7 +138,6 @@ export function changeSelectedGroups(groupIDs: number[]): Thunk { dispatch2(fetchNeededBarReadings(getState().graph.timeInterval, getState().graph.selectedUnit)); dispatch2(fetchNeededCompareReadings(getState().graph.comparePeriod, getState().graph.selectedUnit)); dispatch2(fetchNeededMapReadings(getState().graph.timeInterval, getState().graph.selectedUnit)); - dispatch2(fetchNeededThreeDReadings()); }); return Promise.resolve(); }; @@ -154,7 +151,6 @@ export function changeSelectedUnit(unitID: number): Thunk { dispatch2(fetchNeededBarReadings(getState().graph.timeInterval, unitID)); dispatch2(fetchNeededCompareReadings(getState().graph.comparePeriod, unitID)); dispatch2(fetchNeededMapReadings(getState().graph.timeInterval, unitID)); - dispatch2(fetchNeededThreeDReadings()); }); return Promise.resolve(); } @@ -165,7 +161,6 @@ function fetchNeededReadingsForGraph(timeInterval: TimeInterval, unitID: number) dispatch(fetchNeededLineReadings(timeInterval, unitID)); dispatch(fetchNeededBarReadings(timeInterval, unitID)); dispatch(fetchNeededMapReadings(timeInterval, unitID)); - dispatch(fetchNeededThreeDReadings()); return Promise.resolve(); }; } @@ -213,7 +208,7 @@ function changeRangeSliderIfNeeded(interval: TimeInterval): Thunk { export function updateThreeDReadingInterval(readingInterval: t.ReadingInterval): Thunk { return (dispatch: Dispatch) => { dispatch({ type: ActionType.UpdateThreeDReadingInterval, readingInterval }); - return dispatch(fetchNeededThreeDReadings()); + return Promise.resolve(); }; } @@ -225,7 +220,7 @@ export function changeMeterOrGroupInfo(meterOrGroupID: t.MeterOrGroupID, meterOr // Meter ID can be null, however meterOrGroup defaults to meters a null check on ID can be sufficient return (dispatch: Dispatch) => { dispatch(updateThreeDMeterOrGroupInfo(meterOrGroupID, meterOrGroup)); - return dispatch(fetchNeededThreeDReadings()); + return Promise.resolve(); }; } diff --git a/src/client/app/actions/threeDReadings.ts b/src/client/app/actions/threeDReadings.ts deleted file mode 100644 index 282ed6f9c..000000000 --- a/src/client/app/actions/threeDReadings.ts +++ /dev/null @@ -1,156 +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 { TimeInterval } from '../../../common/TimeInterval'; -import { ActionType, Thunk, Dispatch, GetState } from '../types/redux/actions'; -import { State } from '../types/redux/state'; -import * as t from '../types/redux/threeDReadings'; -import { ChartTypes, ReadingInterval } from '../types/redux/graph' -import { readingsApi } from '../utils/api'; -import { ThreeDReading } from '../types/readings'; -import { isValidThreeDInterval, roundTimeIntervalForFetch } from '../utils/dateRangeCompatability'; - -/** - * @param meterID the IDs of the meters to get readings - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - * @param readingInterval number of readings occurring on the x axis (one day typically corresponds to a y axis tick) - */ -function requestMeterThreeDReadings(meterID: number, timeInterval: TimeInterval, unitID: number, readingInterval: ReadingInterval) - : t.RequestMeterThreeDReadingsAction { - return { type: ActionType.RequestMeterThreeDReadings, meterID, timeInterval, unitID, readingInterval }; -} - -/** - * @param meterID the IDs of the meters to get readings - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - * @param readingInterval number of readings occurring on the x axis (one day typically corresponds to a y axis tick) - * @param readings the readings for the given meters - */ -function receiveMeterThreeDReadings( - meterID: number, timeInterval: TimeInterval, unitID: number, readingInterval: ReadingInterval, readings: ThreeDReading) - : t.ReceiveMeterThreeDReadingsAction { - return { type: ActionType.ReceiveMeterThreeDReadings, meterID, timeInterval, unitID, readingInterval, readings }; -} - -/** - * @param meterID the IDs of the meters to get readings - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - * @param readingInterval number of readings occurring on the x axis (one day typically corresponds to a y axis tick) - */ -function fetchMeterThreeDReadings(meterID: number, timeInterval: TimeInterval, unitID: number, readingInterval: ReadingInterval): Thunk { - return async (dispatch: Dispatch) => { - dispatch(requestMeterThreeDReadings(meterID, timeInterval, unitID, readingInterval)); - const meterThreeDReadings = await readingsApi.meterThreeDReadings(meterID, timeInterval, unitID, readingInterval); - dispatch(receiveMeterThreeDReadings(meterID, timeInterval, unitID, readingInterval, meterThreeDReadings)); - }; -} - -/** - * Fetches 3D readings for the selected meter if needed. - */ -export function fetchNeededThreeDReadings(): Thunk { - return (dispatch: Dispatch, getState: GetState) => { - const state = getState(); - const selectedMeterOrGroupID = state.graph.threeD.meterOrGroupID; - const meterOrGroup = state.graph.threeD.meterOrGroup; - //3D Graphic currently only allows full days. Round start down && end up - const timeInterval = roundTimeIntervalForFetch(state.graph.timeInterval); - - // only fetch if on 3D page - // Time interval must be bounded, Infinite intervals not allowed - // Meter Must Be Selected. - if (state.graph.chartToRender !== ChartTypes.threeD || - !isValidThreeDInterval(timeInterval) || - !selectedMeterOrGroupID) { - return Promise.resolve(); - } - if (meterOrGroup === 'meters') { - if (shouldFetchMeterThreeDReadings(state, selectedMeterOrGroupID, timeInterval, state.graph.selectedUnit, state.graph.threeD.readingInterval)) { - return dispatch(fetchMeterThreeDReadings(selectedMeterOrGroupID, timeInterval, state.graph.selectedUnit, state.graph.threeD.readingInterval)); - } - } else { - if (shouldFetchGroupThreeDReadings(state, selectedMeterOrGroupID, timeInterval, state.graph.selectedUnit, state.graph.threeD.readingInterval)) { - return dispatch(fetchGroupThreeDReadings(selectedMeterOrGroupID, timeInterval, state.graph.selectedUnit, state.graph.threeD.readingInterval)); - } - } - - return Promise.resolve(); - }; -} - -/** - * @param state the Redux state - * @param meterID the ID of the meter to check - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - * @param precision number of readings occurring on the x axis (one day typically corresponds to a y axis tick) - * @returns True if the readings for the given meter, time duration and unit are missing; false otherwise. - */ -// function shouldFetchMeterThreeDReadings(state: State, meterID: number, timeInterval: TimeInterval, unitID: number): boolean { -function shouldFetchMeterThreeDReadings(state: State, meterID: number, timeInterval: TimeInterval, unitID: number, precision: ReadingInterval) - : boolean { - const timeIntervalIndex = timeInterval.toString(); - // Optional chaining returns undefined if any of the properties in the chain aren't present - const hasReadings = state.readings.threeD.byMeterID[meterID]?.[timeIntervalIndex]?.[unitID]?.[precision]?.readings; - // return true if readings aren't present. - return !hasReadings ? true : false; -} - -/** - * @param groupID the IDs of the groups to get readings - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - * @param readingInterval number of readings occurring on the x axis (one day typically corresponds to a y axis tick) - */ -function requestGroupThreeDReadings(groupID: number, timeInterval: TimeInterval, unitID: number, readingInterval: ReadingInterval) - : t.RequestGroupThreeDReadingsAction { - return { type: ActionType.RequestGroupThreeDReadings, groupID, timeInterval, unitID, readingInterval }; -} - -/** - * @param groupID the IDs of the groups to get readings - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - * @param readingInterval number of readings occurring on the x axis (one day typically corresponds to a y axis tick) - * @param readings the readings for the given groups - */ -function receiveGroupThreeDReadings( - groupID: number, timeInterval: TimeInterval, unitID: number, readingInterval: ReadingInterval, readings: ThreeDReading) - : t.ReceiveGroupThreeDReadingsAction { - return { type: ActionType.ReceiveGroupThreeDReadings, groupID, timeInterval, unitID, readingInterval, readings }; -} - -/** - * @param groupID the IDs of the groups to get readings - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - * @param readingInterval number of readings occurring on the x axis (one day typically corresponds to a y axis tick) - */ -function fetchGroupThreeDReadings(groupID: number, timeInterval: TimeInterval, unitID: number, readingInterval: ReadingInterval): Thunk { - return async (dispatch: Dispatch) => { - dispatch(requestGroupThreeDReadings(groupID, timeInterval, unitID, readingInterval)); - const groupThreeDReadings = await readingsApi.groupThreeDReadings(groupID, timeInterval, unitID, readingInterval); - dispatch(receiveGroupThreeDReadings(groupID, timeInterval, unitID, readingInterval, groupThreeDReadings)); - }; -} - -/** - * @param state the Redux state - * @param groupID the ID of the group to check - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - * @param readingInterval number of readings occurring on the x axis (one day typically corresponds to a y axis tick) - * @returns True if the readings for the given group, time duration and unit are missing; false otherwise. - */ -function shouldFetchGroupThreeDReadings( - state: State, groupID: number, timeInterval: TimeInterval, unitID: number, readingInterval: ReadingInterval): boolean { - const timeIntervalIndex = timeInterval.toString(); - // Optional chaining returns undefined if any of the properties in the chain aren't present - const hasReadings = state.readings.threeD.byGroupID[groupID]?.[timeIntervalIndex]?.[unitID]?.[readingInterval]?.readings; - // return true if readings aren't present. - return !hasReadings ? true : false; -} \ No newline at end of file diff --git a/src/client/app/components/HomeComponent.tsx b/src/client/app/components/HomeComponent.tsx index 37cdce270..55ab7d931 100644 --- a/src/client/app/components/HomeComponent.tsx +++ b/src/client/app/components/HomeComponent.tsx @@ -7,16 +7,22 @@ import DashboardContainer from '../containers/DashboardContainer'; import FooterContainer from '../containers/FooterContainer'; import TooltipHelpContainer from '../containers/TooltipHelpContainer'; import HeaderComponent from './HeaderComponent'; +import { metersApi } from '../redux/api/metersApi'; +import { groupsApi } from '../redux/api/groupsApi'; /** * Top-level React component that controls the home page * @returns JSX to create the home page */ export default function HomeComponent() { + // /api/unitReadings/threeD/meters/28?timeInterval=2020-05-08T00:00:00Z_2020-07-15T00:00:00Z&graphicUnitId=1&readingInterval=1 + metersApi.endpoints.getMeters.useQuery(); + groupsApi.endpoints.getGroups.useQuery(); + return (
- +
diff --git a/src/client/app/components/ReadingsPerDaySelectComponent.tsx b/src/client/app/components/ReadingsPerDaySelectComponent.tsx index 67661c347..11801f597 100644 --- a/src/client/app/components/ReadingsPerDaySelectComponent.tsx +++ b/src/client/app/components/ReadingsPerDaySelectComponent.tsx @@ -6,13 +6,16 @@ import * as React from 'react'; import Select from 'react-select'; import { State } from '../types/redux/state'; import { useDispatch, useSelector } from 'react-redux'; +import { useAppSelector } from '../redux/hooks'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; import { ChartTypes, ReadingInterval } from '../types/redux/graph'; import { Dispatch } from '../types/redux/actions'; import { updateThreeDReadingInterval } from '../actions/graph'; -import { ByMeterOrGroup, MeterOrGroup } from '../types/redux/graph'; -import { roundTimeIntervalForFetch } from '../utils/dateRangeCompatability'; +import { selectThreeDQueryArgs, selectThreeDSkip } from '../redux/selectors/threeDSelectors' +import { readingsApi } from '../redux/api/readingsApi' + + import * as moment from 'moment'; /** @@ -23,36 +26,26 @@ export default function ReadingsPerDaySelect() { const dispatch: Dispatch = useDispatch(); const graphState = useSelector((state: State) => state.graph); const readingInterval = useSelector((state: State) => state.graph.threeD.readingInterval); - const actualReadingInterval = useSelector((state: State) => { - const threeDState = state.graph.threeD; - const meterOrGroupID = threeDState.meterOrGroupID; - // meterOrGroup determines whether to get readings from state .byMeterID or .byGroupID - const byMeterOrGroup = threeDState.meterOrGroup === MeterOrGroup.meters ? ByMeterOrGroup.meters : ByMeterOrGroup.groups; - // 3D requires intervals to be rounded to a full day. - const timeInterval = roundTimeIntervalForFetch(state.graph.timeInterval).toString(); - const unitID = state.graph.selectedUnit; - // Level of detail along the xAxis / Readings per day - const readingInterval = state.graph.threeD.readingInterval; - - // If Meter not selected return null data, else return data if any. - const data = !meterOrGroupID ? null : state.readings.threeD[byMeterOrGroup][meterOrGroupID]?.[timeInterval]?.[unitID]?.[readingInterval]?.readings; + const queryArgs = useAppSelector(selectThreeDQueryArgs); + const shouldSkip = useAppSelector(selectThreeDSkip); - if (data && data.zData.length) { - // Special Case: When no compatible data available, data returned is from api is -999 - if (data.zData[0][0] && data.zData[0][0] < 0) { - return ReadingInterval.Incompatible; - } + const { data, isFetching } = readingsApi.endpoints.threeD.useQuery(queryArgs, { skip: shouldSkip }); - // Calculate the actual time interval based on the xLabel values + let actualReadingInterval = ReadingInterval.Hourly + if (data && data.zData[0][0]) { + // Special Case: When no compatible data available, data returned is from api is -999 + if (data.zData[0][0] < 0) { + actualReadingInterval = ReadingInterval.Incompatible; + } else { const startTS = moment.utc(data.xData[0].startTimestamp); const endTS = moment.utc(data.xData[0].endTimestamp); - const actualReadingInterval = endTS.diff(startTS) / 3600000; - return actualReadingInterval + actualReadingInterval = endTS.diff(startTS) / 3600000; } - // Return normal interval - return readingInterval; - }) + } + + // Return normal interval + // return readingInterval; // Iterate over readingInterval enum to create select option const options = Object.values(ReadingInterval) @@ -95,7 +88,7 @@ export default function ReadingsPerDaySelect() { {`${translate('readings.per.day')}:`}

- ) } else { diff --git a/src/client/app/components/ThreeDComponent.tsx b/src/client/app/components/ThreeDComponent.tsx index 93df470d6..d84268769 100644 --- a/src/client/app/components/ThreeDComponent.tsx +++ b/src/client/app/components/ThreeDComponent.tsx @@ -8,76 +8,41 @@ import Plot from 'react-plotly.js'; import ThreeDPillComponent from './ThreeDPillComponent'; import SpinnerComponent from './SpinnerComponent'; import { State } from '../types/redux/state'; -import { useDispatch, useSelector } from 'react-redux'; +import { useSelector } from 'react-redux'; import { ThreeDReading } from '../types/readings' import { roundTimeIntervalForFetch } from '../utils/dateRangeCompatability'; import { lineUnitLabel } from '../utils/graphics'; import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; import translate from '../utils/translate'; import { isValidThreeDInterval } from '../utils/dateRangeCompatability'; -import { ByMeterOrGroup, GraphState, MeterOrGroup } from '../types/redux/graph'; -import { Dispatch } from '../types/redux/actions'; -import { useEffect } from 'react'; -import { fetchNeededThreeDReadings } from '../actions/threeDReadings'; -import { UnitsState } from 'types/redux/units'; -import { MetersState } from 'types/redux/meters'; -import { GroupsState } from 'types/redux/groups'; +import { GraphState, MeterOrGroup } from '../types/redux/graph'; +import { UnitsState } from '../types/redux/units'; +import { MetersState } from '../types/redux/meters'; +import { GroupsState } from '../types/redux/groups'; +import { readingsApi } from '../redux/api/readingsApi' +import { useAppSelector } from '../redux/hooks'; +import { selectThreeDComponentInfo, selectThreeDQueryArgs, selectThreeDSkip } from '../redux/selectors/threeDSelectors' /** * Component used to render 3D graphics * @returns 3D Plotly 3D Surface Graph */ export default function ThreeDComponent() { - const dispatch: Dispatch = useDispatch(); const metersState = useSelector((state: State) => state.meters); const groupsState = useSelector((state: State) => state.groups); const graphState = useSelector((state: State) => state.graph); const unitState = useSelector((state: State) => state.units); - const threeDReadings = useSelector((state: State) => state.readings.threeD); - const isFetching = useSelector((state: State) => state.readings.threeD.isFetching); + const { meterOrGroupID, meterOrGroupName, isAreaCompatible } = useAppSelector(selectThreeDComponentInfo); + const queryArgs = useAppSelector(selectThreeDQueryArgs); + const shouldSkip = useAppSelector(selectThreeDSkip); + const { data, isFetching } = readingsApi.endpoints.threeD.useQuery(queryArgs, { skip: shouldSkip }); - const threeDState = graphState.threeD; - const meterOrGroupID = threeDState.meterOrGroupID; - - // meterOrGroup determines whether to get readings from state .byMeterID or .byGroupID - const byMeterOrGroup = threeDState.meterOrGroup === MeterOrGroup.meters ? ByMeterOrGroup.meters : ByMeterOrGroup.groups; - - // 3D requires intervals to be rounded to a full day. - const timeInterval = roundTimeIntervalForFetch(graphState.timeInterval).toString(); - const unitID = graphState.selectedUnit; - - // Level of detail along the xAxis / Readings per day - const readingInterval = threeDState.readingInterval; // Initialize Default values - let threeDData = null; - let isAreaCompatible = true; - let meterOrGroupName = 'Unknown Meter or Group'; + const threeDData = data; let layout = {}; let dataToRender = null; - // Meter Or Group is selected - if (meterOrGroupID) { - // Get Reading data, if any - threeDData = threeDReadings[byMeterOrGroup][meterOrGroupID]?.[timeInterval]?.[unitID]?.[readingInterval]?.readings; - - // Get Meter or Group's info - const meterOrGroupInfo = threeDState.meterOrGroup === MeterOrGroup.meters ? - metersState.byMeterID[meterOrGroupID] - : - groupsState.byGroupID[meterOrGroupID]; - - // Use Meter or Group's info to determine whether it can be rendered with area normalization - const area = meterOrGroupInfo.area; - const areaUnit = meterOrGroupInfo.areaUnit; - isAreaCompatible = area !== 0 && areaUnit !== AreaUnitType.none; - - // Get Meter Or Groups name/label - meterOrGroupName = threeDState.meterOrGroup === MeterOrGroup.meters ? - metersState.byMeterID[meterOrGroupID].identifier - : - groupsState.byGroupID[meterOrGroupID].name; - } if (!meterOrGroupID) { // No selected Meters @@ -100,12 +65,6 @@ export default function ThreeDComponent() { [dataToRender, layout] = formatThreeDData(threeDData, meterOrGroupID, metersState, groupsState, graphState, unitState); } - // Necessary for the case when a meter/group is selected and time intervals get altered externally. (Line graphic slider, for example.) - useEffect(() => { - // Fetch on initial render only, all other fetch will be called from PillBadges, or meter/group multiselect - dispatch(fetchNeededThreeDReadings()); - }, []) - return (
diff --git a/src/client/app/index.tsx b/src/client/app/index.tsx index fab559ecf..a3e3e50f8 100644 --- a/src/client/app/index.tsx +++ b/src/client/app/index.tsx @@ -3,22 +3,20 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import thunkMiddleware from 'redux-thunk'; import { createRoot } from 'react-dom/client'; -import { createStore, applyMiddleware } from 'redux'; import { Provider } from 'react-redux'; +import { store } from './store' import 'bootstrap/dist/css/bootstrap.css'; import RouteContainer from './containers/RouteContainer'; -import reducers from './reducers'; import './styles/index.css'; -import { composeWithDevTools } from '@redux-devtools/extension'; import initScript from './initScript'; // Creates and applies thunk middleware to the Redux store, which is defined from the Redux reducers. // For now we are enabling Redux debug tools on production builds. If had a good way to only do this // when not in production mode then maybe we should remove this but it does allow for debugging. // Comment this out if enabling traces below. -const store = createStore(reducers, composeWithDevTools(applyMiddleware(thunkMiddleware))); +// const store = createStore(reducers, composeWithDevTools(applyMiddleware(thunkMiddleware))); + // Creates and applies thunk middleware to the Redux store, which is defined from the Redux reducers. // It would be nice to enable this automatically if not in production mode. Unfortunately, the client diff --git a/src/client/app/reducers/index.ts b/src/client/app/reducers/index.ts index 6af4693af..5170cbf3c 100644 --- a/src/client/app/reducers/index.ts +++ b/src/client/app/reducers/index.ts @@ -7,7 +7,6 @@ import meters from './meters'; import lineReadings from './lineReadings'; import barReadings from './barReadings'; import compareReadings from './compareReadings'; -import threeDReadings from './threeDReadings'; import graph from './graph'; import groups from './groups'; import maps from './maps'; @@ -18,6 +17,8 @@ import unsavedWarning from './unsavedWarning'; import units from './units'; import conversions from './conversions'; import options from './options'; +import { baseApi } from '../redux/api/baseApi'; +import { graphSlice } from './graph'; export default combineReducers({ @@ -25,8 +26,7 @@ export default combineReducers({ readings: combineReducers({ line: lineReadings, bar: barReadings, - compare: compareReadings, - threeD: threeDReadings + compare: compareReadings }), graph, maps, @@ -37,5 +37,7 @@ export default combineReducers({ unsavedWarning, units, conversions, - options + options, + // RTK Query's Derived Reducers + [baseApi.reducerPath]: baseApi.reducer }); diff --git a/src/client/app/reducers/threeDReadings.ts b/src/client/app/reducers/threeDReadings.ts deleted file mode 100644 index 8cc6d5eaf..000000000 --- a/src/client/app/reducers/threeDReadings.ts +++ /dev/null @@ -1,119 +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 { ThreeDReadingsAction, ThreeDReadingsState } from '../types/redux/threeDReadings'; -import { ActionType } from '../types/redux/actions'; - -const defaultState: ThreeDReadingsState = { - byMeterID: {}, - byGroupID: {}, - isFetching: false, - metersFetching: false -}; - -export default function readings(state = defaultState, action: ThreeDReadingsAction) { - switch (action.type) { - case ActionType.RequestMeterThreeDReadings: { - const meterID = action.meterID; - const timeInterval = action.timeInterval.toString(); - const unitID = action.unitID; - const readingInterval = action.readingInterval; - const newState: ThreeDReadingsState = { - ...state, - isFetching: true - }; - - // Create meter wrappers if needed - if (newState.byMeterID[meterID] === undefined) { - newState.byMeterID[meterID] = {}; - } - - if (newState.byMeterID[meterID][timeInterval] === undefined) { - newState.byMeterID[meterID][timeInterval] = {}; - } - - // Preserve existing data if exists - if (newState.byMeterID[meterID][timeInterval][unitID] === undefined) { - newState.byMeterID[meterID][timeInterval][unitID] = {}; - } - - if (newState.byMeterID[meterID][timeInterval][unitID][readingInterval] !== undefined) { - newState.byMeterID[meterID][timeInterval][unitID][readingInterval] = { - ...newState.byMeterID[meterID][timeInterval][unitID][readingInterval], - isFetching: true - }; - } - else { - newState.byMeterID[meterID][timeInterval][unitID][readingInterval] = { isFetching: true }; - } - return newState; - } - case ActionType.ReceiveMeterThreeDReadings: { - const meterID = action.meterID; - const timeInterval = action.timeInterval.toString(); - const unitID = action.unitID; - const readingInterval = action.readingInterval; - const newState: ThreeDReadingsState = { - ...state, - isFetching: false - }; - - const readingsForMeter = action.readings; - newState.byMeterID[meterID][timeInterval][unitID][readingInterval] = { readings: readingsForMeter, isFetching: false }; - return newState; - } - case ActionType.RequestGroupThreeDReadings: { - const groupID = action.groupID; - const timeInterval = action.timeInterval.toString(); - const unitID = action.unitID; - const readingInterval = action.readingInterval; - const newState: ThreeDReadingsState = { - ...state, - isFetching: true - }; - - // Create group wrappers if needed - if (newState.byGroupID[groupID] === undefined) { - newState.byGroupID[groupID] = {}; - } - - if (newState.byGroupID[groupID][timeInterval] === undefined) { - newState.byGroupID[groupID][timeInterval] = {}; - } - - // Preserve existing data if exists - if (newState.byGroupID[groupID][timeInterval][unitID] === undefined) { - newState.byGroupID[groupID][timeInterval][unitID] = {}; - } - - if (newState.byGroupID[groupID][timeInterval][unitID][readingInterval] !== undefined) { - newState.byGroupID[groupID][timeInterval][unitID][readingInterval] = { - ...newState.byGroupID[groupID][timeInterval][unitID][readingInterval], - isFetching: true - }; - } - else { - newState.byGroupID[groupID][timeInterval][unitID][readingInterval] = { isFetching: true }; - } - return newState; - } - case ActionType.ReceiveGroupThreeDReadings: { - const groupID = action.groupID; - const timeInterval = action.timeInterval.toString(); - const unitID = action.unitID; - const readingInterval = action.readingInterval; - const newState: ThreeDReadingsState = { - ...state, - isFetching: false - }; - - const readingsForGroup = action.readings; - newState.byGroupID[groupID][timeInterval][unitID][readingInterval] = { readings: readingsForGroup, isFetching: false }; - return newState; - } - - default: - return state; - } -} diff --git a/src/client/app/redux/api/baseApi.ts b/src/client/app/redux/api/baseApi.ts new file mode 100644 index 000000000..0de221159 --- /dev/null +++ b/src/client/app/redux/api/baseApi.ts @@ -0,0 +1,9 @@ +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' +const baseHref = (document.getElementsByTagName('base')[0] || {}).href; + +export const baseApi = createApi({ + reducerPath: 'api', + baseQuery: fetchBaseQuery({ baseUrl: baseHref }), + // Initially no defined endpoints, Use rtk query's injectEndpoints + endpoints: () => ({}) +}) \ No newline at end of file diff --git a/src/client/app/redux/api/groupsApi.ts b/src/client/app/redux/api/groupsApi.ts new file mode 100644 index 000000000..dd4070ca8 --- /dev/null +++ b/src/client/app/redux/api/groupsApi.ts @@ -0,0 +1,10 @@ +import { baseApi } from './baseApi' +import { GroupData } from '../../types/redux/groups' + +export const groupsApi = baseApi.injectEndpoints({ + endpoints: builder => ({ + getGroups: builder.query({ query: () => 'api/groups' }) + }) +}) + +export const selectGroupInfo = groupsApi.endpoints.getGroups.select(); \ No newline at end of file diff --git a/src/client/app/redux/api/metersApi.ts b/src/client/app/redux/api/metersApi.ts new file mode 100644 index 000000000..657fc6dc6 --- /dev/null +++ b/src/client/app/redux/api/metersApi.ts @@ -0,0 +1,20 @@ +import { baseApi } from './baseApi' +import * as _ from 'lodash'; +import { MeterData, MeterDataByID } from '../../types/redux/meters' +import { durationFormat } from '../../utils/durationFormat'; + + +export const metersApi = baseApi.injectEndpoints({ + endpoints: builder => ({ + getMeters: builder.query({ + query: () => 'api/meters', + transformResponse: (response: MeterData[]) => { + response.forEach(meter => { meter.readingFrequency = durationFormat(meter.readingFrequency) }); + return _.keyBy(response, meter => meter.id) + } + }) + }) +}) + +export const { useGetMetersQuery } = metersApi; +export const selectMeterInfo = metersApi.endpoints.getMeters.select() diff --git a/src/client/app/redux/api/readingsApi.ts b/src/client/app/redux/api/readingsApi.ts new file mode 100644 index 000000000..5be6e5681 --- /dev/null +++ b/src/client/app/redux/api/readingsApi.ts @@ -0,0 +1,25 @@ +import { baseApi } from './baseApi' +import { ThreeDReading } from '../../types/readings' +import { MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; + + +export type ThreeDReadingApiParams = { + meterID: number; + timeInterval: string; + unitID: number; + readingInterval: ReadingInterval; + meterOrGroup: MeterOrGroup; +}; + +export const readingsApi = baseApi.injectEndpoints({ + endpoints: builder => ({ + threeD: builder.query({ + query: ({ meterID, timeInterval, unitID, readingInterval, meterOrGroup }) => { + const endpoint = `/api/unitReadings/threeD/${meterOrGroup}/` + const args = `${meterID}?timeInterval=${timeInterval.toString()}&graphicUnitId=${unitID}&readingInterval=${readingInterval}` + return `${endpoint}${args}` + } + }) + }) +}) +export const selectThreeDReadingData = readingsApi.endpoints.threeD.select \ No newline at end of file diff --git a/src/client/app/redux/hooks.ts b/src/client/app/redux/hooks.ts new file mode 100644 index 000000000..e59f17d0c --- /dev/null +++ b/src/client/app/redux/hooks.ts @@ -0,0 +1,7 @@ +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' +import type { RootState, AppDispatch } from '../store' + +// https://react-redux.js.org/using-react-redux/usage-with-typescript#define-typed-hooks +// Use throughout your app instead of plain `useDispatch` and `useSelector` +export const useAppDispatch: () => AppDispatch = useDispatch +export const useAppSelector: TypedUseSelectorHook = useSelector \ No newline at end of file diff --git a/src/client/app/redux/selectors/threeDSelectors.ts b/src/client/app/redux/selectors/threeDSelectors.ts new file mode 100644 index 000000000..f3de7fe66 --- /dev/null +++ b/src/client/app/redux/selectors/threeDSelectors.ts @@ -0,0 +1,71 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { selectMeterInfo } from '../../redux/api/metersApi'; +import { selectGroupInfo } from '../../redux/api/groupsApi'; +import { RootState } from '../../store' +import { MeterOrGroup } from '../../types/redux/graph' +import { AreaUnitType } from '../../utils/getAreaUnitConversion'; +import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatability'; +import { ThreeDReadingApiParams } from '../api/readingsApi' + +// Common Fine Grained selectors +const selectThreeDMeterOrGroupID = (state: RootState) => state.graph.threeD.meterOrGroupID; +const selectThreeDMeterOrGroup = (state: RootState) => state.graph.threeD.meterOrGroup; +const selectGraphTimeInterval = (state: RootState) => state.graph.timeInterval; +const selectGraphUnitID = (state: RootState) => state.graph.selectedUnit; +const selectThreeDReadingInterval = (state: RootState) => state.graph.threeD.readingInterval; +const selectMeterData = (state: RootState) => selectMeterInfo(state).data +const selectGroupData = (state: RootState) => selectGroupInfo(state).data + +// Memoized Selectors +export const selectThreeDComponentInfo = createSelector( + [selectThreeDMeterOrGroupID, selectThreeDMeterOrGroup, selectMeterData, selectGroupData], + (id, meterOrGroup, meterData, groupData) => { + //Default Values + let meterOrGroupName = 'Unselected Meter or Group' + let isAreaCompatible = true; + + if (id) { + // Get Meter or Group's info + if (meterOrGroup === MeterOrGroup.meters && meterData) { + const meterInfo = meterData[id] + meterOrGroupName = meterInfo.identifier; + isAreaCompatible = meterInfo.area !== 0 && meterInfo.areaUnit !== AreaUnitType.none; + } else if (meterOrGroup === MeterOrGroup.meters && groupData) { + const groupInfo = groupData[id]; + meterOrGroupName = groupInfo.name; + isAreaCompatible = groupInfo.area !== 0 && groupInfo.areaUnit !== AreaUnitType.none; + } + + } + return { + meterOrGroupID: id, + // meterOrGroup: meterOrGroup, + meterOrGroupName: meterOrGroupName, + isAreaCompatible: isAreaCompatible + } + } + +) + +export const selectThreeDQueryArgs = createSelector( + selectThreeDMeterOrGroupID, + selectGraphTimeInterval, + selectGraphUnitID, + selectThreeDReadingInterval, + selectThreeDMeterOrGroup, + (id, timeInterval, unitID, readingInterval, meterOrGroup) => { + return { + meterID: id, + timeInterval: roundTimeIntervalForFetch(timeInterval).toString(), + unitID: unitID, + readingInterval: readingInterval, + meterOrGroup: meterOrGroup + } as ThreeDReadingApiParams + } +) + +export const selectThreeDSkip = createSelector( + selectThreeDMeterOrGroupID, + selectGraphTimeInterval, + (id, interval) => !id || !interval.getIsBounded() +) \ No newline at end of file diff --git a/src/client/app/store.ts b/src/client/app/store.ts new file mode 100644 index 000000000..fbd27b721 --- /dev/null +++ b/src/client/app/store.ts @@ -0,0 +1,21 @@ +/* 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 { configureStore } from '@reduxjs/toolkit' +import reducers from './reducers'; +import { baseApi } from './redux/api/baseApi'; + + +export const store = configureStore({ + reducer: reducers, + middleware: getDefaultMiddleware => getDefaultMiddleware({ + immutableCheck: false, + serializableCheck: false + }).concat(baseApi.middleware) +}); + +// Infer the `RootState` and `AppDispatch` types from the store itself +// https://react-redux.js.org/using-react-redux/usage-with-typescript#define-root-state-and-dispatch-types +export type RootState = ReturnType +export type AppDispatch = typeof store.dispatch \ No newline at end of file diff --git a/src/client/app/types/redux/state.ts b/src/client/app/types/redux/state.ts index 40f4c0fa6..22691cf2a 100644 --- a/src/client/app/types/redux/state.ts +++ b/src/client/app/types/redux/state.ts @@ -4,7 +4,6 @@ import { BarReadingsState } from './barReadings'; import { LineReadingsState } from './lineReadings'; -import { ThreeDReadingsState } from './threeDReadings'; import { GraphState } from './graph'; import { GroupsState } from './groups'; import { MetersState } from './meters'; @@ -24,7 +23,6 @@ export interface State { line: LineReadingsState; bar: BarReadingsState; compare: CompareReadingsState; - threeD: ThreeDReadingsState; }; graph: GraphState; maps: MapState; From 5bc53844b8bc4f51875c8c1b8939c60204b8c570 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Mon, 18 Sep 2023 03:22:47 +0000 Subject: [PATCH 003/131] Initial Graph Slice Reducer Refactor --- src/client/app/actions/admin.ts | 9 +- src/client/app/actions/graph.ts | 116 +++-------- .../components/AreaUnitSelectComponent.tsx | 6 +- .../components/ChartDataSelectComponent.tsx | 22 +-- .../app/components/ChartSelectComponent.tsx | 11 +- .../app/components/ErrorBarComponent.tsx | 4 +- .../components/GraphicRateMenuComponent.tsx | 4 +- .../app/components/UIOptionsComponent.tsx | 8 +- src/client/app/containers/RouteContainer.ts | 5 +- .../app/containers/UIOptionsContainer.ts | 4 +- src/client/app/reducers/graph.ts | 185 +++++++----------- src/client/app/reducers/index.ts | 4 +- src/client/app/types/redux/graph.ts | 118 ----------- 13 files changed, 137 insertions(+), 359 deletions(-) diff --git a/src/client/app/actions/admin.ts b/src/client/app/actions/admin.ts index 178150cc7..27449f6d1 100644 --- a/src/client/app/actions/admin.ts +++ b/src/client/app/actions/admin.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 { toggleAreaNormalization, changeBarStacking, changeChartToRender } from './graph'; import { showErrorNotification, showSuccessNotification } from '../utils/notifications'; import { ChartTypes } from '../types/redux/graph'; import { PreferenceRequestItem } from '../types/items'; @@ -15,7 +14,7 @@ import { LanguageTypes } from '../types/redux/i18n'; import * as moment from 'moment'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; import { updateSelectedLanguage } from './options'; - +import { graphSlice } from '../reducers/graph'; export function updateSelectedMeter(meterID: number): t.UpdateImportMeterAction { return { type: ActionType.UpdateImportMeter, meterID }; } @@ -117,12 +116,12 @@ function fetchPreferences(): Thunk { if (!getState().graph.hotlinked) { dispatch((dispatch2: Dispatch) => { const state = getState(); - dispatch2(changeChartToRender(state.admin.defaultChartToRender)); + dispatch2(graphSlice.actions.changeChartToRender(state.admin.defaultChartToRender)); if (preferences.defaultBarStacking !== state.graph.barStacking) { - dispatch2(changeBarStacking()); + dispatch2(graphSlice.actions.changeBarStacking()); } if (preferences.defaultAreaNormalization !== state.graph.areaNormalization) { - dispatch2(toggleAreaNormalization()); + dispatch2(graphSlice.actions.toggleAreaNormalization()); } if (preferences.defaultLanguage !== state.options.selectedLanguage) { dispatch2(updateSelectedLanguage(preferences.defaultLanguage)); diff --git a/src/client/app/actions/graph.ts b/src/client/app/actions/graph.ts index bc4ad23e1..740c5a2f9 100644 --- a/src/client/app/actions/graph.ts +++ b/src/client/app/actions/graph.ts @@ -9,7 +9,7 @@ import { fetchNeededLineReadings } from './lineReadings'; import { fetchNeededBarReadings } from './barReadings'; import { fetchNeededCompareReadings } from './compareReadings'; import { TimeInterval } from '../../../common/TimeInterval'; -import { Dispatch, Thunk, ActionType, GetState } from '../types/redux/actions'; +import { Dispatch, Thunk, GetState } from '../types/redux/actions'; import { State } from '../types/redux/state'; import * as t from '../types/redux/graph'; import * as m from '../types/redux/map'; @@ -18,83 +18,33 @@ import { fetchNeededMapReadings } from './mapReadings'; import { changeSelectedMap, fetchMapsDetails } from './map'; import { fetchUnitsDetailsIfNeeded } from './units'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; - -export function changeRenderOnce() { - return { type: ActionType.ConfirmGraphRenderOnce }; -} - -export function changeChartToRender(chartType: t.ChartTypes): t.ChangeChartToRenderAction { - return { type: ActionType.ChangeChartToRender, chartType }; -} - -export function toggleAreaNormalization(): t.ToggleAreaNormalizationAction { - return { type: ActionType.ToggleAreaNormalization }; -} -export function toggleShowMinMax(): t.ToggleShowMinMaxAction { - return { type: ActionType.ToggleShowMinMax } -} - -export function changeBarStacking(): t.ChangeBarStackingAction { - return { type: ActionType.ChangeBarStacking }; -} - -export function updateSelectedMeters(meterIDs: number[]): t.UpdateSelectedMetersAction { - return { type: ActionType.UpdateSelectedMeters, meterIDs }; -} - -export function updateSelectedGroups(groupIDs: number[]): t.UpdateSelectedGroupsAction { - return { type: ActionType.UpdateSelectedGroups, groupIDs }; -} - -export function updateSelectedUnit(unitID: number): t.UpdateSelectedUnitAction { - return { type: ActionType.UpdateSelectedUnit, unitID }; -} - -export function updateSelectedAreaUnit(areaUnit: AreaUnitType): t.UpdateSelectedAreaUnitAction { - return { type: ActionType.UpdateSelectedAreaUnit, areaUnit }; -} - -export function updateBarDuration(barDuration: moment.Duration): t.UpdateBarDurationAction { - return { type: ActionType.UpdateBarDuration, barDuration }; -} - -export function updateLineGraphRate(lineGraphRate: t.LineGraphRate): t.UpdateLineGraphRate { - return { type: ActionType.UpdateLineGraphRate, lineGraphRate } -} - -export function setHotlinked(hotlinked: boolean): t.SetHotlinked { - return { type: ActionType.SetHotlinked, hotlinked }; -} +import { graphSlice } from '../reducers/graph'; export function setHotlinkedAsync(hotlinked: boolean): Thunk { return (dispatch: Dispatch) => { - dispatch(setHotlinked(hotlinked)); + dispatch(graphSlice.actions.setHotlinked(hotlinked)); return Promise.resolve(); }; } -export function toggleOptionsVisibility(): t.ToggleOptionsVisibility { - return { type: ActionType.ToggleOptionsVisibility }; +export function toggleOptionsVisibility() { + return graphSlice.actions.toggleOptionsVisibility(); } -function changeGraphZoom(timeInterval: TimeInterval): t.ChangeGraphZoomAction { - return { type: ActionType.ChangeGraphZoom, timeInterval }; +function changeGraphZoom(timeInterval: TimeInterval) { + return graphSlice.actions.changeGraphZoom(timeInterval); } export function changeBarDuration(barDuration: moment.Duration): Thunk { return (dispatch: Dispatch, getState: GetState) => { - dispatch(updateBarDuration(barDuration)); + dispatch(graphSlice.actions.updateBarDuration(barDuration)); dispatch(fetchNeededBarReadings(getState().graph.timeInterval, getState().graph.selectedUnit)); return Promise.resolve(); }; } -function updateComparePeriod(comparePeriod: ComparePeriod, currentTime: moment.Moment): t.UpdateComparePeriodAction { - return { - type: ActionType.UpdateComparePeriod, - comparePeriod, - currentTime - }; +function updateComparePeriod(comparePeriod: ComparePeriod, currentTime: moment.Moment) { + return graphSlice.actions.updateComparePeriod({ comparePeriod, currentTime }); } export function changeCompareGraph(comparePeriod: ComparePeriod): Thunk { @@ -111,13 +61,13 @@ export function changeCompareGraph(comparePeriod: ComparePeriod): Thunk { }; } -export function changeCompareSortingOrder(compareSortingOrder: SortingOrder): t.ChangeCompareSortingOrderAction { - return { type: ActionType.ChangeCompareSortingOrder, compareSortingOrder }; +export function changeCompareSortingOrder(compareSortingOrder: SortingOrder) { + return graphSlice.actions.changeCompareSortingOrder(compareSortingOrder); } export function changeSelectedMeters(meterIDs: number[]): Thunk { return (dispatch: Dispatch, getState: GetState) => { - dispatch(updateSelectedMeters(meterIDs)); + dispatch(graphSlice.actions.updateSelectedMeters(meterIDs)); // Nesting dispatches to preserve that updateSelectedMeters() is called before fetching readings dispatch((dispatch2: Dispatch) => { dispatch2(fetchNeededLineReadings(getState().graph.timeInterval, getState().graph.selectedUnit)); @@ -131,7 +81,7 @@ export function changeSelectedMeters(meterIDs: number[]): Thunk { export function changeSelectedGroups(groupIDs: number[]): Thunk { return (dispatch: Dispatch, getState: GetState) => { - dispatch(updateSelectedGroups(groupIDs)); + dispatch(graphSlice.actions.updateSelectedGroups(groupIDs)); // Nesting dispatches to preserve that updateSelectedGroups() is called before fetching readings dispatch((dispatch2: Dispatch) => { dispatch2(fetchNeededLineReadings(getState().graph.timeInterval, getState().graph.selectedUnit)); @@ -145,7 +95,7 @@ export function changeSelectedGroups(groupIDs: number[]): Thunk { export function changeSelectedUnit(unitID: number): Thunk { return (dispatch: Dispatch, getState: GetState) => { - dispatch(updateSelectedUnit(unitID)); + dispatch(graphSlice.actions.updateSelectedUnit(unitID)); dispatch((dispatch2: Dispatch) => { dispatch(fetchNeededLineReadings(getState().graph.timeInterval, unitID)); dispatch2(fetchNeededBarReadings(getState().graph.timeInterval, unitID)); @@ -184,16 +134,16 @@ function shouldChangeRangeSlider(range: TimeInterval): boolean { return range !== TimeInterval.unbounded(); } -function changeRangeSlider(sliderInterval: TimeInterval): t.ChangeSliderRangeAction { - return { type: ActionType.ChangeSliderRange, sliderInterval }; +function changeRangeSlider(sliderInterval: TimeInterval) { + return graphSlice.actions.changeSliderRange(sliderInterval); } /** * remove constraints for rangeslider after user clicked redraw or restore * by setting sliderRange to an empty string */ -function resetRangeSliderStack(): t.ResetRangeSliderStackAction { - return { type: ActionType.ResetRangeSliderStack }; +function resetRangeSliderStack() { + return graphSlice.actions.resetRangeSliderStack(); } function changeRangeSliderIfNeeded(interval: TimeInterval): Thunk { @@ -207,13 +157,13 @@ function changeRangeSliderIfNeeded(interval: TimeInterval): Thunk { export function updateThreeDReadingInterval(readingInterval: t.ReadingInterval): Thunk { return (dispatch: Dispatch) => { - dispatch({ type: ActionType.UpdateThreeDReadingInterval, readingInterval }); + dispatch(graphSlice.actions.updateThreeDReadingInterval(readingInterval)); return Promise.resolve(); }; } -export function updateThreeDMeterOrGroupInfo(meterOrGroupID: t.MeterOrGroupID, meterOrGroup: t.MeterOrGroup): t.UpdateThreeDMeterOrGroupInfo { - return { type: ActionType.UpdateThreeDMeterOrGroupInfo, meterOrGroupID, meterOrGroup }; +export function updateThreeDMeterOrGroupInfo(meterOrGroupID: t.MeterOrGroupID, meterOrGroup: t.MeterOrGroup) { + return graphSlice.actions.updateThreeDMeterOrGroupInfo({ meterOrGroupID, meterOrGroup }); } export function changeMeterOrGroupInfo(meterOrGroupID: t.MeterOrGroupID, meterOrGroup: t.MeterOrGroup = t.MeterOrGroup.meters): Thunk { @@ -252,15 +202,7 @@ export interface LinkOptions { */ export function changeOptionsFromLink(options: LinkOptions) { const dispatchFirst: Thunk[] = [setHotlinkedAsync(true)]; - // Visual Studio indents after the first line in autoformat but ESLint does not like that in this case so override. - /* eslint-disable @typescript-eslint/indent */ - const dispatchSecond: Array = []; - /* eslint-enable @typescript-eslint/indent */ - + const dispatchSecond: Array> = []; if (options.meterIDs) { dispatchFirst.push(fetchMetersDetailsIfNeeded()); dispatchSecond.push(changeSelectedMeters(options.meterIDs)); @@ -273,14 +215,14 @@ export function changeOptionsFromLink(options: LinkOptions) { dispatchSecond.push(updateThreeDMeterOrGroupInfo(options.meterOrGroupID, options.meterOrGroup)); } if (options.chartType) { - dispatchSecond.push(changeChartToRender(options.chartType)); + dispatchSecond.push(graphSlice.actions.changeChartToRender(options.chartType)); } if (options.unitID) { dispatchFirst.push(fetchUnitsDetailsIfNeeded()); dispatchSecond.push(changeSelectedUnit(options.unitID)); } if (options.rate) { - dispatchSecond.push(updateLineGraphRate(options.rate)); + dispatchSecond.push(graphSlice.actions.updateLineGraphRate(options.rate)); } if (options.barDuration) { dispatchFirst.push(changeBarDuration(options.barDuration)); @@ -292,16 +234,16 @@ export function changeOptionsFromLink(options: LinkOptions) { dispatchSecond.push(changeRangeSliderIfNeeded(options.sliderRange)); } if (options.toggleAreaNormalization) { - dispatchSecond.push(toggleAreaNormalization()); + dispatchSecond.push(graphSlice.actions.toggleAreaNormalization()); } if (options.areaUnit) { - dispatchSecond.push(updateSelectedAreaUnit(options.areaUnit as AreaUnitType)); + dispatchSecond.push(graphSlice.actions.updateSelectedAreaUnit(options.areaUnit as AreaUnitType)); } if (options.toggleMinMax) { - dispatchSecond.push(toggleShowMinMax()); + dispatchSecond.push(graphSlice.actions.toggleShowMinMax()); } if (options.toggleBarStacking) { - dispatchSecond.push(changeBarStacking()); + dispatchSecond.push(graphSlice.actions.changeBarStacking()); } if (options.comparePeriod) { dispatchSecond.push(changeCompareGraph(options.comparePeriod)); diff --git a/src/client/app/components/AreaUnitSelectComponent.tsx b/src/client/app/components/AreaUnitSelectComponent.tsx index 5dcf5bff8..919be1eb1 100644 --- a/src/client/app/components/AreaUnitSelectComponent.tsx +++ b/src/client/app/components/AreaUnitSelectComponent.tsx @@ -6,13 +6,13 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { useDispatch, useSelector } from 'react-redux'; import Select from 'react-select'; -import { toggleAreaNormalization, updateSelectedAreaUnit } from '../actions/graph'; import { StringSelectOption } from '../types/items'; import { State } from '../types/redux/state'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; import translate from '../utils/translate'; import { UnitRepresentType } from '../types/redux/units'; import TooltipMarkerComponent from './TooltipMarkerComponent'; +import { graphSlice } from '../reducers/graph'; /** * React Component that creates the area unit selector dropdown @@ -38,7 +38,7 @@ export default function AreaUnitSelectComponent() { }); const handleToggleAreaNormalization = () => { - dispatch(toggleAreaNormalization()); + dispatch(graphSlice.actions.toggleAreaNormalization()); } const labelStyle: React.CSSProperties = { @@ -76,7 +76,7 @@ export default function AreaUnitSelectComponent() { value={{ label: translate(`AreaUnitType.${graphState.selectedAreaUnit}`), value: graphState.selectedAreaUnit} as StringSelectOption} onChange={newSelectedUnit => { if (newSelectedUnit) { - dispatch(updateSelectedAreaUnit(newSelectedUnit.value as AreaUnitType)) + dispatch(graphSlice.actions.updateSelectedAreaUnit(newSelectedUnit.value as AreaUnitType)) } }} /> diff --git a/src/client/app/components/ChartDataSelectComponent.tsx b/src/client/app/components/ChartDataSelectComponent.tsx index bbb135212..cc84e0080 100644 --- a/src/client/app/components/ChartDataSelectComponent.tsx +++ b/src/client/app/components/ChartDataSelectComponent.tsx @@ -16,10 +16,7 @@ import { CartesianPoint, Dimensions, normalizeImageDimensions, calculateScaleFromEndpoints, itemDisplayableOnMap, itemMapInfoOk, gpsToUserGrid } from '../utils/calibration'; -import { - changeSelectedGroups, changeSelectedMeters, changeSelectedUnit, updateSelectedMeters, - updateSelectedGroups, updateSelectedUnit, changeMeterOrGroupInfo -} from '../actions/graph'; +import { changeSelectedGroups, changeSelectedMeters, changeSelectedUnit, changeMeterOrGroupInfo } from '../actions/graph'; import { DisplayableType, UnitData, UnitRepresentType, UnitType } from '../types/redux/units' import { metersInGroup, unitsCompatibleWithMeters } from '../utils/determineCompatibleUnits'; import { Dispatch } from '../types/redux/actions'; @@ -27,6 +24,7 @@ import { UnitsState } from '../types/redux/units'; import { MetersState } from 'types/redux/meters'; import { GroupsState } from 'types/redux/groups'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; +import { graphSlice } from '../reducers/graph'; /** * A component which allows the user to select which data should be displayed on the chart. @@ -213,7 +211,7 @@ export default function ChartDataSelectComponent() { // The selectedUnit becomes the unit of the group selected. Note is should always be set (not -99) since // those groups should not have been visible. The only exception is if there are no selected groups but // then this loop does not run. The loop is assumed to only run once in this case. - state.graph.selectedUnit = state.groups.byGroupID[groupID].defaultGraphicUnit; + // state.graph.selectedUnit = state.groups.byGroupID[groupID].defaultGraphicUnit; } compatibleSelectedGroups.push({ // For groups we display the name since no identifier. @@ -264,10 +262,10 @@ export default function ChartDataSelectComponent() { ); } - // if no area unit selected, set the default area as selected. - if (state.graph.selectedAreaUnit == AreaUnitType.none) { - state.graph.selectedAreaUnit = state.admin.defaultAreaUnit; - } + // // if no area unit selected, set the default area as selected. + // if (state.graph.selectedAreaUnit == AreaUnitType.none) { + // state.graph.selectedAreaUnit = state.admin.defaultAreaUnit; + // } return { // all items, sorted alphabetically and by compatibility @@ -378,9 +376,9 @@ export default function ChartDataSelectComponent() { // Update the selected meters and groups to empty to avoid graphing errors // The update selected meters/groups functions are essentially the same as the change functions // However, they do not attempt to graph. - dispatch(updateSelectedGroups([])); - dispatch(updateSelectedMeters([])); - dispatch(updateSelectedUnit(-99)); + dispatch(graphSlice.actions.updateSelectedGroups([])); + dispatch(graphSlice.actions.updateSelectedMeters([])); + dispatch(graphSlice.actions.updateSelectedUnit(-99)); // Sync threeD state. dispatch(changeMeterOrGroupInfo(null)); } diff --git a/src/client/app/components/ChartSelectComponent.tsx b/src/client/app/components/ChartSelectComponent.tsx index c04bdf99b..83272e84e 100644 --- a/src/client/app/components/ChartSelectComponent.tsx +++ b/src/client/app/components/ChartSelectComponent.tsx @@ -13,6 +13,7 @@ import { useState } from 'react'; import { SelectOption } from '../types/items'; import { Dispatch } from '../types/redux/actions'; import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap'; +import { graphSlice } from '../reducers/graph'; /** * A component that allows users to select which chart should be displayed. @@ -45,23 +46,23 @@ export default function ChartSelectComponent() { dispatch({ type: 'CHANGE_CHART_TO_RENDER', chartType: ChartTypes.line })} + onClick={() => dispatch(graphSlice.actions.changeChartToRender(ChartTypes.line))} > dispatch({ type: 'CHANGE_CHART_TO_RENDER', chartType: ChartTypes.bar })} + onClick={() => dispatch(graphSlice.actions.changeChartToRender(ChartTypes.bar))} > dispatch({ type: 'CHANGE_CHART_TO_RENDER', chartType: ChartTypes.compare })} + onClick={() => dispatch(graphSlice.actions.changeChartToRender(ChartTypes.compare))} > { - dispatch({ type: 'CHANGE_CHART_TO_RENDER', chartType: ChartTypes.map }); + dispatch(graphSlice.actions.changeChartToRender(ChartTypes.map)); if (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 }); @@ -71,7 +72,7 @@ export default function ChartSelectComponent() { dispatch({ type: 'CHANGE_CHART_TO_RENDER', chartType: ChartTypes.threeD })} + onClick={() => dispatch(graphSlice.actions.changeChartToRender(ChartTypes.threeD))} > diff --git a/src/client/app/components/ErrorBarComponent.tsx b/src/client/app/components/ErrorBarComponent.tsx index faf4608d6..f8a83cea9 100644 --- a/src/client/app/components/ErrorBarComponent.tsx +++ b/src/client/app/components/ErrorBarComponent.tsx @@ -5,9 +5,9 @@ import * as React from 'react'; import { useSelector, useDispatch } from 'react-redux'; import { State } from '../types/redux/state'; -import { toggleShowMinMax } from '../actions/graph'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; +import { graphSlice } from '../reducers/graph'; /** * React Component rendering an Error Bar checkbox for toggle operation. @@ -21,7 +21,7 @@ export default function ErrorBarComponent() { * Dispatches an action to toggle visibility of min/max lines on checkbox interaction */ const handleToggleShowMinMax = () => { - dispatch(toggleShowMinMax()); + dispatch(graphSlice.actions.toggleShowMinMax()); } return ( diff --git a/src/client/app/components/GraphicRateMenuComponent.tsx b/src/client/app/components/GraphicRateMenuComponent.tsx index edc9a33e5..4b4d006de 100644 --- a/src/client/app/components/GraphicRateMenuComponent.tsx +++ b/src/client/app/components/GraphicRateMenuComponent.tsx @@ -9,9 +9,9 @@ import { useDispatch, useSelector } from 'react-redux'; import { SelectOption } from '../types/items'; import Select from 'react-select'; import translate from '../utils/translate'; -import { updateLineGraphRate } from '../actions/graph' import { ChartTypes, LineGraphRate, LineGraphRates } from '../types/redux/graph'; import TooltipMarkerComponent from './TooltipMarkerComponent'; +import { graphSlice } from '../reducers/graph'; /** * React component that controls the line graph rate menu @@ -55,7 +55,7 @@ export default function GraphicRateMenuComponent() { value={{ label: translate(graphState.lineGraphRate.label), value: graphState.lineGraphRate.rate } as SelectOption} onChange={newSelectedRate => { if (newSelectedRate) { - dispatch(updateLineGraphRate({ + dispatch(graphSlice.actions.updateLineGraphRate({ label: newSelectedRate.labelIdForTranslate, rate: Number(newSelectedRate.value) } as LineGraphRate)) diff --git a/src/client/app/components/UIOptionsComponent.tsx b/src/client/app/components/UIOptionsComponent.tsx index a628a5b4d..0a1e7dd8c 100644 --- a/src/client/app/components/UIOptionsComponent.tsx +++ b/src/client/app/components/UIOptionsComponent.tsx @@ -10,7 +10,6 @@ import { Button, ButtonGroup, Dropdown, DropdownToggle, DropdownMenu, DropdownIt import ExportComponent from '../components/ExportComponent'; import ChartSelectComponent from './ChartSelectComponent'; import ChartDataSelectComponent from './ChartDataSelectComponent'; -import { ChangeBarStackingAction, ChangeCompareSortingOrderAction, ToggleOptionsVisibility } from '../types/redux/graph'; import ChartLinkContainer from '../containers/ChartLinkContainer'; import { ChartTypes } from '../types/redux/graph'; import { ComparePeriod, SortingOrder } from '../utils/calculateCompare'; @@ -23,6 +22,7 @@ import AreaUnitSelectComponent from './AreaUnitSelectComponent'; import ErrorBarComponent from './ErrorBarComponent'; import DateRangeComponent from './DateRangeComponent'; import ThreeDSelectComponent from './ReadingsPerDaySelectComponent'; +import { graphSlice } from '../reducers/graph'; const Slider = createSliderWithTooltip(sliderWithoutTooltips); @@ -34,10 +34,10 @@ export interface UIOptionsProps { compareSortingOrder: SortingOrder; optionsVisibility: boolean; changeDuration(duration: moment.Duration): Promise; - changeBarStacking(): ChangeBarStackingAction; - toggleOptionsVisibility(): ToggleOptionsVisibility; + changeBarStacking(): ReturnType; + toggleOptionsVisibility(): ReturnType; changeCompareGraph(comparePeriod: ComparePeriod): Promise; - changeCompareSortingOrder(compareSortingOrder: SortingOrder): ChangeCompareSortingOrderAction; + changeCompareSortingOrder(compareSortingOrder: SortingOrder): ReturnType; } type UIOptionsPropsWithIntl = UIOptionsProps & WrappedComponentProps; diff --git a/src/client/app/containers/RouteContainer.ts b/src/client/app/containers/RouteContainer.ts index ad383881d..9019e2f11 100644 --- a/src/client/app/containers/RouteContainer.ts +++ b/src/client/app/containers/RouteContainer.ts @@ -6,10 +6,11 @@ import { connect } from 'react-redux'; import RouteComponent from '../components/RouteComponent'; import { Dispatch } from '../types/redux/actions'; import { State } from '../types/redux/state'; -import { changeOptionsFromLink, LinkOptions, changeRenderOnce } from '../actions/graph'; +import { changeOptionsFromLink, LinkOptions } from '../actions/graph'; import { clearCurrentUser } from '../actions/currentUser'; import { isRoleAdmin } from '../utils/hasPermissions'; import { UserRole } from '../types/items'; +import { graphSlice } from '../reducers/graph'; function mapStateToProps(state: State) { const currentUser = state.currentUser.profile; @@ -37,7 +38,7 @@ function mapDispatchToProps(dispatch: Dispatch) { changeOptionsFromLink: (options: LinkOptions) => dispatch(changeOptionsFromLink(options)), clearCurrentUser: () => dispatch(clearCurrentUser()), // Set the state to indicate chartlinks have been rendered. - changeRenderOnce: () => dispatch(changeRenderOnce()) + changeRenderOnce: () => dispatch(graphSlice.actions.confirmGraphRenderOnce()) }; } diff --git a/src/client/app/containers/UIOptionsContainer.ts b/src/client/app/containers/UIOptionsContainer.ts index be3c8fb05..7e7c00949 100644 --- a/src/client/app/containers/UIOptionsContainer.ts +++ b/src/client/app/containers/UIOptionsContainer.ts @@ -7,7 +7,6 @@ import { connect } from 'react-redux'; import UIOptionsComponent from '../components/UIOptionsComponent'; import { changeBarDuration, - changeBarStacking, changeCompareGraph, changeCompareSortingOrder, toggleOptionsVisibility @@ -15,6 +14,7 @@ import { import { Dispatch } from '../types/redux/actions'; import { State } from '../types/redux/state'; import {ComparePeriod, SortingOrder} from '../utils/calculateCompare'; +import { graphSlice } from '../reducers/graph'; function mapStateToProps(state: State) { return { @@ -31,7 +31,7 @@ function mapStateToProps(state: State) { function mapDispatchToProps(dispatch: Dispatch) { return { changeDuration: (barDuration: moment.Duration) => dispatch(changeBarDuration(barDuration)), - changeBarStacking: () => dispatch(changeBarStacking()), + changeBarStacking: () => dispatch(graphSlice.actions.changeBarStacking()), changeCompareGraph: (comparePeriod: ComparePeriod) => dispatch(changeCompareGraph(comparePeriod)), changeCompareSortingOrder: (sortingOrder: SortingOrder) => dispatch(changeCompareSortingOrder(sortingOrder)), toggleOptionsVisibility: () => dispatch(toggleOptionsVisibility()) diff --git a/src/client/app/reducers/graph.ts b/src/client/app/reducers/graph.ts index a347f95cc..506312e3d 100644 --- a/src/client/app/reducers/graph.ts +++ b/src/client/app/reducers/graph.ts @@ -4,10 +4,11 @@ import * as moment from 'moment'; import { TimeInterval } from '../../../common/TimeInterval'; -import { GraphAction, GraphState, ChartTypes, ReadingInterval, MeterOrGroup } from '../types/redux/graph'; -import { ActionType } from '../types/redux/actions'; +import { GraphState, ChartTypes, ReadingInterval, MeterOrGroup, LineGraphRate } from '../types/redux/graph'; import { calculateCompareTimeInterval, ComparePeriod, SortingOrder } from '../utils/calculateCompare'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; +import { createSlice } from '@reduxjs/toolkit' +import type { PayloadAction } from '@reduxjs/toolkit' const defaultState: GraphState = { selectedMeters: [], @@ -35,118 +36,72 @@ const defaultState: GraphState = { } }; -export default function graph(state = defaultState, action: GraphAction) { - switch (action.type) { - case ActionType.ConfirmGraphRenderOnce: { - return { - ...state, - renderOnce: true - }; +export const graphSlice = createSlice({ + name: 'graph', + initialState: defaultState, + reducers: { + confirmGraphRenderOnce: state => { + state.renderOnce = true + }, + updateSelectedMeters: (state, action: PayloadAction) => { + state.selectedMeters = action.payload + }, + updateSelectedGroups: (state, action: PayloadAction) => { + state.selectedGroups = action.payload + }, + updateSelectedUnit: (state, action: PayloadAction) => { + state.selectedUnit = action.payload + }, + updateSelectedAreaUnit: (state, action: PayloadAction) => { + state.selectedAreaUnit = action.payload + }, + updateBarDuration: (state, action: PayloadAction) => { + state.barDuration = action.payload + }, + changeGraphZoom: (state, action: PayloadAction) => { + state.timeInterval = action.payload + }, + changeSliderRange: (state, action: PayloadAction) => { + state.rangeSliderInterval = action.payload + }, + resetRangeSliderStack: state => { + state.rangeSliderInterval = TimeInterval.unbounded() + }, + updateComparePeriod: (state, action: PayloadAction<{ comparePeriod: ComparePeriod, currentTime: moment.Moment }>) => { + state.comparePeriod = action.payload.comparePeriod + state.compareTimeInterval = calculateCompareTimeInterval(action.payload.comparePeriod, action.payload.currentTime) + }, + changeChartToRender: (state, action: PayloadAction) => { + state.chartToRender = action.payload + }, + toggleAreaNormalization: state => { + state.areaNormalization = !state.areaNormalization + }, + toggleShowMinMax: state => { + state.showMinMax = !state.showMinMax + }, + changeBarStacking: state => { + state.barStacking = !state.barStacking + }, + setHotlinked: (state, action: PayloadAction) => { + state.hotlinked = action.payload + }, + changeCompareSortingOrder: (state, action: PayloadAction) => { + state.compareSortingOrder = action.payload + }, + toggleOptionsVisibility: state => { + state.optionsVisibility = !state.optionsVisibility + }, + updateLineGraphRate: (state, action: PayloadAction) => { + state.lineGraphRate = action.payload + }, + + updateThreeDReadingInterval: (state, action: PayloadAction) => { + state.threeD.readingInterval = action.payload + }, + updateThreeDMeterOrGroupInfo: (state, action: PayloadAction<{ meterOrGroupID: number | null, meterOrGroup: MeterOrGroup }>) => { + state.threeD.meterOrGroupID = action.payload.meterOrGroupID + state.threeD.meterOrGroup = action.payload.meterOrGroup } - case ActionType.UpdateSelectedMeters: - return { - ...state, - selectedMeters: action.meterIDs - }; - case ActionType.UpdateSelectedGroups: - return { - ...state, - selectedGroups: action.groupIDs - }; - case ActionType.UpdateSelectedUnit: - return { - ...state, - selectedUnit: action.unitID - } - case ActionType.UpdateSelectedAreaUnit: - return { - ...state, - selectedAreaUnit: action.areaUnit - } - case ActionType.UpdateBarDuration: - return { - ...state, - barDuration: action.barDuration - }; - case ActionType.ChangeGraphZoom: - return { - ...state, - timeInterval: action.timeInterval - }; - case ActionType.ChangeSliderRange: - return { - ...state, - rangeSliderInterval: action.sliderInterval - }; - case ActionType.ResetRangeSliderStack: - return { - ...state, - rangeSliderInterval: TimeInterval.unbounded() - }; - case ActionType.UpdateComparePeriod: - return { - ...state, - comparePeriod: action.comparePeriod, - compareTimeInterval: calculateCompareTimeInterval(action.comparePeriod, action.currentTime) - }; - case ActionType.ChangeChartToRender: - return { - ...state, - chartToRender: action.chartType - }; - case ActionType.ToggleAreaNormalization: - return { - ...state, - areaNormalization: !state.areaNormalization - }; - case ActionType.ToggleShowMinMax: - return { - ...state, - showMinMax: !state.showMinMax - }; - case ActionType.ChangeBarStacking: - return { - ...state, - barStacking: !state.barStacking - }; - case ActionType.SetHotlinked: - return { - ...state, - hotlinked: action.hotlinked - }; - case ActionType.ChangeCompareSortingOrder: - return { - ...state, - compareSortingOrder: action.compareSortingOrder - }; - case ActionType.ToggleOptionsVisibility: - return { - ...state, - optionsVisibility: !state.optionsVisibility - }; - case ActionType.UpdateLineGraphRate: - return { - ...state, - lineGraphRate: action.lineGraphRate - }; - case ActionType.UpdateThreeDReadingInterval: - return { - ...state, - threeD: { - ...state.threeD, - readingInterval: action.readingInterval - } - }; - case ActionType.UpdateThreeDMeterOrGroupInfo: - return { - ...state, - threeD: { - ...state.threeD, - meterOrGroupID: action.meterOrGroupID, - meterOrGroup: action.meterOrGroup - } - }; - default: - return state; } -} +}) diff --git a/src/client/app/reducers/index.ts b/src/client/app/reducers/index.ts index 5170cbf3c..d2e743241 100644 --- a/src/client/app/reducers/index.ts +++ b/src/client/app/reducers/index.ts @@ -7,7 +7,6 @@ import meters from './meters'; import lineReadings from './lineReadings'; import barReadings from './barReadings'; import compareReadings from './compareReadings'; -import graph from './graph'; import groups from './groups'; import maps from './maps'; import admin from './admin'; @@ -28,7 +27,8 @@ export default combineReducers({ bar: barReadings, compare: compareReadings }), - graph, + // graph, + graph: graphSlice.reducer, maps, groups, admin, diff --git a/src/client/app/types/redux/graph.ts b/src/client/app/types/redux/graph.ts index d0814afca..d7706f513 100644 --- a/src/client/app/types/redux/graph.ts +++ b/src/client/app/types/redux/graph.ts @@ -4,7 +4,6 @@ import * as moment from 'moment'; import { TimeInterval } from '../../../../common/TimeInterval'; -import { ActionType } from './actions'; import { ComparePeriod, SortingOrder } from '../../utils/calculateCompare'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; @@ -39,123 +38,6 @@ export enum ReadingInterval { Incompatible = -999 } -export interface UpdateSelectedMetersAction { - type: ActionType.UpdateSelectedMeters; - meterIDs: number[]; -} - -export interface UpdateSelectedGroupsAction { - type: ActionType.UpdateSelectedGroups; - groupIDs: number[]; -} - -export interface UpdateSelectedUnitAction { - type: ActionType.UpdateSelectedUnit; - unitID: number; -} - -export interface UpdateSelectedAreaUnitAction { - type: ActionType.UpdateSelectedAreaUnit; - areaUnit: AreaUnitType; -} - -export interface UpdateBarDurationAction { - type: ActionType.UpdateBarDuration; - barDuration: moment.Duration; -} - -export interface ChangeChartToRenderAction { - type: ActionType.ChangeChartToRender; - chartType: ChartTypes; -} - -export interface ToggleAreaNormalizationAction { - type: ActionType.ToggleAreaNormalization; -} - -export interface ToggleShowMinMaxAction { - type: ActionType.ToggleShowMinMax; -} - -export interface ChangeBarStackingAction { - type: ActionType.ChangeBarStacking; -} - -export interface ChangeGraphZoomAction { - type: ActionType.ChangeGraphZoom; - timeInterval: TimeInterval; -} - -export interface ChangeSliderRangeAction { - type: ActionType.ChangeSliderRange; - sliderInterval: TimeInterval; -} - -export interface ResetRangeSliderStackAction { - type: ActionType.ResetRangeSliderStack; -} - -export interface UpdateComparePeriodAction { - type: ActionType.UpdateComparePeriod; - comparePeriod: ComparePeriod; - currentTime: moment.Moment; -} - -export interface ChangeCompareSortingOrderAction { - type: ActionType.ChangeCompareSortingOrder; - compareSortingOrder: SortingOrder; -} - -export interface SetHotlinked { - type: ActionType.SetHotlinked; - hotlinked: boolean; -} - -export interface ToggleOptionsVisibility { - type: ActionType.ToggleOptionsVisibility; -} - -export interface UpdateLineGraphRate { - type: ActionType.UpdateLineGraphRate; - lineGraphRate: LineGraphRate; -} - -export interface ConfirmGraphRenderOnce { - type: ActionType.ConfirmGraphRenderOnce; -} - -export interface UpdateThreeDReadingInterval { - type: ActionType.UpdateThreeDReadingInterval; - readingInterval: ReadingInterval; -} - -export interface UpdateThreeDMeterOrGroupInfo { - type: ActionType.UpdateThreeDMeterOrGroupInfo; - meterOrGroupID: MeterOrGroupID; - meterOrGroup: MeterOrGroup; -} - -export type GraphAction = - | ChangeGraphZoomAction - | ChangeSliderRangeAction - | ResetRangeSliderStackAction - | ChangeBarStackingAction - | ToggleAreaNormalizationAction - | ToggleShowMinMaxAction - | ChangeChartToRenderAction - | UpdateBarDurationAction - | UpdateSelectedGroupsAction - | UpdateSelectedMetersAction - | UpdateSelectedUnitAction - | UpdateSelectedAreaUnitAction - | UpdateComparePeriodAction - | SetHotlinked - | ChangeCompareSortingOrderAction - | ToggleOptionsVisibility - | UpdateLineGraphRate - | ConfirmGraphRenderOnce - | UpdateThreeDReadingInterval - | UpdateThreeDMeterOrGroupInfo; export interface LineGraphRate { label: string, From 30f7e6e7532beca3b8ba805596b36a1169791145 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Mon, 18 Sep 2023 03:34:47 +0000 Subject: [PATCH 004/131] Options Slice Refactor --- src/client/app/actions/options.ts | 7 +++---- src/client/app/reducers/index.ts | 4 ++-- src/client/app/reducers/options.ts | 23 +++++++++++------------ 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/client/app/actions/options.ts b/src/client/app/actions/options.ts index ac15a527d..ddb1f7ceb 100644 --- a/src/client/app/actions/options.ts +++ b/src/client/app/actions/options.ts @@ -2,12 +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 { ActionType } from '../types/redux/actions'; import { LanguageTypes } from '../types/redux/i18n'; -import * as t from '../types/redux/options'; import * as moment from 'moment'; +import { optionsSlice } from '../reducers/options'; -export function updateSelectedLanguage(selectedLanguage: LanguageTypes): t.UpdateSelectedLanguageAction { +export function updateSelectedLanguage(selectedLanguage: LanguageTypes) { moment.locale(selectedLanguage); - return { type: ActionType.UpdateSelectedLanguage, selectedLanguage }; + return optionsSlice.actions.updateSelectedLanguage(selectedLanguage); } diff --git a/src/client/app/reducers/index.ts b/src/client/app/reducers/index.ts index d2e743241..d501c2a28 100644 --- a/src/client/app/reducers/index.ts +++ b/src/client/app/reducers/index.ts @@ -15,7 +15,7 @@ import currentUser from './currentUser'; import unsavedWarning from './unsavedWarning'; import units from './units'; import conversions from './conversions'; -import options from './options'; +import {optionsSlice} from './options'; import { baseApi } from '../redux/api/baseApi'; import { graphSlice } from './graph'; @@ -37,7 +37,7 @@ export default combineReducers({ unsavedWarning, units, conversions, - options, + options: optionsSlice.reducer, // RTK Query's Derived Reducers [baseApi.reducerPath]: baseApi.reducer }); diff --git a/src/client/app/reducers/options.ts b/src/client/app/reducers/options.ts index 6f7d85558..f01ef5d8a 100644 --- a/src/client/app/reducers/options.ts +++ b/src/client/app/reducers/options.ts @@ -2,22 +2,21 @@ * 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 '../types/redux/actions'; import { LanguageTypes } from '../types/redux/i18n'; -import { OptionsAction, OptionsState } from '../types/redux/options'; +import { OptionsState } from '../types/redux/options'; +import { createSlice } from '@reduxjs/toolkit' +import type { PayloadAction } from '@reduxjs/toolkit' const defaultState: OptionsState = { selectedLanguage: LanguageTypes.en }; -export default function options(state = defaultState, action: OptionsAction) { - switch (action.type) { - case ActionType.UpdateSelectedLanguage: - return { - ...state, - selectedLanguage: action.selectedLanguage - }; - default: - return state; +export const optionsSlice = createSlice({ + name: 'options', + initialState: defaultState, + reducers: { + updateSelectedLanguage: (state, action: PayloadAction) => { + state.selectedLanguage = action.payload + } } -} \ No newline at end of file +}); From a4446129b8341ffcd776a1456167c67dd513088a Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Mon, 18 Sep 2023 03:53:14 +0000 Subject: [PATCH 005/131] Current User Slice Refactor --- src/client/app/actions/currentUser.ts | 20 +---- .../app/components/HeaderButtonsComponent.tsx | 4 +- .../components/UnsavedWarningComponent.tsx | 4 +- src/client/app/containers/LoginContainer.tsx | 4 +- src/client/app/containers/RouteContainer.ts | 4 +- src/client/app/reducers/currentUser.ts | 73 ++++++++++++------- src/client/app/reducers/index.ts | 6 +- src/client/app/types/redux/currentUser.ts | 24 +++--- 8 files changed, 73 insertions(+), 66 deletions(-) diff --git a/src/client/app/actions/currentUser.ts b/src/client/app/actions/currentUser.ts index 60867c858..7656f6f2a 100644 --- a/src/client/app/actions/currentUser.ts +++ b/src/client/app/actions/currentUser.ts @@ -3,19 +3,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { usersApi, verificationApi } from '../utils/api'; -import { Thunk, ActionType, Dispatch, GetState } from '../types/redux/actions'; +import { Thunk, Dispatch, GetState } from '../types/redux/actions'; import { State } from '../types/redux/state'; -import * as t from '../types/redux/currentUser'; -import { User } from '../types/items'; import { deleteToken, hasToken } from '../utils/token'; +import { currentUserSlice } from '../reducers/currentUser'; -export function requestCurrentUser(): t.RequestCurrentUser { - return { type: ActionType.RequestCurrentUser }; -} - -export function receiveCurrentUser(data: User): t.ReceiveCurrentUser { - return { type: ActionType.ReceiveCurrentUser, data }; -} /** * Check if we should fetch the current user's data. This function has the side effect of deleting an invalid token from local storage. @@ -44,9 +36,9 @@ async function shouldFetchCurrentUser(state: State): Promise { export function fetchCurrentUser(): Thunk { return async (dispatch: Dispatch) => { - dispatch(requestCurrentUser()); + dispatch(currentUserSlice.actions.requestCurrentUser()); const user = await usersApi.getCurrentUser(); - return dispatch(receiveCurrentUser(user)); + return dispatch(currentUserSlice.actions.receiveCurrentUser(user)); }; } @@ -57,8 +49,4 @@ export function fetchCurrentUserIfNeeded(): Thunk { } return Promise.resolve(); }; -} - -export function clearCurrentUser(): t.ClearCurrentUser { - return { type: ActionType.ClearCurrentUser }; } \ No newline at end of file diff --git a/src/client/app/components/HeaderButtonsComponent.tsx b/src/client/app/components/HeaderButtonsComponent.tsx index fa35a79c8..9d2342055 100644 --- a/src/client/app/components/HeaderButtonsComponent.tsx +++ b/src/client/app/components/HeaderButtonsComponent.tsx @@ -12,13 +12,13 @@ import { UserRole } from '../types/items'; import { hasPermissions, isRoleAdmin } from '../utils/hasPermissions'; import { flipLogOutState } from '../actions/unsavedWarning'; import { deleteToken } from '../utils/token'; -import { clearCurrentUser } from '../actions/currentUser'; import { State } from '../types/redux/state'; import { useDispatch, useSelector } from 'react-redux'; import { Navbar, Nav, NavLink, UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap'; import LanguageSelectorComponent from './LanguageSelectorComponent'; import { toggleOptionsVisibility } from '../actions/graph'; import { BASE_URL } from './TooltipHelpComponent'; +import {currentUserSlice} from '../reducers/currentUser'; /** * React Component that defines the header buttons at the top of a page @@ -153,7 +153,7 @@ export default function HeaderButtonsComponent() { // Remove token so has no role. deleteToken(); // Clean up state since lost your role. - dispatch(clearCurrentUser()); + dispatch(currentUserSlice.actions.clearCurrentUser()); } }; diff --git a/src/client/app/components/UnsavedWarningComponent.tsx b/src/client/app/components/UnsavedWarningComponent.tsx index d0650589f..2a22b2032 100644 --- a/src/client/app/components/UnsavedWarningComponent.tsx +++ b/src/client/app/components/UnsavedWarningComponent.tsx @@ -7,9 +7,9 @@ import { FormattedMessage } from 'react-intl'; import { Prompt, withRouter, RouteComponentProps } from 'react-router-dom'; import { FlipLogOutStateAction, RemoveUnsavedChangesAction } from '../types/redux/unsavedWarning'; import { deleteToken } from '../utils/token'; -import { clearCurrentUser } from '../actions/currentUser'; import store from '../index'; import { Modal, ModalBody, ModalFooter, Button } from 'reactstrap'; +import { currentUserSlice } from '../reducers/currentUser'; interface UnsavedWarningProps extends RouteComponentProps { hasUnsavedChanges: boolean; @@ -161,7 +161,7 @@ class UnsavedWarningComponent extends React.Component { private handleLogOut() { deleteToken(); - store.dispatch(clearCurrentUser()); + store.dispatch(currentUserSlice.actions.clearCurrentUser()); } } diff --git a/src/client/app/containers/LoginContainer.tsx b/src/client/app/containers/LoginContainer.tsx index 9e1c07918..ef2a4dc5b 100644 --- a/src/client/app/containers/LoginContainer.tsx +++ b/src/client/app/containers/LoginContainer.tsx @@ -4,16 +4,16 @@ import { connect } from 'react-redux'; import { User } from '../types/items'; -import { receiveCurrentUser } from '../actions/currentUser' import LoginComponent from '../components/LoginComponent'; import { Dispatch } from '../types/redux/actions'; +import { currentUserSlice } from '../reducers/currentUser'; /** * A container that does data fetching for FooterComponent and connects it to the redux store. */ function mapDispatchToProps(dispatch: Dispatch) { return { - saveCurrentUser: (profile: User) => dispatch(receiveCurrentUser(profile)) + saveCurrentUser: (profile: User) => dispatch(currentUserSlice.actions.receiveCurrentUser(profile)) }; } diff --git a/src/client/app/containers/RouteContainer.ts b/src/client/app/containers/RouteContainer.ts index 9019e2f11..51eb80e8a 100644 --- a/src/client/app/containers/RouteContainer.ts +++ b/src/client/app/containers/RouteContainer.ts @@ -7,10 +7,10 @@ import RouteComponent from '../components/RouteComponent'; import { Dispatch } from '../types/redux/actions'; import { State } from '../types/redux/state'; import { changeOptionsFromLink, LinkOptions } from '../actions/graph'; -import { clearCurrentUser } from '../actions/currentUser'; import { isRoleAdmin } from '../utils/hasPermissions'; import { UserRole } from '../types/items'; import { graphSlice } from '../reducers/graph'; +import { currentUserSlice } from '../reducers/currentUser'; function mapStateToProps(state: State) { const currentUser = state.currentUser.profile; @@ -36,7 +36,7 @@ function mapStateToProps(state: State) { function mapDispatchToProps(dispatch: Dispatch) { return { changeOptionsFromLink: (options: LinkOptions) => dispatch(changeOptionsFromLink(options)), - clearCurrentUser: () => dispatch(clearCurrentUser()), + clearCurrentUser: () => dispatch(currentUserSlice.actions.clearCurrentUser()), // Set the state to indicate chartlinks have been rendered. changeRenderOnce: () => dispatch(graphSlice.actions.confirmGraphRenderOnce()) }; diff --git a/src/client/app/reducers/currentUser.ts b/src/client/app/reducers/currentUser.ts index 136639401..2ab0b4f3e 100644 --- a/src/client/app/reducers/currentUser.ts +++ b/src/client/app/reducers/currentUser.ts @@ -2,36 +2,55 @@ * 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 { CurrentUserAction, CurrentUserState } from '../types/redux/currentUser'; -import { ActionType } from '../types/redux/actions'; +import { CurrentUserState } from '../types/redux/currentUser'; +import { createSlice } from '@reduxjs/toolkit' +import type { PayloadAction } from '@reduxjs/toolkit' +import { User } from '../types/items'; + /* * Defines store interactions when version related actions are dispatched to the store. */ -const defaultState: CurrentUserState = { isFetching: false, profile: null }; +const defaultState: CurrentUserState = { isFetching: false, profile: null }; -export default function profile(state = defaultState, action: CurrentUserAction): CurrentUserState { - switch (action.type) { - case ActionType.RequestCurrentUser: - // When the current user's profile is requested, indicate app is fetching data from API - return { - ...state, - isFetching: true - }; - case ActionType.ReceiveCurrentUser: - // When the current user's profile is received, update the store with result from API - return { - ...state, - isFetching: false, - profile: action.data - }; - case ActionType.ClearCurrentUser: - // Removes the current user from the redux store. - return { - ...state, - profile: null - } - default: - return state; +export const currentUserSlice = createSlice({ + name: 'currentUser', + initialState: defaultState, + reducers: { + requestCurrentUser: state => { + state.isFetching = true + }, + receiveCurrentUser: (state, action: PayloadAction) => { + state.isFetching = false + state.profile = action.payload + }, + clearCurrentUser: state => { + state.profile = null + } } -} +}) +// export default function profile(state = defaultState, action: CurrentUserAction): CurrentUserState { +// switch (action.type) { +// case ActionType.RequestCurrentUser: +// // When the current user's profile is requested, indicate app is fetching data from API +// return { +// ...state, +// isFetching: true +// }; +// case ActionType.ReceiveCurrentUser: +// // When the current user's profile is received, update the store with result from API +// return { +// ...state, +// isFetching: false, +// profile: action.data +// }; +// case ActionType.ClearCurrentUser: +// // Removes the current user from the redux store. +// return { +// ...state, +// profile: null +// } +// default: +// return state; +// } +// } \ No newline at end of file diff --git a/src/client/app/reducers/index.ts b/src/client/app/reducers/index.ts index d501c2a28..5db779b3b 100644 --- a/src/client/app/reducers/index.ts +++ b/src/client/app/reducers/index.ts @@ -11,11 +11,11 @@ import groups from './groups'; import maps from './maps'; import admin from './admin'; import version from './version'; -import currentUser from './currentUser'; +import { currentUserSlice } from './currentUser'; import unsavedWarning from './unsavedWarning'; import units from './units'; import conversions from './conversions'; -import {optionsSlice} from './options'; +import { optionsSlice } from './options'; import { baseApi } from '../redux/api/baseApi'; import { graphSlice } from './graph'; @@ -33,7 +33,7 @@ export default combineReducers({ groups, admin, version, - currentUser, + currentUser: currentUserSlice.reducer, unsavedWarning, units, conversions, diff --git a/src/client/app/types/redux/currentUser.ts b/src/client/app/types/redux/currentUser.ts index 4266ae7e0..be43bfc26 100644 --- a/src/client/app/types/redux/currentUser.ts +++ b/src/client/app/types/redux/currentUser.ts @@ -2,26 +2,26 @@ * 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 { ActionType } from './actions'; import { User } from '../items'; /* * Defines the action interfaces used in the corresponding reducers. */ -export interface RequestCurrentUser { - type: ActionType.RequestCurrentUser; -} -export interface ReceiveCurrentUser { - type: ActionType.ReceiveCurrentUser; - data: User; -} +// export interface RequestCurrentUser { +// type: ActionType.RequestCurrentUser; +// } +// export interface ReceiveCurrentUser { +// type: ActionType.ReceiveCurrentUser; +// data: User; +// } -export interface ClearCurrentUser { - type: ActionType.ClearCurrentUser; -} +// export interface ClearCurrentUser { +// type: ActionType.ClearCurrentUser; +// } -export type CurrentUserAction = RequestCurrentUser | ReceiveCurrentUser | ClearCurrentUser; +// export type CurrentUserAction = RequestCurrentUser | ReceiveCurrentUser | ClearCurrentUser; export interface CurrentUserState { isFetching: boolean; From ca578407def9d7892ebbf21fb943c27729b1308a Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Mon, 18 Sep 2023 04:24:08 +0000 Subject: [PATCH 006/131] unSavedWarningSliceRefactor --- src/client/app/actions/unsavedWarning.ts | 42 +++++----- .../app/components/HeaderButtonsComponent.tsx | 4 +- .../components/UnsavedWarningComponent.tsx | 8 +- .../components/admin/PreferencesComponent.tsx | 11 ++- .../components/admin/UsersDetailComponent.tsx | 21 +++-- .../EditConversionModalComponent.tsx | 5 +- .../app/components/maps/MapViewComponent.tsx | 7 +- .../components/maps/MapsDetailComponent.tsx | 5 +- .../meters/EditMeterModalComponent.tsx | 4 +- .../unit/EditUnitModalComponent.tsx | 10 +-- .../app/containers/UnsavedWarningContainer.ts | 6 +- src/client/app/reducers/index.ts | 4 +- src/client/app/reducers/unsavedWarning.ts | 83 ++++++++++++++----- src/client/app/types/redux/unsavedWarning.ts | 38 ++++----- 14 files changed, 147 insertions(+), 101 deletions(-) diff --git a/src/client/app/actions/unsavedWarning.ts b/src/client/app/actions/unsavedWarning.ts index 8dee850b6..ac5cbe943 100644 --- a/src/client/app/actions/unsavedWarning.ts +++ b/src/client/app/actions/unsavedWarning.ts @@ -2,28 +2,26 @@ * 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 '../types/redux/actions'; -import * as t from '../types/redux/unsavedWarning'; -/** - * Notify that there are unsaved changes - * @param removeFunction The function to remove local changes - * @param submitFunction The function to submit unsaved changes - */ -export function updateUnsavedChanges(removeFunction: any, submitFunction: any): t.UpdateUnsavedChangesAction { - return { type: ActionType.UpdateUnsavedChanges, removeFunction, submitFunction }; -} +// /** +// * Notify that there are unsaved changes +// * @param removeFunction The function to remove local changes +// * @param submitFunction The function to submit unsaved changes +// */ +// export function updateUnsavedChanges(removeFunction: any, submitFunction: any): t.UpdateUnsavedChangesAction { +// return { type: ActionType.UpdateUnsavedChanges, removeFunction, submitFunction }; +// } -/** - * Notify that there are no unsaved changes - */ -export function removeUnsavedChanges(): t.RemoveUnsavedChangesAction { - return { type: ActionType.RemoveUnsavedChanges }; -} +// /** +// * Notify that there are no unsaved changes +// */ +// export function removeUnsavedChanges(): t.RemoveUnsavedChangesAction { +// return { type: ActionType.RemoveUnsavedChanges }; +// } -/** - * Notify that the logout button was clicked or unclicked - */ -export function flipLogOutState(): t.FlipLogOutStateAction { - return { type: ActionType.FlipLogOutState }; -} \ No newline at end of file +// /** +// * Notify that the logout button was clicked or unclicked +// */ +// export function flipLogOutState(): t.FlipLogOutStateAction { +// return { type: ActionType.FlipLogOutState }; +// } \ No newline at end of file diff --git a/src/client/app/components/HeaderButtonsComponent.tsx b/src/client/app/components/HeaderButtonsComponent.tsx index 9d2342055..303e20c65 100644 --- a/src/client/app/components/HeaderButtonsComponent.tsx +++ b/src/client/app/components/HeaderButtonsComponent.tsx @@ -10,7 +10,6 @@ import getPage from '../utils/getPage'; import translate from '../utils/translate'; import { UserRole } from '../types/items'; import { hasPermissions, isRoleAdmin } from '../utils/hasPermissions'; -import { flipLogOutState } from '../actions/unsavedWarning'; import { deleteToken } from '../utils/token'; import { State } from '../types/redux/state'; import { useDispatch, useSelector } from 'react-redux'; @@ -19,6 +18,7 @@ import LanguageSelectorComponent from './LanguageSelectorComponent'; import { toggleOptionsVisibility } from '../actions/graph'; import { BASE_URL } from './TooltipHelpComponent'; import {currentUserSlice} from '../reducers/currentUser'; +import { unsavedWarningSlice } from '../reducers/unsavedWarning'; /** * React Component that defines the header buttons at the top of a page @@ -148,7 +148,7 @@ export default function HeaderButtonsComponent() { const handleLogOut = () => { if (unsavedChangesState) { // Unsaved changes so deal with them and then it takes care of logout. - dispatch(flipLogOutState()); + dispatch(unsavedWarningSlice.actions.flipLogOutState()); } else { // Remove token so has no role. deleteToken(); diff --git a/src/client/app/components/UnsavedWarningComponent.tsx b/src/client/app/components/UnsavedWarningComponent.tsx index 2a22b2032..161b0d0f9 100644 --- a/src/client/app/components/UnsavedWarningComponent.tsx +++ b/src/client/app/components/UnsavedWarningComponent.tsx @@ -5,19 +5,19 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { Prompt, withRouter, RouteComponentProps } from 'react-router-dom'; -import { FlipLogOutStateAction, RemoveUnsavedChangesAction } from '../types/redux/unsavedWarning'; import { deleteToken } from '../utils/token'; import store from '../index'; import { Modal, ModalBody, ModalFooter, Button } from 'reactstrap'; import { currentUserSlice } from '../reducers/currentUser'; +import { unsavedWarningSlice } from '../reducers/unsavedWarning'; interface UnsavedWarningProps extends RouteComponentProps { hasUnsavedChanges: boolean; isLogOutClicked: boolean; removeFunction: (callback: () => void) => any; submitFunction: (successCallback: () => void, failureCallback: () => void) => any; - removeUnsavedChanges(): RemoveUnsavedChangesAction; - flipLogOutState(): FlipLogOutStateAction; + removeUnsavedChanges(): ReturnType; + flipLogOutState(): ReturnType; } class UnsavedWarningComponent extends React.Component { @@ -49,7 +49,7 @@ class UnsavedWarningComponent extends React.Component { <> { + message={nextLocation => { const { confirmedToLeave } = this.state; const { hasUnsavedChanges } = this.props; if (!confirmedToLeave && hasUnsavedChanges) { diff --git a/src/client/app/components/admin/PreferencesComponent.tsx b/src/client/app/components/admin/PreferencesComponent.tsx index b8e5683a3..83707465e 100644 --- a/src/client/app/components/admin/PreferencesComponent.tsx +++ b/src/client/app/components/admin/PreferencesComponent.tsx @@ -25,7 +25,6 @@ import { UpdateDefaultMeterDisableChecksAction } from '../../types/redux/admin'; -import { removeUnsavedChanges, updateUnsavedChanges } from '../../actions/unsavedWarning'; import { defineMessages, FormattedMessage, injectIntl, WrappedComponentProps } from 'react-intl'; import { LanguageTypes } from '../../types/redux/i18n'; import TimeZoneSelect from '../TimeZoneSelect'; @@ -34,6 +33,7 @@ import { fetchPreferencesIfNeeded, submitPreferences } from '../../actions/admin import { AreaUnitType } from '../../utils/getAreaUnitConversion'; import translate from '../../utils/translate'; import { TrueFalseType } from '../../types/items'; +import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; interface PreferencesProps { displayTitle: string; @@ -65,7 +65,7 @@ interface PreferencesProps { updateDefaultFileSizeLimit(defaultFileSizeLimit: number): UpdateDefaultFileSizeLimit; updateDefaultAreaUnit(defaultAreaUnit: AreaUnitType): UpdateDefaultAreaUnitAction; updateDefaultMeterReadingFrequency(defaultMeterReadingFrequency: string): UpdateDefaultMeterReadingFrequencyAction; - updateDefaultMeterMinimumValue(defaultMeterMinimumValue : number): UpdateDefaultMeterMinimumValueAction; + updateDefaultMeterMinimumValue(defaultMeterMinimumValue: number): UpdateDefaultMeterMinimumValueAction; updateDefaultMeterMaximumValue(defaultMeterMaximumValue: number): UpdateDefaultMeterMaximumValueAction; updateDefaultMeterMinimumDate(defaultMeterMinimumDate: string): UpdateDefaultMeterMinimumDateAction; updateDefaultMeterMaximumDate(defaultMeterMaximumDate: string): UpdateDefaultMeterMaximumDateAction; @@ -428,12 +428,15 @@ class PreferencesComponent extends React.Component { private updateUnsavedChanges() { // Notify that there are unsaved changes - store.dispatch(updateUnsavedChanges(this.removeUnsavedChangesFunction, this.submitUnsavedChangesFunction)); + store.dispatch(unsavedWarningSlice.actions.updateUnsavedChanges({ + removeFunction: this.removeUnsavedChangesFunction, + submitFunction: this.submitUnsavedChangesFunction + })); } private removeUnsavedChanges() { // Notify that there are no unsaved changes - store.dispatch(removeUnsavedChanges()); + store.dispatch(unsavedWarningSlice.actions.removeUnsavedChanges()); } private handleDisplayTitleChange(e: { target: HTMLInputElement; }) { diff --git a/src/client/app/components/admin/UsersDetailComponent.tsx b/src/client/app/components/admin/UsersDetailComponent.tsx index 71025acbe..4c899bde5 100644 --- a/src/client/app/components/admin/UsersDetailComponent.tsx +++ b/src/client/app/components/admin/UsersDetailComponent.tsx @@ -10,8 +10,8 @@ import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import { FormattedMessage } from 'react-intl'; import UnsavedWarningContainer from '../../containers/UnsavedWarningContainer'; -import { updateUnsavedChanges, removeUnsavedChanges } from '../../actions/unsavedWarning'; import store from '../../index' +import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; interface UserDisplayComponentProps { users: User[]; @@ -59,12 +59,15 @@ export default function UserDetailComponent(props: UserDisplayComponentProps) { const addUnsavedChanges = () => { // Notify that there are unsaved changes - store.dispatch(updateUnsavedChanges(removeUnsavedChangesFunction, submitUnsavedChangesFunction)); + store.dispatch(unsavedWarningSlice.actions.updateUnsavedChanges({ + removeFunction: removeUnsavedChangesFunction, + submitFunction: submitUnsavedChangesFunction + })); } const clearUnsavedChanges = () => { // Notify that there are no unsaved changes - store.dispatch(removeUnsavedChanges()); + store.dispatch(unsavedWarningSlice.actions.removeUnsavedChanges()); } return ( @@ -73,7 +76,7 @@ export default function UserDetailComponent(props: UserDisplayComponentProps) {

- +
@@ -82,9 +85,9 @@ export default function UserDetailComponent(props: UserDisplayComponentProps) { - - - + + + @@ -107,7 +110,7 @@ export default function UserDetailComponent(props: UserDisplayComponentProps) { @@ -124,7 +127,7 @@ export default function UserDetailComponent(props: UserDisplayComponentProps) { clearUnsavedChanges(); }} > - + diff --git a/src/client/app/components/conversion/EditConversionModalComponent.tsx b/src/client/app/components/conversion/EditConversionModalComponent.tsx index bafab9607..5e392a0c3 100644 --- a/src/client/app/components/conversion/EditConversionModalComponent.tsx +++ b/src/client/app/components/conversion/EditConversionModalComponent.tsx @@ -11,7 +11,6 @@ import translate from '../../utils/translate'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; import '../../styles/modal.css'; -import { removeUnsavedChanges } from '../../actions/unsavedWarning'; import { submitEditedConversion, deleteConversion } from '../../actions/conversions'; import { TrueFalseType } from '../../types/items'; import { ConversionData } from '../../types/redux/conversions'; @@ -19,6 +18,8 @@ import { UnitDataById } from 'types/redux/units'; import ConfirmActionModalComponent from '../ConfirmActionModalComponent' import { tooltipBaseStyle } from '../../styles/modalStyle'; import { Dispatch } from 'types/redux/actions'; +import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; + interface EditConversionModalComponentProps { show: boolean; @@ -131,7 +132,7 @@ export default function EditConversionModalComponent(props: EditConversionModalC if (conversionHasChanges) { // Save our changes by dispatching the submitEditedConversion action dispatch(submitEditedConversion(state, shouldRedoCik)); - dispatch(removeUnsavedChanges()); + dispatch(unsavedWarningSlice.actions.removeUnsavedChanges()); } } diff --git a/src/client/app/components/maps/MapViewComponent.tsx b/src/client/app/components/maps/MapViewComponent.tsx index f37a59938..8c9a2b061 100644 --- a/src/client/app/components/maps/MapViewComponent.tsx +++ b/src/client/app/components/maps/MapViewComponent.tsx @@ -10,8 +10,8 @@ import { FormattedMessage, injectIntl, WrappedComponentProps } from 'react-intl' import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map'; import * as moment from 'moment'; import store from '../../index'; -import { updateUnsavedChanges } from '../../actions/unsavedWarning'; import { fetchMapsDetails, submitEditedMaps, confirmEditedMaps } from '../../actions/map'; +import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; interface MapViewProps { // The ID of the map to be displayed @@ -107,7 +107,10 @@ class MapViewComponent extends React.Component) { diff --git a/src/client/app/components/maps/MapsDetailComponent.tsx b/src/client/app/components/maps/MapsDetailComponent.tsx index a4e1fa77c..b6be2009a 100644 --- a/src/client/app/components/maps/MapsDetailComponent.tsx +++ b/src/client/app/components/maps/MapsDetailComponent.tsx @@ -11,10 +11,11 @@ import MapViewContainer from '../../containers/maps/MapViewContainer'; import {Link} from 'react-router-dom'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import { removeUnsavedChanges } from '../../actions/unsavedWarning'; import store from '../../index'; import UnsavedWarningContainer from '../../containers/UnsavedWarningContainer'; import HeaderComponent from '../../components/HeaderComponent'; +import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; + interface MapsDetailProps { maps: number[]; @@ -114,7 +115,7 @@ export default class MapsDetailComponent extends React.Component handleStringChange(e)} value={state.name} - invalid={state.name === ''}/> + invalid={state.name === ''} /> @@ -288,7 +288,7 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp min='1' invalid={state.secInRate <= 0} /> - + {/* Suffix input */} diff --git a/src/client/app/containers/UnsavedWarningContainer.ts b/src/client/app/containers/UnsavedWarningContainer.ts index 1c689b341..67bb21d30 100644 --- a/src/client/app/containers/UnsavedWarningContainer.ts +++ b/src/client/app/containers/UnsavedWarningContainer.ts @@ -2,11 +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 { removeUnsavedChanges, flipLogOutState } from '../actions/unsavedWarning'; import { connect } from 'react-redux'; import UnsavedWarningComponent from '../components/UnsavedWarningComponent'; import { State } from '../types/redux/state'; import { Dispatch } from '../types/redux/actions'; +import { unsavedWarningSlice } from '../reducers/unsavedWarning'; function mapStateToProps(state: State) { return { @@ -19,8 +19,8 @@ function mapStateToProps(state: State) { function mapDispatchToProps(dispatch: Dispatch) { return { - removeUnsavedChanges: () => dispatch(removeUnsavedChanges()), - flipLogOutState: () => dispatch(flipLogOutState()) + removeUnsavedChanges: () => dispatch(unsavedWarningSlice.actions.removeUnsavedChanges()), + flipLogOutState: () => dispatch(unsavedWarningSlice.actions.flipLogOutState()) }; } diff --git a/src/client/app/reducers/index.ts b/src/client/app/reducers/index.ts index 5db779b3b..67346ef91 100644 --- a/src/client/app/reducers/index.ts +++ b/src/client/app/reducers/index.ts @@ -12,7 +12,7 @@ import maps from './maps'; import admin from './admin'; import version from './version'; import { currentUserSlice } from './currentUser'; -import unsavedWarning from './unsavedWarning'; +import { unsavedWarningSlice } from './unsavedWarning'; import units from './units'; import conversions from './conversions'; import { optionsSlice } from './options'; @@ -34,7 +34,7 @@ export default combineReducers({ admin, version, currentUser: currentUserSlice.reducer, - unsavedWarning, + unsavedWarning: unsavedWarningSlice.reducer, units, conversions, options: optionsSlice.reducer, diff --git a/src/client/app/reducers/unsavedWarning.ts b/src/client/app/reducers/unsavedWarning.ts index 89e08c09b..963e32403 100644 --- a/src/client/app/reducers/unsavedWarning.ts +++ b/src/client/app/reducers/unsavedWarning.ts @@ -2,9 +2,9 @@ * 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 { UnsavedWarningAction, UnsavedWarningState } from '../types/redux/unsavedWarning'; -import { ActionType } from '../types/redux/actions'; +import { UnsavedWarningState } from '../types/redux/unsavedWarning'; import { any } from 'prop-types'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; const defaultState: UnsavedWarningState = { hasUnsavedChanges: false, @@ -13,26 +13,63 @@ const defaultState: UnsavedWarningState = { submitFunction: () => any }; -export default function unsavedWarning(state = defaultState, action: UnsavedWarningAction) { - switch (action.type) { - case ActionType.UpdateUnsavedChanges: - return { - ...state, - hasUnsavedChanges: true, - removeFunction: action.removeFunction, - submitFunction: action.submitFunction - } - case ActionType.RemoveUnsavedChanges: - return { - ...state, - hasUnsavedChanges: false - } - case ActionType.FlipLogOutState: - return { - ...state, - isLogOutClicked: !state.isLogOutClicked - } - default: - return state; +export const unsavedWarningSlice = createSlice({ + name: 'unsavedWarning', + initialState: defaultState, + reducers: { + // case ActionType.UpdateUnsavedChanges: + // return { + // ...state, + // hasUnsavedChanges: true, + // removeFunction: action.removeFunction, + // submitFunction: action.submitFunction + // } + updateUnsavedChanges: (state, action: PayloadAction<{ removeFunction: any, submitFunction: any }>) => { + state.hasUnsavedChanges = true; + state.removeFunction = action.payload.removeFunction; + state.submitFunction = action.payload.submitFunction; + }, + // case ActionType.RemoveUnsavedChanges: + // return { + // ...state, + // hasUnsavedChanges: false + // } + removeUnsavedChanges: state => { + state.hasUnsavedChanges = false; + }, + // case ActionType.FlipLogOutState: + // return { + // ...state, + // isLogOutClicked: !state.isLogOutClicked + // } + flipLogOutState: state => { + state.isLogOutClicked = !state.isLogOutClicked; + } + } } +); + +// export default function unsavedWarning(state = defaultState, action: UnsavedWarningAction) { +// switch (action.type) { +// case ActionType.UpdateUnsavedChanges: +// return { +// ...state, +// hasUnsavedChanges: true, +// removeFunction: action.removeFunction, +// submitFunction: action.submitFunction +// } +// case ActionType.RemoveUnsavedChanges: +// return { +// ...state, +// hasUnsavedChanges: false +// } +// case ActionType.FlipLogOutState: +// return { +// ...state, +// isLogOutClicked: !state.isLogOutClicked +// } +// default: +// return state; +// } +// } \ No newline at end of file diff --git a/src/client/app/types/redux/unsavedWarning.ts b/src/client/app/types/redux/unsavedWarning.ts index a0cf4bd59..d41ca6a06 100644 --- a/src/client/app/types/redux/unsavedWarning.ts +++ b/src/client/app/types/redux/unsavedWarning.ts @@ -2,29 +2,29 @@ * 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 { ActionType } from './actions'; -export type UnsavedWarningAction = UpdateUnsavedChangesAction | RemoveUnsavedChangesAction | FlipLogOutStateAction; +// export type UnsavedWarningAction = UpdateUnsavedChangesAction | RemoveUnsavedChangesAction | FlipLogOutStateAction; -/** - * The action triggered when there are new unsaved changes - */ -export interface UpdateUnsavedChangesAction { - type: ActionType.UpdateUnsavedChanges; - removeFunction: any; - submitFunction: any; -} +// /** +// * The action triggered when there are new unsaved changes +// */ +// export interface UpdateUnsavedChangesAction { +// type: ActionType.UpdateUnsavedChanges; +// removeFunction: any; +// submitFunction: any; +// } -/** - * The action triggered when the users decide to discard unsaved changes or click the submit button - */ -export interface RemoveUnsavedChangesAction { - type: ActionType.RemoveUnsavedChanges; -} +// /** +// * The action triggered when the users decide to discard unsaved changes or click the submit button +// */ +// export interface RemoveUnsavedChangesAction { +// type: ActionType.RemoveUnsavedChanges; +// } -export interface FlipLogOutStateAction { - type: ActionType.FlipLogOutState; -} +// export interface FlipLogOutStateAction { +// type: ActionType.FlipLogOutState; +// } export interface UnsavedWarningState { hasUnsavedChanges: boolean; From d3b61a0dc90624b7eb2c3b32191a4cd1cc7e8f6f Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Mon, 18 Sep 2023 05:35:12 +0000 Subject: [PATCH 007/131] Meter Slice Refactor --- src/client/app/actions/meters.ts | 57 +++---------- src/client/app/actions/version.ts | 22 +---- src/client/app/reducers/index.ts | 8 +- src/client/app/reducers/meters.ts | 116 ++++++++------------------ src/client/app/reducers/version.ts | 71 +++++++++++----- src/client/app/types/redux/meters.ts | 115 +++++++++---------------- src/client/app/types/redux/version.ts | 17 ++-- 7 files changed, 153 insertions(+), 253 deletions(-) diff --git a/src/client/app/actions/meters.ts b/src/client/app/actions/meters.ts index 2fc15399c..b9934fb1b 100644 --- a/src/client/app/actions/meters.ts +++ b/src/client/app/actions/meters.ts @@ -1,65 +1,34 @@ /* 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/. */ + * 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, Thunk, Dispatch, GetState } from '../types/redux/actions'; +import { Thunk, Dispatch, GetState } from '../types/redux/actions'; import { showSuccessNotification } from '../utils/notifications'; import translate from '../utils/translate'; import * as t from '../types/redux/meters'; import { metersApi } from '../utils/api'; import { updateCikAndDBViewsIfNeeded } from './admin'; +import { metersSlice } from '../reducers/meters'; -export function requestMetersDetails(): t.RequestMetersDetailsAction { - return { type: ActionType.RequestMetersDetails }; -} - -export function receiveMetersDetails(data: t.MeterData[]): t.ReceiveMetersDetailsAction { - return { type: ActionType.ReceiveMetersDetails, data }; -} export function fetchMetersDetails(): Thunk { return async (dispatch: Dispatch, getState: GetState) => { // ensure a fetch is not currently happening if (!getState().meters.isFetching) { // set isFetching to true - dispatch(requestMetersDetails()); + dispatch(metersSlice.actions.requestMetersDetails()); // attempt to retrieve meters details from database const meters = await metersApi.getMetersDetails(); // update the state with the meters details and set isFetching to false - dispatch(receiveMetersDetails(meters)); + dispatch(metersSlice.actions.receiveMetersDetails(meters)); // If this is the first fetch, inform the store that the first fetch has been made if (!getState().meters.hasBeenFetchedOnce) { - dispatch(confirmMetersFetchedOnce()); + dispatch(metersSlice.actions.confirmMetersFetchedOnce()); } } } } -export function changeDisplayedMeters(meters: number[]): t.ChangeDisplayedMetersAction { - return { type: ActionType.ChangeDisplayedMeters, selectedMeters: meters }; -} - -// Pushes meterId onto submitting meters state array -export function submitMeterEdits(meterId: number): t.SubmitEditedMeterAction { - return { type: ActionType.SubmitEditedMeter, meterId }; -} - -export function confirmMeterEdits(editedMeter: t.MeterEditData): t.ConfirmEditedMeterAction { - return { type: ActionType.ConfirmEditedMeter, editedMeter }; -} - -export function confirmMeterAdd(addedMeter: t.MeterEditData): t.ConfirmAddMeterAction { - return { type: ActionType.ConfirmAddMeter, addedMeter }; -} - -export function deleteSubmittedMeter(meterId: number): t.DeleteSubmittedMeterAction { - return { type: ActionType.DeleteSubmittedMeter, meterId } -} - -export function confirmMetersFetchedOnce(): t.ConfirmMetersFetchedOnceAction { - return { type: ActionType.ConfirmMetersFetchedOnce }; -} - // Fetch the meters details from the database if they have not already been fetched once export function fetchMetersDetailsIfNeeded(): Thunk { return (dispatch: Dispatch, getState: GetState) => { @@ -78,7 +47,7 @@ export function submitEditedMeter(editedMeter: t.MeterData, shouldRefreshReading if (getState().meters.submitting.indexOf(editedMeter.id) === -1) { // Inform the store we are about to edit the passed in meter // Pushes meterId of the meterData to submit onto the submitting state array - dispatch(submitMeterEdits(editedMeter.id)); + dispatch(metersSlice.actions.submitEditedMeter(editedMeter.id)); // Attempt to edit the meter in the database try { @@ -88,9 +57,9 @@ export function submitEditedMeter(editedMeter: t.MeterData, shouldRefreshReading dispatch(updateCikAndDBViewsIfNeeded(false, shouldRefreshReadingViews)); const changedMeter = await metersApi.edit(editedMeter); // Clear meter Id from submitting state array - dispatch(deleteSubmittedMeter(editedMeter.id)); + dispatch(metersSlice.actions.deleteSubmittedMeter(editedMeter.id)); // Update the store with our new edits based on what came from DB. - dispatch(confirmMeterEdits(changedMeter)); + dispatch(metersSlice.actions.confirmAddMeter(changedMeter)); // Success! showSuccessNotification(translate('meter.successfully.edited.meter')); } catch (err) { @@ -99,21 +68,21 @@ export function submitEditedMeter(editedMeter: t.MeterData, shouldRefreshReading window.alert(translate('meter.failed.to.edit.meter') + '"' + err.response.data as string + '"'); // Clear our changes from to the submitting meters state // We must do this in case fetch failed to keep the store in sync with the database - dispatch(deleteSubmittedMeter(editedMeter.id)); + dispatch(metersSlice.actions.deleteSubmittedMeter(editedMeter.id)); } } }; } // Add meter to database -export function addMeter(meter: t.MeterEditData): Thunk { +export function addMeter(meter: t.MeterData): Thunk { return async (dispatch: Dispatch) => { try { // Attempt to add meter to database const meterChanged = await metersApi.addMeter(meter); // Update the store with our new edits based on what came from DB. // The id and reading frequency may have been updated. - dispatch(confirmMeterAdd(meterChanged)); + dispatch(metersSlice.actions.confirmAddMeter(meterChanged)); showSuccessNotification(translate('meter.successfully.create.meter')); } catch (err) { // TODO Better way than popup with React but want to stay so user can read/copy. diff --git a/src/client/app/actions/version.ts b/src/client/app/actions/version.ts index 95628ff8e..fb7df3252 100644 --- a/src/client/app/actions/version.ts +++ b/src/client/app/actions/version.ts @@ -3,24 +3,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { versionApi } from '../utils/api'; -import { Thunk, ActionType, Dispatch, GetState } from '../types/redux/actions'; +import { Thunk, Dispatch, GetState } from '../types/redux/actions'; import { State } from '../types/redux/state'; -import * as t from '../types/redux/version'; +import { versionSlice } from '../reducers/version'; -/** - * Request version action - */ -export function requestVersion(): t.RequestVersion { - return { type: ActionType.RequestVersion }; -} - -/** - * Receive version action - * @param data Received version - */ -export function receiveVersion(data: string): t.ReceiveVersion { - return { type: ActionType.ReceiveVersion, data }; -} /** * @param state The redux state. @@ -35,10 +21,10 @@ function shouldFetchVersion(state: State): boolean { */ export function fetchVersion(): Thunk { return async (dispatch: Dispatch) => { - dispatch(requestVersion()); + dispatch(versionSlice.actions.requestVersion()); // Returns the version string const version = await versionApi.getVersion(); - return dispatch(receiveVersion(version)); + return dispatch(versionSlice.actions.receiveVersion(version)); }; } diff --git a/src/client/app/reducers/index.ts b/src/client/app/reducers/index.ts index 67346ef91..1c790715c 100644 --- a/src/client/app/reducers/index.ts +++ b/src/client/app/reducers/index.ts @@ -3,14 +3,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { combineReducers } from 'redux'; -import meters from './meters'; +import { metersSlice } from './meters'; import lineReadings from './lineReadings'; import barReadings from './barReadings'; import compareReadings from './compareReadings'; import groups from './groups'; import maps from './maps'; import admin from './admin'; -import version from './version'; +import { versionSlice } from './version'; import { currentUserSlice } from './currentUser'; import { unsavedWarningSlice } from './unsavedWarning'; import units from './units'; @@ -21,7 +21,7 @@ import { graphSlice } from './graph'; export default combineReducers({ - meters, + meters: metersSlice.reducer, readings: combineReducers({ line: lineReadings, bar: barReadings, @@ -32,7 +32,7 @@ export default combineReducers({ maps, groups, admin, - version, + version: versionSlice.reducer, currentUser: currentUserSlice.reducer, unsavedWarning: unsavedWarningSlice.reducer, units, diff --git a/src/client/app/reducers/meters.ts b/src/client/app/reducers/meters.ts index a97eb0ccb..0b41a8e2b 100644 --- a/src/client/app/reducers/meters.ts +++ b/src/client/app/reducers/meters.ts @@ -2,9 +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 * as _ from 'lodash'; -import { MetersAction, MetersState } from '../types/redux/meters'; -import { ActionType } from '../types/redux/actions'; +import { MetersState } from '../types/redux/meters'; import { durationFormat } from '../utils/durationFormat'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import * as t from '../types/redux/meters' const defaultState: MetersState = { hasBeenFetchedOnce: false, @@ -14,84 +15,37 @@ const defaultState: MetersState = { submitting: [] }; -export default function meters(state = defaultState, action: MetersAction) { - switch (action.type) { - case ActionType.ConfirmMetersFetchedOnce: { - return { - ...state, - hasBeenFetchedOnce: true - }; - } - case ActionType.RequestMetersDetails: { - return { - ...state, - isFetching: true - }; - } - case ActionType.ReceiveMetersDetails: { - // Convert the readingFrequency from the DB format to user friendly format. - action.data.forEach(meter => { - meter.readingFrequency = durationFormat(meter.readingFrequency); - }); - return { - ...state, - isFetching: false, - byMeterID: _.keyBy(action.data, meter => meter.id) - }; - } - case ActionType.ChangeDisplayedMeters: { - return { - ...state, - selectedMeters: action.selectedMeters - }; - } - case ActionType.SubmitEditedMeter: { - const submitting = state.submitting; - submitting.push(action.meterId); - return { - ...state, - submitting - }; - } - case ActionType.ConfirmEditedMeter: { - // Return new state object with updated edited meter info. - // Convert the readingFrequency from the DB format to user friendly format. - action.editedMeter.readingFrequency = durationFormat(action.editedMeter.readingFrequency); - return { - ...state, - byMeterID: { - ...state.byMeterID, - [action.editedMeter.id]: { - ...action.editedMeter - } - } - }; - } - case ActionType.ConfirmAddMeter: { - // Return new state object with updated edited meter info. - // Convert the readingFrequency from the DB format to user friendly format. - action.addedMeter.readingFrequency = durationFormat(action.addedMeter.readingFrequency); - return { - ...state, - byMeterID: { - ...state.byMeterID, - [action.addedMeter.id]: { - ...action.addedMeter - } - } - }; - } - case ActionType.DeleteSubmittedMeter: { - // Remove the current submitting meter from the submitting state - const submitting = state.submitting; - submitting.splice(submitting.indexOf(action.meterId)); - return { - ...state, - submitting - }; - } - default: { - return state; + +export const metersSlice = createSlice({ + name: 'meters', + initialState: defaultState, + reducers: { + confirmMetersFetchedOnce: state => { + state.hasBeenFetchedOnce = true; + }, + requestMetersDetails: state => { + state.isFetching = true; + }, + receiveMetersDetails: (state, action: PayloadAction) => { + state.isFetching = false; + state.byMeterID = _.keyBy(action.payload, meter => meter.id); + }, + changeDisplayedMeters: (state, action: PayloadAction) => { + state.selectedMeters = action.payload; + }, + submitEditedMeter: (state, action: PayloadAction) => { + state.submitting.push(action.payload); + }, + confirmEditedMeter: (state, action: PayloadAction) => { + action.payload.readingFrequency = durationFormat(action.payload.readingFrequency); + state.byMeterID[action.payload.id] = action.payload; + }, + confirmAddMeter: (state, action: PayloadAction) => { + action.payload.readingFrequency = durationFormat(action.payload.readingFrequency); + state.byMeterID[action.payload.id] = action.payload; + }, + deleteSubmittedMeter: (state, action: PayloadAction) => { + state.submitting.splice(state.submitting.indexOf(action.payload)); } } -} +}); \ No newline at end of file diff --git a/src/client/app/reducers/version.ts b/src/client/app/reducers/version.ts index 5947668db..191db9bef 100644 --- a/src/client/app/reducers/version.ts +++ b/src/client/app/reducers/version.ts @@ -2,30 +2,59 @@ * 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 { VersionAction, VersionState } from '../types/redux/version'; -import { ActionType } from '../types/redux/actions'; +import { VersionState } from '../types/redux/version'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; /* * Defines store interactions when user profile related actions are dispatched to the store. */ const defaultState: VersionState = { isFetching: false, version: '' }; - -export default function version(state = defaultState, action: VersionAction) { - switch (action.type) { - case ActionType.RequestVersion: - // When version is requested, indicate app is fetching data from API - return { - ...state, - isFetching: true - }; - case ActionType.ReceiveVersion: - // When version is received, update the store with result from API - return { - ...state, - isFetching: false, - version: action.data - }; - default: - return state; +export const versionSlice = createSlice({ + name: 'version', + initialState: defaultState, + reducers: { + // case ActionType.RequestVersion: + // // When version is requested, indicate app is fetching data from API + // return { + // ...state, + // isFetching: true + // }; + requestVersion: state => { + state.isFetching = true; + }, + // case ActionType.ReceiveVersion: + // // When version is received, update the store with result from API + // return { + // ...state, + // isFetching: false, + // version: action.data + // }; + receiveVersion: (state, action: PayloadAction) => { + state.isFetching = false; + state.version = action.payload; + } } -} \ No newline at end of file +}); +// default: +// return state; +// } + +// export default function version(state = defaultState, action: VersionAction) { +// switch (action.type) { +// case ActionType.RequestVersion: +// // When version is requested, indicate app is fetching data from API +// return { +// ...state, +// isFetching: true +// }; +// case ActionType.ReceiveVersion: +// // When version is received, update the store with result from API +// return { +// ...state, +// isFetching: false, +// version: action.data +// }; +// default: +// return state; +// } +// } \ No newline at end of file diff --git a/src/client/app/types/redux/meters.ts b/src/client/app/types/redux/meters.ts index 55eb46221..33651b5bc 100644 --- a/src/client/app/types/redux/meters.ts +++ b/src/client/app/types/redux/meters.ts @@ -2,55 +2,55 @@ * 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 { GPSPoint } from 'utils/calibration'; -import { ActionType } from './actions'; +// import { ActionType } from './actions'; import { AreaUnitType } from 'utils/getAreaUnitConversion'; -export interface RequestMetersDetailsAction { - type: ActionType.RequestMetersDetails; -} +// export interface RequestMetersDetailsAction { +// type: ActionType.RequestMetersDetails; +// } -export interface ReceiveMetersDetailsAction { - type: ActionType.ReceiveMetersDetails; - data: MeterData[]; -} +// export interface ReceiveMetersDetailsAction { +// type: ActionType.ReceiveMetersDetails; +// data: MeterData[]; +// } -export interface ChangeDisplayedMetersAction { - type: ActionType.ChangeDisplayedMeters; - selectedMeters: number[]; -} +// export interface ChangeDisplayedMetersAction { +// type: ActionType.ChangeDisplayedMeters; +// selectedMeters: number[]; +// } -export interface ConfirmEditedMeterAction { - type: ActionType.ConfirmEditedMeter; - editedMeter: MeterEditData; -} +// export interface ConfirmEditedMeterAction { +// type: ActionType.ConfirmEditedMeter; +// editedMeter: MeterEditData; +// } -export interface ConfirmAddMeterAction { - type: ActionType.ConfirmAddMeter; - addedMeter: MeterEditData; -} +// export interface ConfirmAddMeterAction { +// type: ActionType.ConfirmAddMeter; +// addedMeter: MeterEditData; +// } -export interface DeleteSubmittedMeterAction { - type: ActionType.DeleteSubmittedMeter; - meterId: number; -} +// export interface DeleteSubmittedMeterAction { +// type: ActionType.DeleteSubmittedMeter; +// meterId: number; +// } -export interface SubmitEditedMeterAction { - type: ActionType.SubmitEditedMeter; - meterId: number; -} +// export interface SubmitEditedMeterAction { +// type: ActionType.SubmitEditedMeter; +// meterId: number; +// } -export interface ConfirmMetersFetchedOnceAction { - type: ActionType.ConfirmMetersFetchedOnce; -} +// export interface ConfirmMetersFetchedOnceAction { +// type: ActionType.ConfirmMetersFetchedOnce; +// } -export type MetersAction = RequestMetersDetailsAction -| ReceiveMetersDetailsAction -| ChangeDisplayedMetersAction -| ConfirmEditedMeterAction -| ConfirmAddMeterAction -| DeleteSubmittedMeterAction -| SubmitEditedMeterAction -| ConfirmMetersFetchedOnceAction; +// export type MetersAction = RequestMetersDetailsAction +// | ReceiveMetersDetailsAction +// | ChangeDisplayedMetersAction +// | ConfirmEditedMeterAction +// | ConfirmAddMeterAction +// | DeleteSubmittedMeterAction +// | SubmitEditedMeterAction +// | ConfirmMetersFetchedOnceAction; // The relates to the JS object Meter.types for the same use in src/server/models/Meter.js. // They should be kept in sync. @@ -70,43 +70,6 @@ export enum MeterTimeSortType { } export interface MeterData { - id: number; - identifier: string; - name: string; - area: number; - enabled: boolean; - displayable: boolean; - meterType: string; - url: string; - timeZone: string; - gps: GPSPoint | null; - unitId: number; - defaultGraphicUnit: number; - note: string; - cumulative: boolean; - cumulativeReset: boolean; - cumulativeResetStart: string; - cumulativeResetEnd: string; - endOnlyTime: boolean; - reading: number; - readingGap: number; - readingVariation: number; - readingDuplication: number; - timeSort: string; - startTimestamp: string; - endTimestamp: string; - previousEnd: string; - areaUnit: AreaUnitType; - readingFrequency: string; - minVal: number; - maxVal: number; - minDate: string; - maxDate: string; - maxError: number; - disableChecks: boolean; -} - -export interface MeterEditData { id: number; identifier: string; name: string; diff --git a/src/client/app/types/redux/version.ts b/src/client/app/types/redux/version.ts index aba0f3508..f62a3aba1 100644 --- a/src/client/app/types/redux/version.ts +++ b/src/client/app/types/redux/version.ts @@ -2,22 +2,21 @@ * 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'; /* * Defines the action interfaces used in the corresponding reducers. */ -export interface RequestVersion { - type: ActionType.RequestVersion; -} +// export interface RequestVersion { +// type: ActionType.RequestVersion; +// } -export interface ReceiveVersion { - type: ActionType.ReceiveVersion; - data: string; -} +// export interface ReceiveVersion { +// type: ActionType.ReceiveVersion; +// data: string; +// } -export type VersionAction = RequestVersion | ReceiveVersion; +// export type VersionAction = RequestVersion | ReceiveVersion; export interface VersionState { isFetching: boolean; From 9c5405cbcda7b7379a5fb8fa0fe7519a8773bf76 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Mon, 18 Sep 2023 19:31:33 +0000 Subject: [PATCH 008/131] Incremental RTK Adoptions --- src/client/app/actions/admin.ts | 111 +----- src/client/app/actions/conversions.ts | 63 +--- src/client/app/actions/groups.ts | 59 +--- src/client/app/actions/units.ts | 43 +-- .../app/components/MeterDropDownComponent.tsx | 4 +- .../components/admin/PreferencesComponent.tsx | 55 +-- .../app/containers/MeterDropdownContainer.ts | 4 +- .../containers/admin/PreferencesContainer.ts | 80 +++-- src/client/app/index.tsx | 18 - src/client/app/reducers/admin.ts | 252 +++++-------- src/client/app/reducers/conversions.ts | 117 +++--- src/client/app/reducers/groups.ts | 332 +++++++++++------- src/client/app/reducers/index.ts | 19 +- src/client/app/reducers/units.ts | 91 ++--- src/client/app/store.ts | 6 +- src/client/app/types/redux/admin.ts | 137 -------- src/client/app/types/redux/conversions.ts | 49 --- src/client/app/types/redux/groups.ts | 62 ---- src/client/app/types/redux/units.ts | 43 --- src/client/app/utils/api/MetersApi.ts | 10 +- 20 files changed, 498 insertions(+), 1057 deletions(-) diff --git a/src/client/app/actions/admin.ts b/src/client/app/actions/admin.ts index 27449f6d1..a34d62614 100644 --- a/src/client/app/actions/admin.ts +++ b/src/client/app/actions/admin.ts @@ -3,115 +3,23 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { showErrorNotification, showSuccessNotification } from '../utils/notifications'; -import { ChartTypes } from '../types/redux/graph'; -import { PreferenceRequestItem } from '../types/items'; -import * as t from '../types/redux/admin'; -import { ActionType, Dispatch, GetState, Thunk } from '../types/redux/actions'; +import { Dispatch, GetState, Thunk } from '../types/redux/actions'; import { State } from '../types/redux/state'; import { conversionArrayApi, preferencesApi } from '../utils/api'; import translate from '../utils/translate'; -import { LanguageTypes } from '../types/redux/i18n'; import * as moment from 'moment'; -import { AreaUnitType } from '../utils/getAreaUnitConversion'; import { updateSelectedLanguage } from './options'; import { graphSlice } from '../reducers/graph'; -export function updateSelectedMeter(meterID: number): t.UpdateImportMeterAction { - return { type: ActionType.UpdateImportMeter, meterID }; -} - -export function updateDisplayTitle(displayTitle: string): t.UpdateDisplayTitleAction { - return { type: ActionType.UpdateDisplayTitle, displayTitle }; -} - -export function updateTimeZone(timeZone: string): t.UpdateDefaultTimeZone { - return { type: ActionType.UpdateDefaultTimeZone, timeZone }; -} - -export function updateDefaultChartToRender(defaultChartToRender: ChartTypes): t.UpdateDefaultChartToRenderAction { - return { type: ActionType.UpdateDefaultChartToRender, defaultChartToRender }; -} - -export function toggleDefaultBarStacking(): t.ToggleDefaultBarStackingAction { - return { type: ActionType.ToggleDefaultBarStacking }; -} - -export function toggleDefaultAreaNormalization(): t.ToggleDefaultAreaNormalizationAction { - return { type: ActionType.ToggleDefaultAreaNormalization }; -} - -export function updateDefaultAreaUnit(defaultAreaUnit: AreaUnitType): t.UpdateDefaultAreaUnitAction { - return { type: ActionType.UpdateDefaultAreaUnit, defaultAreaUnit }; -} - -export function updateDefaultLanguage(defaultLanguage: LanguageTypes): t.UpdateDefaultLanguageAction { - moment.locale(defaultLanguage); - return { type: ActionType.UpdateDefaultLanguage, defaultLanguage }; -} - -export function updateDefaultWarningFileSize(defaultWarningFileSize: number): t.UpdateDefaultWarningFileSize { - return { type: ActionType.UpdateDefaultWarningFileSize, defaultWarningFileSize }; -} - -export function updateDefaultFileSizeLimit(defaultFileSizeLimit: number): t.UpdateDefaultFileSizeLimit { - return { type: ActionType.UpdateDefaultFileSizeLimit, defaultFileSizeLimit }; -} - -export function updateDefaultMeterReadingFrequency(defaultMeterReadingFrequency: string): t.UpdateDefaultMeterReadingFrequencyAction { - return { type: ActionType.UpdateDefaultMeterReadingFrequency, defaultMeterReadingFrequency }; -} - -export function updateDefaultMeterMinimumValue(defaultMeterMinimumValue: number): t.UpdateDefaultMeterMinimumValueAction { - return { type: ActionType.UpdateDefaultMeterMinimumValue, defaultMeterMinimumValue }; -} - -export function updateDefaultMeterMaximumValue(defaultMeterMaximumValue: number): t.UpdateDefaultMeterMaximumValueAction { - return { type: ActionType.UpdateDefaultMeterMaximumValue, defaultMeterMaximumValue }; -} - -export function updateDefaultMeterMinimumDate(defaultMeterMinimumDate: string): t.UpdateDefaultMeterMinimumDateAction { - return { type: ActionType.UpdateDefaultMeterMinimumDate, defaultMeterMinimumDate }; -} - -export function updateDefaultMeterMaximumDate(defaultMeterMaximumDate: string): t.UpdateDefaultMeterMaximumDateAction { - return { type: ActionType.UpdateDefaultMeterMaximumDate, defaultMeterMaximumDate }; -} - -export function updateDefaultMeterReadingGap(defaultMeterReadingGap: number): t.UpdateDefaultMeterReadingGapAction { - return { type: ActionType.UpdateDefaultMeterReadingGap, defaultMeterReadingGap }; -} - -export function updateDefaultMeterMaximumErrors(defaultMeterMaximumErrors: number): t.UpdateDefaultMeterMaximumErrorsAction { - return { type: ActionType.UpdateDefaultMeterMaximumErrors, defaultMeterMaximumErrors }; -} - -export function updateDefaultMeterDisableChecks(defaultMeterDisableChecks: boolean): t.UpdateDefaultMeterDisableChecksAction { - return { type: ActionType.UpdateDefaultMeterDisableChecks, defaultMeterDisableChecks }; -} - -function requestPreferences(): t.RequestPreferencesAction { - return { type: ActionType.RequestPreferences }; -} - -function receivePreferences(data: PreferenceRequestItem): t.ReceivePreferencesAction { - return { type: ActionType.ReceivePreferences, data }; -} - -function markPreferencesNotSubmitted(): t.MarkPreferencesNotSubmittedAction { - return { type: ActionType.MarkPreferencesNotSubmitted }; -} - -function markPreferencesSubmitted(defaultMeterReadingFrequency: string): t.MarkPreferencesSubmittedAction { - return { type: ActionType.MarkPreferencesSubmitted, defaultMeterReadingFrequency }; -} +import { adminSlice } from '../reducers/admin'; /** * Dispatches a fetch for admin preferences and sets the state based upon the result */ function fetchPreferences(): Thunk { return async (dispatch: Dispatch, getState: GetState) => { - dispatch(requestPreferences()); + dispatch(adminSlice.actions.requestPreferences()); const preferences = await preferencesApi.getPreferences(); - dispatch(receivePreferences(preferences)); + dispatch(adminSlice.actions.receivePreferences(preferences)); moment.locale(getState().admin.defaultLanguage); if (!getState().graph.hotlinked) { dispatch((dispatch2: Dispatch) => { @@ -195,10 +103,10 @@ export function submitPreferences() { }); // Only return the defaultMeterReadingFrequency because the value from the DB // generally differs from what the user input so update state with DB value. - dispatch(markPreferencesSubmitted(preferences.defaultMeterReadingFrequency)); + dispatch(adminSlice.actions.markPreferencesSubmitted(preferences.defaultMeterReadingFrequency)); showSuccessNotification(translate('updated.preferences')); } catch (e) { - dispatch(markPreferencesNotSubmitted()); + dispatch(adminSlice.actions.markPreferencesNotSubmitted()); showErrorNotification(translate('failed.to.submit.changes')); } }; @@ -238,9 +146,6 @@ export function submitPreferencesIfNeeded(): Thunk { }; } -function toggleWaitForCikAndDB(): t.ToggleWaitForCikAndDB { - return { type: ActionType.ToggleWaitForCikAndDB }; -} /** * @param state The redux state. @@ -260,10 +165,10 @@ export function updateCikAndDBViewsIfNeeded(shouldRedoCik: boolean, shouldRefres return async (dispatch: Dispatch, getState: GetState) => { if (shouldUpdateCikAndDBViews(getState())) { // set the page to a loading state - dispatch(toggleWaitForCikAndDB()); + dispatch(adminSlice.actions.toggleWaitForCikAndDB()); await conversionArrayApi.refresh(shouldRedoCik, shouldRefreshReadingViews); // revert to normal state once refresh is complete - dispatch(toggleWaitForCikAndDB()); + dispatch(adminSlice.actions.toggleWaitForCikAndDB()); if (shouldRedoCik || shouldRefreshReadingViews) { // Only reload window if redoCik and/or refresh reading views. window.location.reload(); diff --git a/src/client/app/actions/conversions.ts b/src/client/app/actions/conversions.ts index 6f95a22fb..15e0cb5f5 100644 --- a/src/client/app/actions/conversions.ts +++ b/src/client/app/actions/conversions.ts @@ -2,74 +2,33 @@ * 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, Thunk, Dispatch, GetState } from '../types/redux/actions'; +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 requestConversionsDetails(): t.RequestConversionsDetailsAction { - return { type: ActionType.RequestConversionsDetails }; -} - -export function receiveConversionsDetails(data: t.ConversionData[]): t.ReceiveConversionsDetailsAction { - return { type: ActionType.ReceiveConversionsDetails, data }; -} 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(requestConversionsDetails()); + 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(receiveConversionsDetails(conversions)); + 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(confirmConversionsFetchedOnce()); + dispatch(conversionsSlice.actions.conversionsFetchedOnce()); } } } } -export function changeDisplayedConversions(conversions: number[]): t.ChangeDisplayedConversionsAction { - return { type: ActionType.ChangeDisplayedConversions, selectedConversions: conversions }; -} - -/** - * Pushes ConversionData onto submitting conversions state array - * @param conversionData data to push - */ -export function submitConversionEdits(conversionData: t.ConversionData): t.SubmitEditedConversionAction { - return { type: ActionType.SubmitEditedConversion, conversionData}; -} - -export function confirmConversionEdits(editedConversion: t.ConversionData): t.ConfirmEditedConversionAction { - return { type: ActionType.ConfirmEditedConversion, editedConversion }; -} - -/** - * Removes ConversionData from submitting state array - * @param conversionData data to remove - */ -export function deleteSubmittedConversion(conversionData: t.ConversionData): t.DeleteSubmittedConversionAction { - return {type: ActionType.DeleteSubmittedConversion, conversionData} -} - -export function confirmConversionsFetchedOnce(): t.ConfirmConversionsFetchedOnceAction { - return { type: ActionType.ConfirmConversionsFetchedOnce }; -} - -/** - * Removes the passed ConversionData from the store - * @param conversionData data to remove - */ -export function confirmDeletedConversion(conversionData: t.ConversionData): t.DeleteConversionAction { - return { type: ActionType.DeleteConversion, conversionData } -} /** * Fetch the conversions details from the database if they have not already been fetched once @@ -98,14 +57,14 @@ export function submitEditedConversion(editedConversion: t.ConversionData, shoul 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(submitConversionEdits(editedConversion)); + 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(confirmConversionEdits(editedConversion)); + dispatch(conversionsSlice.actions.confirmEditedConversion(editedConversion)); // Success! showSuccessNotification(translate('conversion.successfully.edited.conversion')); } catch (err) { @@ -113,7 +72,7 @@ export function submitEditedConversion(editedConversion: t.ConversionData, shoul showErrorNotification(translate('conversion.failed.to.edit.conversion') + ' "' + err.response.data as string + '"'); } // Clear conversionData object from submitting state array - dispatch(deleteSubmittedConversion(editedConversion)); + dispatch(conversionsSlice.actions.deleteSubmittedConversion(editedConversion)); } }; } @@ -150,7 +109,7 @@ export function deleteConversion(conversion: t.ConversionData): (dispatch: Dispa if (conversionDataIndex === -1) { // Inform the store we are about to work on this conversion // Update the submitting state array - dispatch(submitConversionEdits(conversion)); + dispatch(conversionsSlice.actions.submitEditedConversion(conversion)); try { // Attempt to delete the conversion from the database await conversionsApi.delete(conversion); @@ -158,13 +117,13 @@ export function deleteConversion(conversion: t.ConversionData): (dispatch: Dispa dispatch(updateCikAndDBViewsIfNeeded(true, false)); // Delete was successful // Update the store to match - dispatch(confirmDeletedConversion(conversion)); + 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(deleteSubmittedConversion(conversion)); + dispatch(conversionsSlice.actions.deleteSubmittedConversion(conversion)); } } } \ No newline at end of file diff --git a/src/client/app/actions/groups.ts b/src/client/app/actions/groups.ts index 67245054f..90712cac2 100644 --- a/src/client/app/actions/groups.ts +++ b/src/client/app/actions/groups.ts @@ -2,57 +2,27 @@ * 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 { Dispatch, GetState, Thunk, ActionType } from '../types/redux/actions'; +import { Dispatch, GetState, Thunk } from '../types/redux/actions'; import { State } from '../types/redux/state'; import { showErrorNotification, showSuccessNotification } from '../utils/notifications'; import * as t from '../types/redux/groups'; import { groupsApi } from '../utils/api'; import translate from '../utils/translate'; - -function requestGroupsDetails(): t.RequestGroupsDetailsAction { - return { type: ActionType.RequestGroupsDetails }; -} - -function receiveGroupsDetails(data: t.GroupDetailsData[]): t.ReceiveGroupsDetailsAction { - return { type: ActionType.ReceiveGroupsDetails, data }; -} - -function requestGroupChildren(groupID: number): t.RequestGroupChildrenAction { - return { type: ActionType.RequestGroupChildren, groupID }; -} - -function receiveGroupChildren(groupID: number, data: { meters: number[], groups: number[], deepMeters: number[] }): t.ReceiveGroupChildrenAction { - return { type: ActionType.ReceiveGroupChildren, groupID, data }; -} - -function requestAllGroupsChildren(): t.RequestAllGroupsChildrenAction { - return { type: ActionType.RequestAllGroupsChildren }; -} - -function receiveAllGroupsChildren(data: t.GroupChildren[]): t.ReceiveAllGroupsChildrenAction { - return { type: ActionType.ReceiveAllGroupsChildren, data }; -} - -export function changeDisplayedGroups(groupIDs: number[]): t.ChangeDisplayedGroupsAction { - return { type: ActionType.ChangeDisplayedGroups, groupIDs }; -} +import { groupsSlice } from '../reducers/groups'; export function fetchGroupsDetails(): Thunk { return async (dispatch: Dispatch, getState: GetState) => { - dispatch(requestGroupsDetails()); + dispatch(groupsSlice.actions.requestGroupsDetails()); // Returns the names, IDs and most info of all groups in the groups table. const groupsDetails = await groupsApi.details(); - dispatch(receiveGroupsDetails(groupsDetails)); + dispatch(groupsSlice.actions.receiveGroupsDetails(groupsDetails)); // If this is the first fetch, inform the store that the first fetch has been made if (!getState().groups.hasBeenFetchedOnce) { - dispatch(confirmGroupsFetchedOnce()); + dispatch(groupsSlice.actions.confirmGroupsFetchedOnce()); } }; } -export function confirmGroupsFetchedOnce(): t.ConfirmGroupsFetchedOnceAction { - return { type: ActionType.ConfirmGroupsFetchedOnce }; -} function shouldFetchGroupsDetails(state: State): boolean { // If isFetching then don't do this. If already fetched then don't do this. @@ -80,9 +50,9 @@ function shouldFetchGroupChildren(state: State, groupID: number) { function fetchGroupChildren(groupID: number) { return async (dispatch: Dispatch) => { - dispatch(requestGroupChildren(groupID)); + dispatch(groupsSlice.actions.requestGroupChildren(groupID)); const childGroupIDs = await groupsApi.children(groupID); - dispatch(receiveGroupChildren(groupID, childGroupIDs)); + dispatch(groupsSlice.actions.receiveGroupChildren({ groupID, data: childGroupIDs })); }; } @@ -103,22 +73,19 @@ function fetchAllGroupChildren(): Thunk { // ensure a fetch is not currently happening if (!getState().groups.isFetchingAllChildren) { // set isFetching to true - dispatch(requestAllGroupsChildren()); + dispatch(groupsSlice.actions.requestAllGroupsChildren()); // Retrieve all groups children from database const groupsChildren = await groupsApi.getAllGroupsChildren(); // update the state with all groups children - dispatch(receiveAllGroupsChildren(groupsChildren)); + dispatch(groupsSlice.actions.receiveAllGroupsChildren(groupsChildren)); // If this is the first fetch, inform the store that the first fetch has been made if (!getState().groups.hasChildrenBeenFetchedOnce) { - dispatch(confirmAllGroupsChildrenFetchedOnce()); + dispatch(groupsSlice.actions.confirmAllGroupsChildrenFetchedOnce()); } } } } -export function confirmAllGroupsChildrenFetchedOnce(): t.ConfirmAllGroupsChildrenFetchedOnceAction { - return { type: ActionType.ConfirmAllGroupsChildrenFetchedOnce }; -} export function fetchAllGroupChildrenIfNeeded(): Thunk { return (dispatch: Dispatch, getState: GetState) => { @@ -156,10 +123,6 @@ export function submitNewGroup(group: t.GroupData): Dispatch { }; } -export function confirmGroupEdits(editedGroup: t.GroupEditData): t.ConfirmEditedGroupAction { - return { type: ActionType.ConfirmEditedGroup, editedGroup }; -} - /** * Pushes group changes out to DB. * @param group The group to update @@ -182,7 +145,7 @@ export function submitGroupEdits(group: t.GroupEditData, reload: boolean = true) window.location.reload(); } else { // If we did not reload then we need to refresh the edited group's state with: - dispatch(confirmGroupEdits(group)); + dispatch(groupsSlice.actions.confirmEditedGroup(group)); // An then we need to fix up any other groups impacted. // This is removed since you won't see it. // Success! diff --git a/src/client/app/actions/units.ts b/src/client/app/actions/units.ts index 57396118a..28595c871 100644 --- a/src/client/app/actions/units.ts +++ b/src/client/app/actions/units.ts @@ -2,60 +2,33 @@ * 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, Thunk, Dispatch, GetState } from '../types/redux/actions'; +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/units'; import { unitsApi } from '../utils/api'; import { updateCikAndDBViewsIfNeeded } from './admin'; +import { unitsSlice } from '../reducers/units'; -export function requestUnitsDetails(): t.RequestUnitsDetailsAction { - return { type: ActionType.RequestUnitsDetails }; -} - -export function receiveUnitsDetails(data: t.UnitData[]): t.ReceiveUnitsDetailsAction { - return { type: ActionType.ReceiveUnitsDetails, data }; -} export function fetchUnitsDetails(): Thunk { return async (dispatch: Dispatch, getState: GetState) => { // ensure a fetch is not currently happening if (!getState().units.isFetching) { // set isFetching to true - dispatch(requestUnitsDetails()); + dispatch(unitsSlice.actions.requestUnitsDetails()); // attempt to retrieve units details from database const units = await unitsApi.getUnitsDetails(); // update the state with the units details and set isFetching to false - dispatch(receiveUnitsDetails(units)); + dispatch(unitsSlice.actions.receiveUnitsDetails(units)); // If this is the first fetch, inform the store that the first fetch has been made if (!getState().units.hasBeenFetchedOnce) { - dispatch(confirmUnitsFetchedOnce()); + dispatch(unitsSlice.actions.confirmUnitsFetchedOnce()); } } } } -export function changeDisplayedUnits(units: number[]): t.ChangeDisplayedUnitsAction { - return { type: ActionType.ChangeDisplayedUnits, selectedUnits: units }; -} - -// Pushes unitId onto submitting units state array -export function submitUnitEdits(unitId: number): t.SubmitEditedUnitAction { - return { type: ActionType.SubmitEditedUnit, unitId }; -} - -export function confirmUnitEdits(editedUnit: t.UnitData): t.ConfirmEditedUnitAction { - return { type: ActionType.ConfirmEditedUnit, editedUnit }; -} - -export function deleteSubmittedUnit(unitId: number): t.DeleteSubmittedUnitAction { - return { type: ActionType.DeleteSubmittedUnit, unitId } -} - -export function confirmUnitsFetchedOnce(): t.ConfirmUnitsFetchedOnceAction { - return { type: ActionType.ConfirmUnitsFetchedOnce }; -} - // Fetch the units details from the database if they have not already been fetched once export function fetchUnitsDetailsIfNeeded(): Thunk { return (dispatch: Dispatch, getState: GetState) => { @@ -74,7 +47,7 @@ export function submitEditedUnit(editedUnit: t.UnitData, shouldRedoCik: boolean, if (getState().units.submitting.indexOf(editedUnit.id) === -1) { // Inform the store we are about to edit the passed in unit // Pushes unitId of the unitData to submit onto the submitting state array - dispatch(submitUnitEdits(editedUnit.id)); + dispatch(unitsSlice.actions.submitEditedUnit(editedUnit.id)); // Attempt to edit the unit in the database try { @@ -82,7 +55,7 @@ export function submitEditedUnit(editedUnit: t.UnitData, shouldRedoCik: boolean, await unitsApi.edit(editedUnit); dispatch(updateCikAndDBViewsIfNeeded(shouldRedoCik, shouldRefreshReadingViews)); // Update the store with our new edits - dispatch(confirmUnitEdits(editedUnit)); + dispatch(unitsSlice.actions.confirmEditedUnit(editedUnit)); // Success! showSuccessNotification(translate('unit.successfully.edited.unit')); } catch (err) { @@ -90,7 +63,7 @@ export function submitEditedUnit(editedUnit: t.UnitData, shouldRedoCik: boolean, showErrorNotification(translate('unit.failed.to.edit.unit')); } // Clear unit Id from submitting state array - dispatch(deleteSubmittedUnit(editedUnit.id)); + dispatch(unitsSlice.actions.confirmUnitEdits(editedUnit.id)); } }; } diff --git a/src/client/app/components/MeterDropDownComponent.tsx b/src/client/app/components/MeterDropDownComponent.tsx index 9b57957c0..0b692d0af 100644 --- a/src/client/app/components/MeterDropDownComponent.tsx +++ b/src/client/app/components/MeterDropDownComponent.tsx @@ -3,12 +3,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { UpdateImportMeterAction } from '../types/redux/admin'; import { NamedIDItem } from '../types/items'; +import { adminSlice } from '../reducers/admin'; export interface MeterDropDownProps { meters: NamedIDItem[]; - updateSelectedMeter(meterID: number): UpdateImportMeterAction; + updateSelectedMeter(meterID: number): ReturnType } export default class MeterDropDownComponent extends React.Component { diff --git a/src/client/app/components/admin/PreferencesComponent.tsx b/src/client/app/components/admin/PreferencesComponent.tsx index 83707465e..081143fd5 100644 --- a/src/client/app/components/admin/PreferencesComponent.tsx +++ b/src/client/app/components/admin/PreferencesComponent.tsx @@ -5,26 +5,6 @@ import * as React from 'react'; import { Input, Button } from 'reactstrap'; import { ChartTypes } from '../../types/redux/graph'; -import { - ToggleDefaultBarStackingAction, - UpdateDefaultChartToRenderAction, - UpdateDefaultLanguageAction, - UpdateDefaultTimeZone, - UpdateDisplayTitleAction, - UpdateDefaultWarningFileSize, - UpdateDefaultFileSizeLimit, - ToggleDefaultAreaNormalizationAction, - UpdateDefaultAreaUnitAction, - UpdateDefaultMeterReadingFrequencyAction, - UpdateDefaultMeterMinimumValueAction, - UpdateDefaultMeterMaximumValueAction, - UpdateDefaultMeterMinimumDateAction, - UpdateDefaultMeterMaximumDateAction, - UpdateDefaultMeterReadingGapAction, - UpdateDefaultMeterMaximumErrorsAction, - UpdateDefaultMeterDisableChecksAction - -} from '../../types/redux/admin'; import { defineMessages, FormattedMessage, injectIntl, WrappedComponentProps } from 'react-intl'; import { LanguageTypes } from '../../types/redux/i18n'; import TimeZoneSelect from '../TimeZoneSelect'; @@ -34,6 +14,7 @@ import { AreaUnitType } from '../../utils/getAreaUnitConversion'; import translate from '../../utils/translate'; import { TrueFalseType } from '../../types/items'; import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; +import { adminSlice } from '../../reducers/admin'; interface PreferencesProps { displayTitle: string; @@ -54,24 +35,24 @@ interface PreferencesProps { defaultMeterReadingGap: number; defaultMeterMaximumErrors: number; defaultMeterDisableChecks: boolean; - updateDisplayTitle(title: string): UpdateDisplayTitleAction; - updateDefaultChartType(defaultChartToRender: ChartTypes): UpdateDefaultChartToRenderAction; - toggleDefaultBarStacking(): ToggleDefaultBarStackingAction; - toggleDefaultAreaNormalization(): ToggleDefaultAreaNormalizationAction; - updateDefaultLanguage(defaultLanguage: LanguageTypes): UpdateDefaultLanguageAction; + updateDisplayTitle(title: string): ReturnType; + updateDefaultChartType(defaultChartToRender: ChartTypes): ReturnType; + toggleDefaultBarStacking(): ReturnType; + toggleDefaultAreaNormalization(): ReturnType; + updateDefaultLanguage(defaultLanguage: LanguageTypes): ReturnType; submitPreferences(): Promise; - updateDefaultTimeZone(timeZone: string): UpdateDefaultTimeZone; - updateDefaultWarningFileSize(defaultWarningFileSize: number): UpdateDefaultWarningFileSize; - updateDefaultFileSizeLimit(defaultFileSizeLimit: number): UpdateDefaultFileSizeLimit; - updateDefaultAreaUnit(defaultAreaUnit: AreaUnitType): UpdateDefaultAreaUnitAction; - updateDefaultMeterReadingFrequency(defaultMeterReadingFrequency: string): UpdateDefaultMeterReadingFrequencyAction; - updateDefaultMeterMinimumValue(defaultMeterMinimumValue: number): UpdateDefaultMeterMinimumValueAction; - updateDefaultMeterMaximumValue(defaultMeterMaximumValue: number): UpdateDefaultMeterMaximumValueAction; - updateDefaultMeterMinimumDate(defaultMeterMinimumDate: string): UpdateDefaultMeterMinimumDateAction; - updateDefaultMeterMaximumDate(defaultMeterMaximumDate: string): UpdateDefaultMeterMaximumDateAction; - updateDefaultMeterReadingGap(defaultMeterReadingGap: number): UpdateDefaultMeterReadingGapAction; - updateDefaultMeterMaximumErrors(defaultMeterMaximumErrors: number): UpdateDefaultMeterMaximumErrorsAction; - updateDefaultMeterDisableChecks(defaultMeterDisableChecks: boolean): UpdateDefaultMeterDisableChecksAction; + updateDefaultTimeZone(timeZone: string): ReturnType; + updateDefaultWarningFileSize(defaultWarningFileSize: number): ReturnType; + updateDefaultFileSizeLimit(defaultFileSizeLimit: number): ReturnType; + updateDefaultAreaUnit(defaultAreaUnit: AreaUnitType): ReturnType; + updateDefaultMeterReadingFrequency(defaultMeterReadingFrequency: string): ReturnType; + updateDefaultMeterMinimumValue(defaultMeterMinimumValue: number): ReturnType; + updateDefaultMeterMaximumValue(defaultMeterMaximumValue: number): ReturnType; + updateDefaultMeterMinimumDate(defaultMeterMinimumDate: string): ReturnType; + updateDefaultMeterMaximumDate(defaultMeterMaximumDate: string): ReturnType; + updateDefaultMeterReadingGap(defaultMeterReadingGap: number): ReturnType; + updateDefaultMeterMaximumErrors(defaultMeterMaximumErrors: number): ReturnType; + updateDefaultMeterDisableChecks(defaultMeterDisableChecks: boolean): ReturnType; } type PreferencesPropsWithIntl = PreferencesProps & WrappedComponentProps; diff --git a/src/client/app/containers/MeterDropdownContainer.ts b/src/client/app/containers/MeterDropdownContainer.ts index 1480a5987..728a040c3 100644 --- a/src/client/app/containers/MeterDropdownContainer.ts +++ b/src/client/app/containers/MeterDropdownContainer.ts @@ -4,10 +4,10 @@ import * as _ from 'lodash'; import { connect } from 'react-redux'; -import { updateSelectedMeter } from '../actions/admin'; import MeterDropdownComponent from '../components/MeterDropDownComponent'; import { State } from '../types/redux/state'; import { Dispatch } from '../types/redux/actions'; +import { adminSlice } from '../reducers/admin'; function mapStateToProps(state: State) { return { @@ -16,7 +16,7 @@ function mapStateToProps(state: State) { } function mapDispatchToProps(dispatch: Dispatch) { return { - updateSelectedMeter: (meterID: number) => dispatch(updateSelectedMeter(meterID)) + updateSelectedMeter: (meterID: number) => dispatch(adminSlice.actions.updateImportMeter(meterID)) }; } diff --git a/src/client/app/containers/admin/PreferencesContainer.ts b/src/client/app/containers/admin/PreferencesContainer.ts index 2babca2f8..acd84bb49 100644 --- a/src/client/app/containers/admin/PreferencesContainer.ts +++ b/src/client/app/containers/admin/PreferencesContainer.ts @@ -4,31 +4,13 @@ import { connect } from 'react-redux'; import PreferencesComponent from '../../components/admin/PreferencesComponent'; -import { - updateDisplayTitle, - updateDefaultChartToRender, - toggleDefaultBarStacking, - updateTimeZone, - updateDefaultLanguage, - submitPreferencesIfNeeded, - updateDefaultWarningFileSize, - updateDefaultFileSizeLimit, - toggleDefaultAreaNormalization, - updateDefaultAreaUnit, - updateDefaultMeterReadingFrequency, - updateDefaultMeterMinimumValue, - updateDefaultMeterMaximumValue, - updateDefaultMeterMinimumDate, - updateDefaultMeterMaximumDate, - updateDefaultMeterReadingGap, - updateDefaultMeterMaximumErrors, - updateDefaultMeterDisableChecks -} from '../../actions/admin'; +import { submitPreferencesIfNeeded } from '../../actions/admin'; import { State } from '../../types/redux/state'; import { Dispatch } from '../../types/redux/actions'; import { ChartTypes } from '../../types/redux/graph'; import { LanguageTypes } from '../../types/redux/i18n'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; +import { adminSlice } from '../../reducers/admin'; function mapStateToProps(state: State) { return { @@ -55,25 +37,49 @@ function mapStateToProps(state: State) { function mapDispatchToProps(dispatch: Dispatch) { return { - updateDisplayTitle: (displayTitle: string) => dispatch(updateDisplayTitle(displayTitle)), - updateDefaultChartType: (defaultChartToRender: ChartTypes) => dispatch(updateDefaultChartToRender(defaultChartToRender)), - updateDefaultTimeZone: (timeZone: string) => dispatch(updateTimeZone(timeZone)), - toggleDefaultBarStacking: () => dispatch(toggleDefaultBarStacking()), - updateDefaultLanguage: (defaultLanguage: LanguageTypes) => dispatch(updateDefaultLanguage(defaultLanguage)), + updateDisplayTitle: (displayTitle: string) => dispatch(adminSlice.actions.updateDisplayTitle(displayTitle)), + + updateDefaultChartType: (defaultChartToRender: ChartTypes) => dispatch(adminSlice.actions.updateDefaultChartToRender(defaultChartToRender)), + + updateDefaultTimeZone: (timeZone: string) => dispatch(adminSlice.actions.updateDefaultTimeZone(timeZone)), + + toggleDefaultBarStacking: () => dispatch(adminSlice.actions.toggleDefaultBarStacking()), + + updateDefaultLanguage: (defaultLanguage: LanguageTypes) => dispatch(adminSlice.actions.updateDefaultLanguage(defaultLanguage)), + submitPreferences: () => dispatch(submitPreferencesIfNeeded()), - updateDefaultWarningFileSize: (defaultWarningFileSize: number) => dispatch(updateDefaultWarningFileSize(defaultWarningFileSize)), - updateDefaultFileSizeLimit: (defaultFileSizeLimit: number) => dispatch(updateDefaultFileSizeLimit(defaultFileSizeLimit)), - toggleDefaultAreaNormalization: () => dispatch(toggleDefaultAreaNormalization()), - updateDefaultAreaUnit: (defaultAreaUnit: AreaUnitType) => dispatch(updateDefaultAreaUnit(defaultAreaUnit)), + + updateDefaultWarningFileSize: (defaultWarningFileSize: number) => dispatch(adminSlice.actions.updateDefaultWarningFileSize(defaultWarningFileSize)), + + updateDefaultFileSizeLimit: (defaultFileSizeLimit: number) => dispatch(adminSlice.actions.updateDefaultFileSizeLimit(defaultFileSizeLimit)), + + toggleDefaultAreaNormalization: () => dispatch(adminSlice.actions.toggleDefaultAreaNormalization()), + + updateDefaultAreaUnit: (defaultAreaUnit: AreaUnitType) => dispatch(adminSlice.actions.updateDefaultAreaUnit(defaultAreaUnit)), + updateDefaultMeterReadingFrequency: (defaultMeterReadingFrequency: string) => - dispatch(updateDefaultMeterReadingFrequency(defaultMeterReadingFrequency)), - updateDefaultMeterMinimumValue: (defaultMeterMinimumValue: number) => dispatch(updateDefaultMeterMinimumValue(defaultMeterMinimumValue)), - updateDefaultMeterMaximumValue: (defaultMeterMaximumValue: number) => dispatch(updateDefaultMeterMaximumValue(defaultMeterMaximumValue)), - updateDefaultMeterMinimumDate: (defaultMeterMinimumDate: string) => dispatch(updateDefaultMeterMinimumDate(defaultMeterMinimumDate)), - updateDefaultMeterMaximumDate: (defaultMeterMaximumDate: string) => dispatch(updateDefaultMeterMaximumDate(defaultMeterMaximumDate)), - updateDefaultMeterReadingGap: (defaultMeterReadingGap: number) => dispatch(updateDefaultMeterReadingGap(defaultMeterReadingGap)), - updateDefaultMeterMaximumErrors: (defaultMeterMaximumErrors: number) => dispatch(updateDefaultMeterMaximumErrors(defaultMeterMaximumErrors)), - updateDefaultMeterDisableChecks: (defaultMeterDisableChecks: boolean) => dispatch(updateDefaultMeterDisableChecks(defaultMeterDisableChecks)) + dispatch(adminSlice.actions.updateDefaultMeterReadingFrequency(defaultMeterReadingFrequency)), + + updateDefaultMeterMinimumValue: (defaultMeterMinimumValue: number) => + dispatch(adminSlice.actions.updateDefaultMeterMinimumValue(defaultMeterMinimumValue)), + + updateDefaultMeterMaximumValue: (defaultMeterMaximumValue: number) => + dispatch(adminSlice.actions.updateDefaultMeterMaximumValue(defaultMeterMaximumValue)), + + updateDefaultMeterMinimumDate: (defaultMeterMinimumDate: string) => + dispatch(adminSlice.actions.updateDefaultMeterMinimumDate(defaultMeterMinimumDate)), + + updateDefaultMeterMaximumDate: (defaultMeterMaximumDate: string) => + dispatch(adminSlice.actions.updateDefaultMeterMaximumDate(defaultMeterMaximumDate)), + + updateDefaultMeterReadingGap: (defaultMeterReadingGap: number) => + dispatch(adminSlice.actions.updateDefaultMeterReadingGap(defaultMeterReadingGap)), + + updateDefaultMeterMaximumErrors: (defaultMeterMaximumErrors: number) => + dispatch(adminSlice.actions.updateDefaultMeterMaximumErrors(defaultMeterMaximumErrors)), + + updateDefaultMeterDisableChecks: (defaultMeterDisableChecks: boolean) => + dispatch(adminSlice.actions.updateDefaultMeterDisableChecks(defaultMeterDisableChecks)) }; } diff --git a/src/client/app/index.tsx b/src/client/app/index.tsx index a3e3e50f8..2d0438555 100644 --- a/src/client/app/index.tsx +++ b/src/client/app/index.tsx @@ -11,24 +11,6 @@ import RouteContainer from './containers/RouteContainer'; import './styles/index.css'; import initScript from './initScript'; -// Creates and applies thunk middleware to the Redux store, which is defined from the Redux reducers. -// For now we are enabling Redux debug tools on production builds. If had a good way to only do this -// when not in production mode then maybe we should remove this but it does allow for debugging. -// Comment this out if enabling traces below. -// const store = createStore(reducers, composeWithDevTools(applyMiddleware(thunkMiddleware))); - - -// Creates and applies thunk middleware to the Redux store, which is defined from the Redux reducers. -// It would be nice to enable this automatically if not in production mode. Unfortunately, the client -// side does not see the docker environment variables so it would require more work to do this. Doing -// in the initScript with a proper route would likely fix this up. -// For now, -// the developer needs to comment out the line above and uncomment the two lines below to get traces. -// The webpack rebuild should make the change while OED is running. -// Allow tracing of code. -// const composeEnhancers = composeWithDevTools({trace: true}); -// const store = createStore(reducers, composeEnhancers(applyMiddleware(thunkMiddleware))); - // Store information that would rarely change throughout using OED into the Redux store when the application first mounts. store.dispatch(initScript()); diff --git a/src/client/app/reducers/admin.ts b/src/client/app/reducers/admin.ts index de89b2764..e2fd06cec 100644 --- a/src/client/app/reducers/admin.ts +++ b/src/client/app/reducers/admin.ts @@ -3,12 +3,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { ChartTypes } from '../types/redux/graph'; -import { ActionType } from '../types/redux/actions'; -import { AdminState, AdminAction } from '../types/redux/admin'; +import { AdminState } from '../types/redux/admin'; import { LanguageTypes } from '../types/redux/i18n'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; import { durationFormat } from '../utils/durationFormat'; import * as moment from 'moment'; +import { PreferenceRequestItem } from '../types/items'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; const defaultState: AdminState = { selectedMeter: null, @@ -34,160 +35,101 @@ const defaultState: AdminState = { defaultMeterDisableChecks: false }; -export default function admin(state = defaultState, action: AdminAction) { - switch (action.type) { - case ActionType.UpdateImportMeter: - return { - ...state, - selectedMeter: action.meterID - }; - case ActionType.UpdateDisplayTitle: - return { - ...state, - displayTitle: action.displayTitle, - submitted: false - }; - case ActionType.UpdateDefaultChartToRender: - return { - ...state, - defaultChartToRender: action.defaultChartToRender, - submitted: false - }; - case ActionType.ToggleDefaultBarStacking: - return { - ...state, - defaultBarStacking: !state.defaultBarStacking, - submitted: false - }; - case ActionType.ToggleDefaultAreaNormalization: - return { - ...state, - defaultAreaNormalization: !state.defaultAreaNormalization, - submitted: false - } - case ActionType.UpdateDefaultAreaUnit: - return { - ...state, - defaultAreaUnit: action.defaultAreaUnit, - submitted: false - } - case ActionType.UpdateDefaultTimeZone: - return { - ...state, - defaultTimeZone: action.timeZone, - submitted: false - }; - case ActionType.UpdateDefaultLanguage: - return { - ...state, - defaultLanguage: action.defaultLanguage, - submitted: false - }; - case ActionType.RequestPreferences: - return { - ...state, - isFetching: true - }; - case ActionType.ReceivePreferences: - return { +export const adminSlice = createSlice({ + name: 'admin', + initialState: defaultState, + reducers: { + updateImportMeter: (state, action: PayloadAction) => { + state.selectedMeter = action.payload; + }, + updateDisplayTitle: (state, action: PayloadAction) => { + state.displayTitle = action.payload; + state.submitted = false; + }, + updateDefaultChartToRender: (state, action: PayloadAction) => { + state.defaultChartToRender = action.payload; + state.submitted = false; + }, + toggleDefaultBarStacking: state => { + state.defaultBarStacking = !state.defaultBarStacking; + state.submitted = false; + }, + toggleDefaultAreaNormalization: state => { + state.defaultAreaNormalization = !state.defaultAreaNormalization; + state.submitted = false; + }, + updateDefaultAreaUnit: (state, action: PayloadAction) => { + state.defaultAreaUnit = action.payload; + state.submitted = false; + }, + updateDefaultTimeZone: (state, action: PayloadAction) => { + state.defaultTimeZone = action.payload; + state.submitted = false; + }, + updateDefaultLanguage: (state, action: PayloadAction) => { + state.defaultLanguage = action.payload; + state.submitted = false; + }, + requestPreferences: state => { + state.isFetching = true; + }, + receivePreferences: (state, action: PayloadAction) => { + state = { ...state, isFetching: false, - displayTitle: action.data.displayTitle, - defaultChartToRender: action.data.defaultChartToRender, - defaultBarStacking: action.data.defaultBarStacking, - defaultLanguage: action.data.defaultLanguage, - defaultTimeZone: action.data.defaultTimezone, - defaultWarningFileSize: action.data.defaultWarningFileSize, - defaultFileSizeLimit: action.data.defaultFileSizeLimit, - defaultAreaNormalization: action.data.defaultAreaNormalization, - defaultAreaUnit: action.data.defaultAreaUnit, - defaultMeterReadingFrequency: durationFormat(action.data.defaultMeterReadingFrequency), - defaultMeterMinimumValue: action.data.defaultMeterMinimumValue, - defaultMeterMaximumValue: action.data.defaultMeterMaximumValue, - defaultMeterMinimumDate: action.data.defaultMeterMinimumDate, - defaultMeterMaximumDate: action.data.defaultMeterMaximumDate, - defaultMeterReadingGap: action.data.defaultMeterReadingGap, - defaultMeterMaximumErrors: action.data.defaultMeterMaximumErrors, - defaultMeterDisableChecks: action.data.defaultMeterDisableChecks - }; - case ActionType.MarkPreferencesNotSubmitted: - return { - ...state, - submitted: false - }; - case ActionType.MarkPreferencesSubmitted: - return { - ...state, - // Convert the duration returned from Postgres into more human format. - defaultMeterReadingFrequency: durationFormat(action.defaultMeterReadingFrequency), - submitted: true - }; - case ActionType.UpdateDefaultWarningFileSize: - return { - ...state, - defaultWarningFileSize: action.defaultWarningFileSize, - submitted: false - } - case ActionType.UpdateDefaultFileSizeLimit: - return { - ...state, - defaultFileSizeLimit: action.defaultFileSizeLimit, - submitted: false - } - case ActionType.ToggleWaitForCikAndDB: - return { - ...state, - isUpdatingCikAndDBViews: !state.isUpdatingCikAndDBViews - } - case ActionType.UpdateDefaultMeterReadingFrequency: - return { - ...state, - defaultMeterReadingFrequency: action.defaultMeterReadingFrequency, - submitted: false - } - case ActionType.UpdateDefaultMeterMinimumValue: - return { - ...state, - defaultMeterMinimumValue: action.defaultMeterMinimumValue, - submitted: false - } - case ActionType.UpdateDefaultMeterMaximumValue: - return { - ...state, - defaultMeterMaximumValue: action.defaultMeterMaximumValue, - submitted: false - } - case ActionType.UpdateDefaultMeterMinimumDate: - return { - ...state, - defaultMeterMinimumDate: action.defaultMeterMinimumDate, - submitted: false - } - case ActionType.UpdateDefaultMeterMaximumDate: - return { - ...state, - defaultMeterMaximumDate: action.defaultMeterMaximumDate, - submitted: false - } - case ActionType.UpdateDefaultMeterReadingGap: - return { - ...state, - defaultMeterReadingGap: action.defaultMeterReadingGap, - submitted: false - } - case ActionType.UpdateDefaultMeterMaximumErrors: - return { - ...state, - defaultMeterMaximumErrors: action.defaultMeterMaximumErrors, - submitted: false - } - case ActionType.UpdateDefaultMeterDisableChecks: - return { - ...state, - defaultMeterDisableChecks: action.defaultMeterDisableChecks, - submitted: false + ...action.payload, + defaultMeterReadingFrequency: durationFormat(action.payload.defaultMeterReadingFrequency) } - default: - return state; + }, + markPreferencesNotSubmitted: state => { + state.submitted = false; + }, + markPreferencesSubmitted: (state, action: PayloadAction) => { + state.defaultMeterReadingFrequency = durationFormat(action.payload); + state.submitted = true; + }, + updateDefaultWarningFileSize: (state, action: PayloadAction) => { + state.defaultWarningFileSize = action.payload; + state.submitted = false; + }, + updateDefaultFileSizeLimit: (state, action: PayloadAction) => { + state.defaultFileSizeLimit = action.payload; + state.submitted = false; + }, + toggleWaitForCikAndDB: state => { + state.isUpdatingCikAndDBViews = !state.isUpdatingCikAndDBViews; + }, + updateDefaultMeterReadingFrequency: (state, action: PayloadAction) => { + state.defaultMeterReadingFrequency = action.payload; + state.submitted = false; + }, + updateDefaultMeterMinimumValue: (state, action: PayloadAction) => { + state.defaultMeterMinimumValue = action.payload; + state.submitted = false; + }, + updateDefaultMeterMaximumValue: (state, action: PayloadAction) => { + state.defaultMeterMaximumValue = action.payload; + state.submitted = false; + }, + updateDefaultMeterMinimumDate: (state, action: PayloadAction) => { + state.defaultMeterMinimumDate = action.payload; + state.submitted = false; + }, + updateDefaultMeterMaximumDate: (state, action: PayloadAction) => { + state.defaultMeterMaximumDate = action.payload; + state.submitted = false; + }, + updateDefaultMeterReadingGap: (state, action: PayloadAction) => { + state.defaultMeterReadingGap = action.payload; + state.submitted = false; + }, + updateDefaultMeterMaximumErrors: (state, action: PayloadAction) => { + state.defaultMeterMaximumErrors = action.payload; + state.submitted = false; + }, + updateDefaultMeterDisableChecks: (state, action: PayloadAction) => { + state.defaultMeterDisableChecks = action.payload; + state.submitted = false; + } } -} +}); \ No newline at end of file diff --git a/src/client/app/reducers/conversions.ts b/src/client/app/reducers/conversions.ts index 2acae3123..08a6a77c5 100644 --- a/src/client/app/reducers/conversions.ts +++ b/src/client/app/reducers/conversions.ts @@ -2,8 +2,9 @@ * 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 { ConversionsAction, ConversionsState } from '../types/redux/conversions'; -import { ActionType } from '../types/redux/actions'; +import { ConversionsState } from '../types/redux/conversions'; +import * as t from '../types/redux/conversions'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; const defaultState: ConversionsState = { hasBeenFetchedOnce: false, @@ -13,84 +14,62 @@ const defaultState: ConversionsState = { conversions: [] }; -export default function conversions(state = defaultState, action: ConversionsAction) { - switch (action.type) { - case ActionType.ConfirmConversionsFetchedOnce: - return { - ...state, - hasBeenFetchedOnce: true - }; - case ActionType.RequestConversionsDetails: - return { - ...state, - isFetching: true - }; - case ActionType.ReceiveConversionsDetails: - return { - ...state, - isFetching: false, - conversions: action.data - }; - case ActionType.ChangeDisplayedConversions: - return { - ...state, - selectedConversions: action.selectedConversions - }; - case ActionType.SubmitEditedConversion: { - const submitting = state.submitting; - submitting.push(action.conversionData); - return { - ...state, - submitting: [...submitting] - }; - } - case ActionType.ConfirmEditedConversion: { + +export const conversionsSlice = createSlice({ + name: 'conversions', + initialState: defaultState, + reducers: { + conversionsFetchedOnce: state => { + state.hasBeenFetchedOnce = true; + }, + requestConversionsDetails: state => { + state.isFetching = true; + }, + receiveConversionsDetails: (state, action: PayloadAction) => { + state.isFetching = false; + state.conversions = action.payload; + }, + changeDisplayedConversions: (state, action: PayloadAction) => { + state.selectedConversions = action.payload; + }, + submitEditedConversion: (state, action: PayloadAction) => { + state.submitting.push(action.payload); + }, + confirmEditedConversion: (state, action: PayloadAction) => { // Overwrite the conversion data at the edited conversion's index with the edited conversion's conversion data // The passed in id should be correct as it is inherited from the pre-edited conversion // See EditConversionModalComponent line 134 for details (starts with if(conversionHasChanges)) const conversions = state.conversions; - // Search the array of ConversionData in conversions for an object with source/destination ids matching that of the action payload - const conversionDataIndex = conversions.findIndex(conversionData => (( - conversionData.sourceId === action.editedConversion.sourceId) && - conversionData.destinationId === action.editedConversion.destinationId)); - // Overwrite ConversionData at index with edited ConversionData - conversions[conversionDataIndex] = action.editedConversion; - return { - ...state, - // React expects us to return an immutable object in order to invoke a rerender, so we must use spread notation here - conversions: [...conversions] - }; - } - case ActionType.DeleteSubmittedConversion: { + const conversionDataIndex = conversions.findIndex(conversionData => ( + conversionData.sourceId === action.payload.sourceId + && + conversionData.destinationId === action.payload.destinationId + )); + conversions[conversionDataIndex] = action.payload; + }, + deleteSubmittedConversion: (state, action: PayloadAction) => { // Remove the current submitting conversion from the submitting state const submitting = state.submitting; // Search the array of ConversionData in submitting for an object with source/destination ids matching that of the action payload - const conversionDataIndex = submitting.findIndex(conversionData => (( - conversionData.sourceId === action.conversionData.sourceId) && - conversionData.destinationId === action.conversionData.destinationId)); + const conversionDataIndex = submitting.findIndex(conversionData => ( + conversionData.sourceId === action.payload.sourceId + && + conversionData.destinationId === action.payload.destinationId + )); // Remove the object from the submitting array - submitting.splice(conversionDataIndex); - return { - ...state, - submitting: [...submitting] - }; - } - case ActionType.DeleteConversion: { + submitting.splice(conversionDataIndex, 1); + }, + deleteConversion: (state, action: PayloadAction) => { // Retrieve conversions state const conversions = state.conversions; // Search the array of ConversionData in conversions for an object with source/destination ids matching that of the action payload - const conversionDataIndex = conversions.findIndex(conversionData => (( - conversionData.sourceId === action.conversionData.sourceId) && - conversionData.destinationId === action.conversionData.destinationId)); + const conversionDataIndex = conversions.findIndex(conversionData => ( + conversionData.sourceId === action.payload.sourceId + && + conversionData.destinationId === action.payload.destinationId + )); // Remove the ConversionData from the conversions array - conversions.splice(conversionDataIndex); - // Return a new array of ConversionData without the deleted conversion - return { - ...state, - conversions: [...conversions] - } + conversions.splice(conversionDataIndex, 1); } - default: - return state; } -} +}); \ No newline at end of file diff --git a/src/client/app/reducers/groups.ts b/src/client/app/reducers/groups.ts index c1cd11bbe..19d9a5f94 100644 --- a/src/client/app/reducers/groups.ts +++ b/src/client/app/reducers/groups.ts @@ -3,8 +3,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as _ from 'lodash'; -import { GroupsAction, GroupsState, DisplayMode } from '../types/redux/groups'; -import { ActionType } from '../types/redux/actions'; +import { GroupsState, DisplayMode } from '../types/redux/groups'; +import * as t from '../types/redux/groups'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; const defaultState: GroupsState = { hasBeenFetchedOnce: false, @@ -21,39 +22,23 @@ const defaultState: GroupsState = { displayMode: DisplayMode.View }; -export default function groups(state = defaultState, action: GroupsAction) { - switch (action.type) { - // Records if group details have been fetched at least once - case ActionType.ConfirmGroupsFetchedOnce: { - return { - ...state, - hasBeenFetchedOnce: true - }; - } - // Records if all group meter/group children have been fetched at least once. - // Normally just once but can reset to get it to fetch again. - case ActionType.ConfirmAllGroupsChildrenFetchedOnce: { - return { - ...state, - hasChildrenBeenFetchedOnce: true - }; - } - // The following are reducers related to viewing and fetching groups data - case ActionType.RequestGroupsDetails: - return { - ...state, - isFetching: true - }; - case ActionType.ReceiveGroupsDetails: { - /* - add new fields to each group object: - isFetching flag for each group - arrays to store the IDs of child groups and Meters. We get all other data from other parts of state. - - NOTE: if you get an error here saying `action.data.map` is not a function, please comment on - this issue: https://github.com/OpenEnergyDashboard/OED/issues/86 - */ - const newGroups = action.data.map(group => ({ +export const groupsSlice = createSlice({ + name: 'groups', + initialState: defaultState, + reducers: { + confirmGroupsFetchedOnce: state => { + state.hasBeenFetchedOnce = true; + }, + confirmAllGroupsChildrenFetchedOnce: state => { + // Records if all group meter/group children have been fetched at least once. + // Normally just once but can reset to get it to fetch again. + state.hasChildrenBeenFetchedOnce = true; + }, + requestGroupsDetails: state => { + state.isFetching = true; + }, + receiveGroupsDetails: (state, action: PayloadAction) => { + const newGroups = action.payload.map(group => ({ ...group, isFetching: false, // Sometimes OED fetches both the details and the child meters/groups as separate actions. Since the order they will happen is @@ -62,110 +47,201 @@ export default function groups(state = defaultState, action: GroupsAction) { childGroups: (state.byGroupID[group.id] && state.byGroupID[group.id].childGroups) ? state.byGroupID[group.id].childGroups : [], childMeters: (state.byGroupID[group.id] && state.byGroupID[group.id].childMeters) ? state.byGroupID[group.id].childMeters : [], selectedGroups: [], - selectedMeters: [] + selectedMeters: [], + deepMeters: [] })); // newGroups is an array: this converts it into a nested object where the key to each group is its ID. // Without this, byGroupID will not be keyed by group ID. - const newGroupsByID = _.keyBy(newGroups, 'id'); - // Note that there is an `isFetching` for groups as a whole AND one for each group. - return { - ...state, - isFetching: false, - byGroupID: newGroupsByID - }; - } - - case ActionType.RequestGroupChildren: { + state.isFetching = false; + // TODO FIX TYPES HERE Weird interaction here + state.byGroupID = _.keyBy(newGroups, 'id'); + }, + requestGroupChildren: (state, action: PayloadAction) => { // Make no changes except setting isFetching = true for the group whose children we are fetching. - return { - ...state, - byGroupID: { - ...state.byGroupID, - [action.groupID]: { - ...state.byGroupID[action.groupID], - isFetching: true - } - } - - }; - } - - case ActionType.ReceiveGroupChildren: { - // Set isFetching = false for the group, and set the group's children to the arrays in the response. - return { - ...state, - byGroupID: { - ...state.byGroupID, - [action.groupID]: { - ...state.byGroupID[action.groupID], - isFetching: false, - childGroups: action.data.groups, - childMeters: action.data.meters, - deepMeters: action.data.deepMeters - } - } - }; - } - - // When start fetching all groups meters/groups children. - case ActionType.RequestAllGroupsChildren: { - // Note that fetching - return { - ...state, - isFetchingAllChildren: true, - // When the group children are forced to be re-fetched on creating a new group, we need to indicate - // here that the children are not yet gotten. This causes the group detail page to redraw when this - // is finished so the new group has the latest info. - hasChildrenBeenFetchedOnce: false - } - } - - // When receive all groups meters/groups children. - case ActionType.ReceiveAllGroupsChildren: { - // Set up temporary state so only change/return once. - const newState: GroupsState = { - ...state, - byGroupID: { - ...state.byGroupID - } - } + state.byGroupID[action.payload].isFetching = true; + }, + receiveGroupChildren: (state, action: PayloadAction<{ groupID: number, data: { meters: number[], groups: number[], deepMeters: number[] } }>) => { + state.byGroupID[action.payload.groupID].isFetching = false; + state.byGroupID[action.payload.groupID].childGroups = action.payload.data.groups; + state.byGroupID[action.payload.groupID].childMeters = action.payload.data.meters; + state.byGroupID[action.payload.groupID].deepMeters = action.payload.data.deepMeters; + }, + requestAllGroupsChildren: state => { + state.isFetchingAllChildren = true; + // When the group children are forced to be re-fetched on creating a new group, we need to indicate + // here that the children are not yet gotten. This causes the group detail page to redraw when this + // is finished so the new group has the latest info. + state.hasChildrenBeenFetchedOnce = false; + }, + receiveAllGroupsChildren: (state, action: PayloadAction) => { // For each group that received data, set the children meters and groups. - for (const groupInfo of action.data) { + for (const groupInfo of action.payload) { // Group id of the current item const groupId = groupInfo.groupId; // Reset the newState for this group to have child meters/groups. - newState.byGroupID[groupId].childMeters = groupInfo.childMeters; - newState.byGroupID[groupId].childGroups = groupInfo.childGroups; + state.byGroupID[groupId].childMeters = groupInfo.childMeters; + state.byGroupID[groupId].childGroups = groupInfo.childGroups; } // Note that not fetching children - newState.isFetchingAllChildren = false - // The updated state. - return newState; - } - - case ActionType.ChangeDisplayedGroups: { - return { - ...state, - selectedGroups: action.groupIDs - }; - } - - case ActionType.ConfirmEditedGroup: { + state.isFetchingAllChildren = false + }, + changeDisplayedGroups: (state, action: PayloadAction) => { + state.selectedGroups = action.payload; + }, confirmEditedGroup: (state, action: PayloadAction) => { // Return new state object with updated edited group info. - return { - ...state, - byGroupID: { - ...state.byGroupID, - [action.editedGroup.id]: { - // There is state that is in each group that is not part of the edit information state. - ...state.byGroupID[action.editedGroup.id], - ...action.editedGroup - } - } + state.byGroupID[action.payload.id] = { + // There is state that is in each group that is not part of the edit information state. + ...state.byGroupID[action.payload.id], + ...action.payload }; } - - default: - return state; } -} +}); +// export default function groups(state = defaultState, action: GroupsAction) { +// switch (action.type) { +// // Records if group details have been fetched at least once +// case ActionType.groupsSlice.actions.confirmGroupsFetchedOnce: { +// return { +// ...state, +// hasBeenFetchedOnce: true +// }; +// } +// // Records if all group meter/group children have been fetched at least once. +// // Normally just once but can reset to get it to fetch again. +// case ActionType.groupsSlice.actions.confirmAllGroupsChildrenFetchedOnce: { +// return { +// ...state, +// hasChildrenBeenFetchedOnce: true +// }; +// } +// // The following are reducers related to viewing and fetching groups data +// case ActionType.groupsSlice.actions.requestGroupsDetails: +// return { +// ...state, +// isFetching: true +// }; +// case ActionType.groupsSlice.actions.receiveGroupsDetails: { +// /* +// add new fields to each group object: +// isFetching flag for each group +// arrays to store the IDs of child groups and Meters. We get all other data from other parts of state. + +// NOTE: if you get an error here saying `action.data.map` is not a function, please comment on +// this issue: https://github.com/OpenEnergyDashboard/OED/issues/86 +// */ +// const newGroups = action.data.map(group => ({ +// ...group, +// isFetching: false, +// // Sometimes OED fetches both the details and the child meters/groups as separate actions. Since the order they will happen is +// // uncertain, we need to preserve the child meters/groups if they exist. If not, put empty so no issues when accessing in other +// // places. Note this may be the wrong values but they should refresh quickly once all actions are done. +// childGroups: (state.byGroupID[group.id] && state.byGroupID[group.id].childGroups) ? state.byGroupID[group.id].childGroups : [], +// childMeters: (state.byGroupID[group.id] && state.byGroupID[group.id].childMeters) ? state.byGroupID[group.id].childMeters : [], +// selectedGroups: [], +// selectedMeters: [] +// })); +// // newGroups is an array: this converts it into a nested object where the key to each group is its ID. +// // Without this, byGroupID will not be keyed by group ID. +// const newGroupsByID = _.keyBy(newGroups, 'id'); +// // Note that there is an `isFetching` for groups as a whole AND one for each group. +// return { +// ...state, +// isFetching: false, +// byGroupID: newGroupsByID +// }; +// } + +// case ActionType.groupsSlice.actions.requestGroupChildren: { +// // Make no changes except setting isFetching = true for the group whose children we are fetching. +// return { +// ...state, +// byGroupID: { +// ...state.byGroupID, +// [action.groupID]: { +// ...state.byGroupID[action.groupID], +// isFetching: true +// } +// } + +// }; +// } + +// case ActionType.groupsSlice.actions.receiveGroupChildren: { +// // Set isFetching = false for the group, and set the group's children to the arrays in the response. +// return { +// ...state, +// byGroupID: { +// ...state.byGroupID, +// [action.groupID]: { +// ...state.byGroupID[action.groupID], +// isFetching: false, +// childGroups: action.data.groups, +// childMeters: action.data.meters, +// deepMeters: action.data.deepMeters +// } +// } +// }; +// } + +// // When start fetching all groups meters/groups children. +// case ActionType.groupsSlice.actions.requestAllGroupsChildren: { +// // Note that fetching +// return { +// ...state, +// isFetchingAllChildren: true, +// // When the group children are forced to be re-fetched on creating a new group, we need to indicate +// // here that the children are not yet gotten. This causes the group detail page to redraw when this +// // is finished so the new group has the latest info. +// hasChildrenBeenFetchedOnce: false +// } +// } + +// // When receive all groups meters/groups children. +// case ActionType.groupsSlice.actions.receiveAllGroupsChildren: { +// // Set up temporary state so only change/return once. +// const newState: GroupsState = { +// ...state, +// byGroupID: { +// ...state.byGroupID +// } +// } +// // For each group that received data, set the children meters and groups. +// for (const groupInfo of action.data) { +// // Group id of the current item +// const groupId = groupInfo.groupId; +// // Reset the newState for this group to have child meters/groups. +// newState.byGroupID[groupId].childMeters = groupInfo.childMeters; +// newState.byGroupID[groupId].childGroups = groupInfo.childGroups; +// } +// // Note that not fetching children +// newState.isFetchingAllChildren = false +// // The updated state. +// return newState; +// } + +// case ActionType.ChangeDisplayedGroups: { +// return { +// ...state, +// selectedGroups: action.groupIDs +// }; +// } + +// case ActionType.ConfirmEditedGroup: { +// // Return new state object with updated edited group info. +// return { +// ...state, +// byGroupID: { +// ...state.byGroupID, +// [action.editedGroup.id]: { +// // There is state that is in each group that is not part of the edit information state. +// ...state.byGroupID[action.editedGroup.id], +// ...action.editedGroup +// } +// } +// }; +// } + +// default: +// return state; +// } +// } \ No newline at end of file diff --git a/src/client/app/reducers/index.ts b/src/client/app/reducers/index.ts index 1c790715c..8d83aa603 100644 --- a/src/client/app/reducers/index.ts +++ b/src/client/app/reducers/index.ts @@ -7,36 +7,35 @@ import { metersSlice } from './meters'; import lineReadings from './lineReadings'; import barReadings from './barReadings'; import compareReadings from './compareReadings'; -import groups from './groups'; +import { groupsSlice } from './groups'; import maps from './maps'; -import admin from './admin'; +import { adminSlice } from './admin'; import { versionSlice } from './version'; import { currentUserSlice } from './currentUser'; import { unsavedWarningSlice } from './unsavedWarning'; -import units from './units'; -import conversions from './conversions'; +import { unitsSlice } from './units'; +import { conversionsSlice } from './conversions'; import { optionsSlice } from './options'; import { baseApi } from '../redux/api/baseApi'; import { graphSlice } from './graph'; -export default combineReducers({ +export const rootReducer = combineReducers({ meters: metersSlice.reducer, readings: combineReducers({ line: lineReadings, bar: barReadings, compare: compareReadings }), - // graph, graph: graphSlice.reducer, maps, - groups, - admin, + groups: groupsSlice.reducer, + admin: adminSlice.reducer, version: versionSlice.reducer, currentUser: currentUserSlice.reducer, unsavedWarning: unsavedWarningSlice.reducer, - units, - conversions, + units: unitsSlice.reducer, + conversions: conversionsSlice.reducer, options: optionsSlice.reducer, // RTK Query's Derived Reducers [baseApi.reducerPath]: baseApi.reducer diff --git a/src/client/app/reducers/units.ts b/src/client/app/reducers/units.ts index 0ae4605d6..7d8e13f7b 100644 --- a/src/client/app/reducers/units.ts +++ b/src/client/app/reducers/units.ts @@ -2,9 +2,9 @@ * 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 _ from 'lodash'; -import { UnitsAction, UnitsState } from '../types/redux/units'; -import { ActionType } from '../types/redux/actions'; - +import { UnitsState } from '../types/redux/units'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import * as t from '../types/redux/units'; const defaultState: UnitsState = { hasBeenFetchedOnce: false, isFetching: false, @@ -13,64 +13,31 @@ const defaultState: UnitsState = { units: {} }; -export default function units(state = defaultState, action: UnitsAction) { - switch (action.type) { - case ActionType.ConfirmUnitsFetchedOnce: { - return { - ...state, - hasBeenFetchedOnce: true - }; - } - case ActionType.RequestUnitsDetails: { - return { - ...state, - isFetching: true - }; - } - case ActionType.ReceiveUnitsDetails: { - return { - ...state, - isFetching: false, - units: _.keyBy(action.data, unit => unit.id) - }; - } - case ActionType.ChangeDisplayedUnits: { - return { - ...state, - selectedUnits: action.selectedUnits - }; - } - case ActionType.SubmitEditedUnit: { - const submitting = state.submitting; - submitting.push(action.unitId); - return { - ...state, - submitting: [...submitting] - }; - } - case ActionType.ConfirmEditedUnit: { - // Return new state object with updated edited meter info. - return { - ...state, - units: { - ...state.units, - [action.editedUnit.id]: { - ...action.editedUnit - } - } - }; - } - case ActionType.DeleteSubmittedUnit: { - // Remove the current submitting unit from the submitting state - const submitting = state.submitting; - submitting.splice(submitting.indexOf(action.unitId)); - return { - ...state, - submitting: [...submitting] - }; - } - default: { - return state; +export const unitsSlice = createSlice({ + name: 'units', + initialState: defaultState, + reducers: { + confirmUnitsFetchedOnce: state => { + state.hasBeenFetchedOnce = true; + }, + requestUnitsDetails: state => { + state.isFetching = true; + }, + receiveUnitsDetails: (state, action: PayloadAction) => { + state.isFetching = false; + state.units = _.keyBy(action.payload, unit => unit.id); + }, + changeDisplayedUnits: (state, action: PayloadAction) => { + state.selectedUnits = action.payload; + }, + submitEditedUnit: (state, action: PayloadAction) => { + state.submitting.push(action.payload); + }, + confirmEditedUnit: (state, action: PayloadAction) => { + state.units[action.payload.id] = action.payload; + }, + confirmUnitEdits: (state, action: PayloadAction) => { + state.submitting.splice(state.submitting.indexOf(action.payload), 1); } } -} +}); \ No newline at end of file diff --git a/src/client/app/store.ts b/src/client/app/store.ts index fbd27b721..61b9c3af5 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -3,14 +3,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { configureStore } from '@reduxjs/toolkit' -import reducers from './reducers'; +import { rootReducer } from './reducers'; import { baseApi } from './redux/api/baseApi'; export const store = configureStore({ - reducer: reducers, + reducer: rootReducer, middleware: getDefaultMiddleware => getDefaultMiddleware({ - immutableCheck: false, + // immutableCheck: false, serializableCheck: false }).concat(baseApi.middleware) }); diff --git a/src/client/app/types/redux/admin.ts b/src/client/app/types/redux/admin.ts index 777432fd1..1910120a3 100644 --- a/src/client/app/types/redux/admin.ts +++ b/src/client/app/types/redux/admin.ts @@ -2,147 +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 { PreferenceRequestItem } from '../items'; import { ChartTypes } from './graph'; import { LanguageTypes } from './i18n'; -import { ActionType } from './actions'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; -export type AdminAction = - | UpdateImportMeterAction - | UpdateDisplayTitleAction - | UpdateDefaultChartToRenderAction - | UpdateDefaultLanguageAction - | ToggleDefaultBarStackingAction - | ToggleDefaultAreaNormalizationAction - | RequestPreferencesAction - | ReceivePreferencesAction - | MarkPreferencesNotSubmittedAction - | UpdateDefaultTimeZone - | UpdateDefaultWarningFileSize - | UpdateDefaultFileSizeLimit - | MarkPreferencesSubmittedAction - | ToggleWaitForCikAndDB - | UpdateDefaultAreaUnitAction - | UpdateDefaultMeterReadingFrequencyAction - | UpdateDefaultMeterMinimumValueAction - | UpdateDefaultMeterMaximumValueAction - | UpdateDefaultMeterMinimumDateAction - | UpdateDefaultMeterMaximumDateAction - | UpdateDefaultMeterReadingGapAction - | UpdateDefaultMeterMaximumErrorsAction - | UpdateDefaultMeterDisableChecksAction; - -export interface UpdateImportMeterAction { - type: ActionType.UpdateImportMeter; - meterID: number; -} - -export interface UpdateDisplayTitleAction { - type: ActionType.UpdateDisplayTitle; - displayTitle: string; -} - -export interface UpdateDefaultChartToRenderAction { - type: ActionType.UpdateDefaultChartToRender; - defaultChartToRender: ChartTypes; -} - -export interface ToggleDefaultBarStackingAction { - type: ActionType.ToggleDefaultBarStacking; -} - -export interface ToggleDefaultAreaNormalizationAction { - type: ActionType.ToggleDefaultAreaNormalization; -} - -export interface UpdateDefaultAreaUnitAction { - type: ActionType.UpdateDefaultAreaUnit; - defaultAreaUnit: AreaUnitType; -} - -export interface UpdateDefaultTimeZone { - type: ActionType.UpdateDefaultTimeZone; - timeZone: string; -} - -export interface UpdateDefaultLanguageAction { - type: ActionType.UpdateDefaultLanguage; - defaultLanguage: LanguageTypes; -} - -export interface RequestPreferencesAction { - type: ActionType.RequestPreferences; -} - -export interface ReceivePreferencesAction { - type: ActionType.ReceivePreferences; - data: PreferenceRequestItem; -} - -export interface MarkPreferencesNotSubmittedAction { - type: ActionType.MarkPreferencesNotSubmitted; -} - -export interface MarkPreferencesSubmittedAction { - type: ActionType.MarkPreferencesSubmitted; - defaultMeterReadingFrequency: string; -} - -export interface UpdateDefaultWarningFileSize { - type: ActionType.UpdateDefaultWarningFileSize; - defaultWarningFileSize: number; -} - -export interface UpdateDefaultFileSizeLimit { - type: ActionType.UpdateDefaultFileSizeLimit; - defaultFileSizeLimit: number; -} - -export interface ToggleWaitForCikAndDB { - type: ActionType.ToggleWaitForCikAndDB; -} - -export interface UpdateDefaultMeterReadingFrequencyAction { - type: ActionType.UpdateDefaultMeterReadingFrequency; - defaultMeterReadingFrequency: string; -} - -export interface UpdateDefaultMeterMinimumValueAction { - type: ActionType.UpdateDefaultMeterMinimumValue; - defaultMeterMinimumValue: number; -} - -export interface UpdateDefaultMeterMaximumValueAction { - type: ActionType.UpdateDefaultMeterMaximumValue; - defaultMeterMaximumValue: number; -} - - -export interface UpdateDefaultMeterMinimumDateAction { - type: ActionType.UpdateDefaultMeterMinimumDate; - defaultMeterMinimumDate: string; -} - -export interface UpdateDefaultMeterMaximumDateAction { - type: ActionType.UpdateDefaultMeterMaximumDate; - defaultMeterMaximumDate: string; -} - -export interface UpdateDefaultMeterReadingGapAction { - type: ActionType.UpdateDefaultMeterReadingGap; - defaultMeterReadingGap: number; -} - -export interface UpdateDefaultMeterMaximumErrorsAction { - type: ActionType.UpdateDefaultMeterMaximumErrors; - defaultMeterMaximumErrors: number; -} - -export interface UpdateDefaultMeterDisableChecksAction { - type: ActionType.UpdateDefaultMeterDisableChecks; - defaultMeterDisableChecks: boolean; -} export interface AdminState { selectedMeter: number | null; diff --git a/src/client/app/types/redux/conversions.ts b/src/client/app/types/redux/conversions.ts index 23b44678a..fa7da9d04 100644 --- a/src/client/app/types/redux/conversions.ts +++ b/src/client/app/types/redux/conversions.ts @@ -2,55 +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 { ActionType } from './actions'; - -export interface RequestConversionsDetailsAction { - type: ActionType.RequestConversionsDetails; -} - -export interface ReceiveConversionsDetailsAction { - type: ActionType.ReceiveConversionsDetails; - data: ConversionData[]; -} - -export interface ChangeDisplayedConversionsAction { - type: ActionType.ChangeDisplayedConversions; - selectedConversions: number[]; -} - -export interface ConfirmEditedConversionAction { - type: ActionType.ConfirmEditedConversion; - editedConversion: ConversionData; -} - -export interface DeleteSubmittedConversionAction { - type: ActionType.DeleteSubmittedConversion; - conversionData: ConversionData; -} - -export interface SubmitEditedConversionAction { - type: ActionType.SubmitEditedConversion; - conversionData: ConversionData; -} - -export interface ConfirmConversionsFetchedOnceAction { - type: ActionType.ConfirmConversionsFetchedOnce; -} - -export interface DeleteConversionAction { - type: ActionType.DeleteConversion; - conversionData: ConversionData; -} - -export type ConversionsAction = RequestConversionsDetailsAction -| ReceiveConversionsDetailsAction -| ChangeDisplayedConversionsAction -| ConfirmEditedConversionAction -| DeleteSubmittedConversionAction -| SubmitEditedConversionAction -| ConfirmConversionsFetchedOnceAction -| DeleteConversionAction; - export interface ConversionData { sourceId: number; destinationId: number; diff --git a/src/client/app/types/redux/groups.ts b/src/client/app/types/redux/groups.ts index d5a65dc89..f07ceb5ea 100644 --- a/src/client/app/types/redux/groups.ts +++ b/src/client/app/types/redux/groups.ts @@ -2,72 +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 { ActionType } from './actions'; import { GPSPoint } from 'utils/calibration'; import { AreaUnitType } from 'utils/getAreaUnitConversion'; export enum DisplayMode { View = 'view', Edit = 'edit', Create = 'create' } - -export type GroupsAction = - | RequestGroupsDetailsAction - | ReceiveGroupsDetailsAction - | RequestGroupChildrenAction - | ReceiveGroupChildrenAction - | RequestAllGroupsChildrenAction - | ReceiveAllGroupsChildrenAction - | ChangeDisplayedGroupsAction - | ConfirmEditedGroupAction - | ConfirmGroupsFetchedOnceAction - | ConfirmAllGroupsChildrenFetchedOnceAction - ; - -export interface RequestGroupsDetailsAction { - type: ActionType.RequestGroupsDetails; -} - -export interface ReceiveGroupsDetailsAction { - type: ActionType.ReceiveGroupsDetails; - data: GroupDetailsData[]; -} - -export interface RequestGroupChildrenAction { - type: ActionType.RequestGroupChildren; - groupID: number; -} - -export interface ReceiveGroupChildrenAction { - type: ActionType.ReceiveGroupChildren; - groupID: number; - data: { meters: number[], groups: number[], deepMeters: number[] }; -} - -export interface RequestAllGroupsChildrenAction { - type: ActionType.RequestAllGroupsChildren; -} - -export interface ReceiveAllGroupsChildrenAction { - type: ActionType.ReceiveAllGroupsChildren; - data: GroupChildren[]; -} - -export interface ConfirmEditedGroupAction { - type: ActionType.ConfirmEditedGroup; - editedGroup: GroupEditData; -} - -export interface ChangeDisplayedGroupsAction { - type: ActionType.ChangeDisplayedGroups; - groupIDs: number[]; -} - -export interface ConfirmGroupsFetchedOnceAction { - type: ActionType.ConfirmGroupsFetchedOnce; -} - -export interface ConfirmAllGroupsChildrenFetchedOnceAction { - type: ActionType.ConfirmAllGroupsChildrenFetchedOnce -} - export interface GroupMetadata { isFetching: boolean; selectedGroups: number[]; diff --git a/src/client/app/types/redux/units.ts b/src/client/app/types/redux/units.ts index 0668b94e1..771c4ba44 100644 --- a/src/client/app/types/redux/units.ts +++ b/src/client/app/types/redux/units.ts @@ -2,49 +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 { ActionType } from './actions'; - -export interface RequestUnitsDetailsAction { - type: ActionType.RequestUnitsDetails; -} - -export interface ReceiveUnitsDetailsAction { - type: ActionType.ReceiveUnitsDetails; - data: UnitData[]; -} - -export interface ChangeDisplayedUnitsAction { - type: ActionType.ChangeDisplayedUnits; - selectedUnits: number[]; -} - -export interface ConfirmEditedUnitAction { - type: ActionType.ConfirmEditedUnit; - editedUnit: UnitData; -} - -export interface DeleteSubmittedUnitAction { - type: ActionType.DeleteSubmittedUnit; - unitId: number; -} - -export interface SubmitEditedUnitAction { - type: ActionType.SubmitEditedUnit; - unitId: number; -} - -export interface ConfirmUnitsFetchedOnceAction { - type: ActionType.ConfirmUnitsFetchedOnce; -} - -export type UnitsAction = RequestUnitsDetailsAction -| ReceiveUnitsDetailsAction -| ChangeDisplayedUnitsAction -| ConfirmEditedUnitAction -| DeleteSubmittedUnitAction -| SubmitEditedUnitAction -| ConfirmUnitsFetchedOnceAction; - export enum UnitType { unit = 'unit', meter = 'meter', diff --git a/src/client/app/utils/api/MetersApi.ts b/src/client/app/utils/api/MetersApi.ts index bea0b10c9..537f18960 100644 --- a/src/client/app/utils/api/MetersApi.ts +++ b/src/client/app/utils/api/MetersApi.ts @@ -8,7 +8,7 @@ import ApiBackend from './ApiBackend'; import { NamedIDItem } from '../../types/items'; import { CompareReadings, RawReadings } from '../../types/readings'; import { TimeInterval } from '../../../../common/TimeInterval'; -import { MeterData, MeterEditData } from '../../types/redux/meters'; +import { MeterData } from '../../types/redux/meters'; import * as moment from 'moment'; export default class MetersApi { @@ -37,14 +37,14 @@ export default class MetersApi { ); } - public async edit(meter: MeterData): Promise { - return await this.backend.doPostRequest( + public async edit(meter: MeterData): Promise { + return await this.backend.doPostRequest( '/api/meters/edit', meter ); } - public async addMeter(meter: MeterEditData): Promise { - return await this.backend.doPostRequest('/api/meters/addMeter', meter); + public async addMeter(meter: MeterData): Promise { + return await this.backend.doPostRequest('/api/meters/addMeter', meter); } public async getMetersDetails(): Promise { From 74895245a57145a28006dd666a409405166af08c Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Tue, 19 Sep 2023 20:02:59 +0000 Subject: [PATCH 009/131] wip --- .../components/ChartDataSelectComponent.tsx | 54 +++--- .../ConfirmActionModalComponent.tsx | 1 + .../app/components/DashboardComponent.tsx | 178 +++++++----------- .../app/components/DateRangeComponent.tsx | 2 +- src/client/app/components/HomeComponent.tsx | 9 +- .../components/InitializationComponent.tsx | 9 +- .../MeterAndGroupSelectComponent.tsx | 17 ++ src/client/app/components/ThreeDComponent.tsx | 4 +- src/client/app/index.tsx | 2 +- src/client/app/reducers/graph.ts | 13 +- src/client/app/reducers/groups.ts | 177 +++-------------- src/client/app/reducers/meters.ts | 9 + src/client/app/redux/api/baseApi.ts | 4 +- src/client/app/redux/api/groupsApi.ts | 6 +- .../app/redux/selectors/threeDSelectors.ts | 13 +- src/client/app/redux/selectors/uiSelectors.ts | 140 ++++++++++++++ src/client/app/store.ts | 2 +- src/client/app/types/redux/groups.ts | 7 +- ...atability.ts => dateRangeCompatibility.ts} | 0 19 files changed, 329 insertions(+), 318 deletions(-) create mode 100644 src/client/app/components/MeterAndGroupSelectComponent.tsx create mode 100644 src/client/app/redux/selectors/uiSelectors.ts rename src/client/app/utils/{dateRangeCompatability.ts => dateRangeCompatibility.ts} (100%) diff --git a/src/client/app/components/ChartDataSelectComponent.tsx b/src/client/app/components/ChartDataSelectComponent.tsx index cc84e0080..b81b40772 100644 --- a/src/client/app/components/ChartDataSelectComponent.tsx +++ b/src/client/app/components/ChartDataSelectComponent.tsx @@ -25,29 +25,21 @@ import { MetersState } from 'types/redux/meters'; import { GroupsState } from 'types/redux/groups'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; import { graphSlice } from '../reducers/graph'; - +// import { selectMetersAndGroupsCompatibility, selectVisibleMetersAndGroups } from '../redux/selectors/uiSelectors' +// import { useAppSelector } from '../redux/hooks'; /** * A component which allows the user to select which data should be displayed on the chart. * @returns Chart data select element */ export default function ChartDataSelectComponent() { - const divBottomPadding: React.CSSProperties = { - paddingBottom: '15px' - }; - const labelStyle: React.CSSProperties = { - fontWeight: 'bold', - margin: 0 - }; - const messages = defineMessages({ - selectGroups: { id: 'select.groups' }, - selectMeters: { id: 'select.meters' }, - selectUnit: { id: 'select.unit' }, - helpSelectGroups: { id: 'help.home.select.groups' }, - helpSelectMeters: { id: 'help.home.select.meters' } - }); - + // Must specify type if using ThunkDispatch + const dispatch: Dispatch = useDispatch(); const intl = useIntl(); - + // TESTING SELECTORS + // const visibleMetersAndGroups = useAppSelector(state => selectVisibleMetersAndGroups(state)) + // const meterNGroupCompat = useAppSelector(state => selectMetersAndGroupsCompatibility(state)) + // console.log('visibleMetersAndGroups', visibleMetersAndGroups) + // console.log('meterNGroupCompat', meterNGroupCompat) const dataProps = useSelector((state: State) => { const allMeters = state.meters.byMeterID; const allGroups = state.groups.byGroupID; @@ -211,7 +203,9 @@ export default function ChartDataSelectComponent() { // The selectedUnit becomes the unit of the group selected. Note is should always be set (not -99) since // those groups should not have been visible. The only exception is if there are no selected groups but // then this loop does not run. The loop is assumed to only run once in this case. - // state.graph.selectedUnit = state.groups.byGroupID[groupID].defaultGraphicUnit; + // dispatch(changeSelectedUnit(state.groups.byGroupID[groupID].defaultGraphicUnit)); + + } compatibleSelectedGroups.push({ // For groups we display the name since no identifier. @@ -262,11 +256,6 @@ export default function ChartDataSelectComponent() { ); } - // // if no area unit selected, set the default area as selected. - // if (state.graph.selectedAreaUnit == AreaUnitType.none) { - // state.graph.selectedAreaUnit = state.admin.defaultAreaUnit; - // } - return { // all items, sorted alphabetically and by compatibility sortedMeters, @@ -287,8 +276,6 @@ export default function ChartDataSelectComponent() { } }); - // Must specify type if using ThunkDispatch - const dispatch: Dispatch = useDispatch(); return (
@@ -751,4 +738,19 @@ function syncThreeDState( // reset currently active threeD Meter or group when it is removed and is currently active. dispatch(changeMeterOrGroupInfo(null)); } -} \ No newline at end of file +} + +const divBottomPadding: React.CSSProperties = { + paddingBottom: '15px' +}; +const labelStyle: React.CSSProperties = { + fontWeight: 'bold', + margin: 0 +}; +const messages = defineMessages({ + selectGroups: { id: 'select.groups' }, + selectMeters: { id: 'select.meters' }, + selectUnit: { id: 'select.unit' }, + helpSelectGroups: { id: 'help.home.select.groups' }, + helpSelectMeters: { id: 'help.home.select.meters' } +}); \ No newline at end of file diff --git a/src/client/app/components/ConfirmActionModalComponent.tsx b/src/client/app/components/ConfirmActionModalComponent.tsx index 3753f02eb..94ab41956 100644 --- a/src/client/app/components/ConfirmActionModalComponent.tsx +++ b/src/client/app/components/ConfirmActionModalComponent.tsx @@ -1,6 +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 '../styles/modal.css'; import translate from '../utils/translate'; diff --git a/src/client/app/components/DashboardComponent.tsx b/src/client/app/components/DashboardComponent.tsx index af098ce0f..daa231fee 100644 --- a/src/client/app/components/DashboardComponent.tsx +++ b/src/client/app/components/DashboardComponent.tsx @@ -9,134 +9,90 @@ import BarChartContainer from '../containers/BarChartContainer'; import MultiCompareChartContainer from '../containers/MultiCompareChartContainer'; import MapChartContainer from '../containers/MapChartContainer'; import ThreeDComponent from './ThreeDComponent'; -import SpinnerComponent from './SpinnerComponent'; import { ChartTypes } from '../types/redux/graph'; import * as moment from 'moment'; import { TimeInterval } from '../../../common/TimeInterval'; import { Button } from 'reactstrap'; import { FormattedMessage } from 'react-intl'; import TooltipMarkerComponent from './TooltipMarkerComponent'; - -interface DashboardProps { - chartToRender: ChartTypes; - optionsVisibility: boolean; - lineLoading: false; - barLoading: false; - compareLoading: false; - mapLoading: false; - selectedTimeInterval: TimeInterval; - changeTimeInterval(timeInterval: TimeInterval): Promise; -} - +import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { changeGraphZoomIfNeeded } from '../actions/graph'; +import { Dispatch } from '../types/redux/actions'; /** * React component that controls the dashboard + * @returns the Primary Dashboard Component comprising of Ui Controls, and */ -export default class DashboardComponent extends React.Component { - constructor(props: DashboardProps) { - super(props); - this.handleTimeIntervalChange = this.handleTimeIntervalChange.bind(this); - } +export default function DashboardComponent() { + const dispatch: Dispatch = useAppDispatch(); + const chartToRender = useAppSelector(state => state.graph.chartToRender); + const optionsVisibility = useAppSelector(state => state.graph.optionsVisibility); - public render() { - let ChartToRender: - typeof LineChartContainer | - typeof MultiCompareChartContainer | - typeof BarChartContainer | - typeof MapChartContainer | - typeof ThreeDComponent; + const optionsClassName = optionsVisibility ? 'col-2 d-none d-lg-block' : 'd-none'; + const chartClassName = optionsVisibility ? 'col-12 col-lg-10' : 'col-12'; - let showSpinner = false; - if (this.props.chartToRender === ChartTypes.line) { - if (this.props.lineLoading) { - showSpinner = true; - } - ChartToRender = LineChartContainer; - } else if (this.props.chartToRender === ChartTypes.bar) { - if (this.props.barLoading) { - showSpinner = true; - } - ChartToRender = BarChartContainer; - } else if (this.props.chartToRender === ChartTypes.compare) { - if (this.props.compareLoading) { - showSpinner = true; - } - ChartToRender = MultiCompareChartContainer; - } else if (this.props.chartToRender === ChartTypes.map) { - if (this.props.mapLoading) { - showSpinner = true; - } - ChartToRender = MapChartContainer; - } else if (this.props.chartToRender === ChartTypes.threeD) { - /* To avoid the spinner rendering over UI elements (PillBadges) in the 3d component, - the spinner and logic now lives inside the 3dComponent instead. 'showSpinner' is hardcoded to false here.*/ - showSpinner = false; - ChartToRender = ThreeDComponent; - } else { - throw new Error('unrecognized type of chart'); - } + const buttonMargin: React.CSSProperties = { + marginRight: '10px' + }; - const optionsClassName = this.props.optionsVisibility ? 'col-2 d-none d-lg-block' : 'd-none'; - const chartClassName = this.props.optionsVisibility ? 'col-12 col-lg-10' : 'col-12'; + return ( +
+
+
+ +
+
+ { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + chartToRender === ChartTypes.line && + } + { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + chartToRender === ChartTypes.bar && + } + { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + chartToRender === ChartTypes.compare && + } + { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + chartToRender === ChartTypes.map && + } + { + chartToRender === ChartTypes.threeD && + } - const buttonMargin: React.CSSProperties = { - marginRight: '10px' - }; + {(chartToRender === ChartTypes.line) ? ( + [, - , - - ] - ) : ( - null - )} -
+ > + , + , + + ] + ) : ( + null + )}
- ); - } - - private handleTimeIntervalChange(mode: string) { - if (mode === 'all') { - this.props.changeTimeInterval(TimeInterval.unbounded()); - } else if (mode === 'range') { - const timeInterval = TimeInterval.fromString(getRangeSliderInterval()); - this.props.changeTimeInterval(timeInterval); - } - } +
+ ); } /** diff --git a/src/client/app/components/DateRangeComponent.tsx b/src/client/app/components/DateRangeComponent.tsx index 59e9bcbd7..ae69b2eec 100644 --- a/src/client/app/components/DateRangeComponent.tsx +++ b/src/client/app/components/DateRangeComponent.tsx @@ -9,7 +9,7 @@ import DateRangePicker from '@wojtekmaj/react-daterange-picker'; import { CloseReason, Value } from '@wojtekmaj/react-daterange-picker/dist/cjs/shared/types'; import 'react-calendar/dist/Calendar.css'; import '@wojtekmaj/react-daterange-picker/dist/DateRangePicker.css'; -import { dateRangeToTimeInterval, timeIntervalToDateRange } from '../utils/dateRangeCompatability'; +import { dateRangeToTimeInterval, timeIntervalToDateRange } from '../utils/dateRangeCompatibility'; import TooltipMarkerComponent from './TooltipMarkerComponent'; import translate from '../utils/translate'; import { State } from '../types/redux/state'; diff --git a/src/client/app/components/HomeComponent.tsx b/src/client/app/components/HomeComponent.tsx index 55ab7d931..76cda1055 100644 --- a/src/client/app/components/HomeComponent.tsx +++ b/src/client/app/components/HomeComponent.tsx @@ -3,27 +3,22 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import DashboardContainer from '../containers/DashboardContainer'; import FooterContainer from '../containers/FooterContainer'; import TooltipHelpContainer from '../containers/TooltipHelpContainer'; import HeaderComponent from './HeaderComponent'; -import { metersApi } from '../redux/api/metersApi'; -import { groupsApi } from '../redux/api/groupsApi'; +import DashboardComponent from './DashboardComponent'; /** * Top-level React component that controls the home page * @returns JSX to create the home page */ export default function HomeComponent() { - // /api/unitReadings/threeD/meters/28?timeInterval=2020-05-08T00:00:00Z_2020-07-15T00:00:00Z&graphicUnitId=1&readingInterval=1 - metersApi.endpoints.getMeters.useQuery(); - groupsApi.endpoints.getGroups.useQuery(); return (
- +
); diff --git a/src/client/app/components/InitializationComponent.tsx b/src/client/app/components/InitializationComponent.tsx index f2878392a..317aad5fb 100644 --- a/src/client/app/components/InitializationComponent.tsx +++ b/src/client/app/components/InitializationComponent.tsx @@ -7,7 +7,6 @@ import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { State } from '../types/redux/state'; import { fetchMetersDetails, fetchMetersDetailsIfNeeded } from '../actions/meters'; -import { fetchGroupsDetailsIfNeeded } from '../actions/groups'; import { ConversionArray } from '../types/conversionArray'; import { fetchPreferencesIfNeeded } from '../actions/admin'; import { fetchMapsDetails } from '../actions/map'; @@ -15,7 +14,10 @@ import { fetchUnitsDetailsIfNeeded } from '../actions/units'; import { fetchConversionsDetailsIfNeeded } from '../actions/conversions'; import { Dispatch } from 'types/redux/actions'; import { Slide, ToastContainer } from 'react-toastify'; +import { metersApi } from '../redux/api/metersApi'; +import { groupsApi } from '../redux/api/groupsApi'; import 'react-toastify/dist/ReactToastify.css'; +import { fetchGroupsDetailsIfNeeded } from '../actions/groups'; /** * Initializes OED redux with needed details @@ -24,10 +26,11 @@ import 'react-toastify/dist/ReactToastify.css'; export default function InitializationComponent() { const dispatch: Dispatch = useDispatch(); - + metersApi.endpoints.getMeters.useQuery(); + groupsApi.endpoints.getGroups.useQuery(); // Only run once by making it depend on an empty array. useEffect(() => { - dispatch(fetchMetersDetailsIfNeeded()); + // dispatch(fetchMetersDetailsIfNeeded()); dispatch(fetchGroupsDetailsIfNeeded()); dispatch(fetchPreferencesIfNeeded()); dispatch(fetchMapsDetails()); diff --git a/src/client/app/components/MeterAndGroupSelectComponent.tsx b/src/client/app/components/MeterAndGroupSelectComponent.tsx new file mode 100644 index 000000000..31ffa96ba --- /dev/null +++ b/src/client/app/components/MeterAndGroupSelectComponent.tsx @@ -0,0 +1,17 @@ +/* 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' + +/** + * @returns A React-Select component for UI Options Panel + */ +export default function MeterAndGroupSelectComponent() { + + return ( + <> + Hello, M n G Select! + + ) +} \ No newline at end of file diff --git a/src/client/app/components/ThreeDComponent.tsx b/src/client/app/components/ThreeDComponent.tsx index d84268769..53f415cdd 100644 --- a/src/client/app/components/ThreeDComponent.tsx +++ b/src/client/app/components/ThreeDComponent.tsx @@ -10,11 +10,11 @@ import SpinnerComponent from './SpinnerComponent'; import { State } from '../types/redux/state'; import { useSelector } from 'react-redux'; import { ThreeDReading } from '../types/readings' -import { roundTimeIntervalForFetch } from '../utils/dateRangeCompatability'; +import { roundTimeIntervalForFetch } from '../utils/dateRangeCompatibility'; import { lineUnitLabel } from '../utils/graphics'; import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; import translate from '../utils/translate'; -import { isValidThreeDInterval } from '../utils/dateRangeCompatability'; +import { isValidThreeDInterval } from '../utils/dateRangeCompatibility'; import { GraphState, MeterOrGroup } from '../types/redux/graph'; import { UnitsState } from '../types/redux/units'; import { MetersState } from '../types/redux/meters'; diff --git a/src/client/app/index.tsx b/src/client/app/index.tsx index 2d0438555..60a948f6d 100644 --- a/src/client/app/index.tsx +++ b/src/client/app/index.tsx @@ -20,7 +20,7 @@ const container = document.getElementById('root'); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const root = createRoot(container!); root.render( - + ); diff --git a/src/client/app/reducers/graph.ts b/src/client/app/reducers/graph.ts index 506312e3d..e69366361 100644 --- a/src/client/app/reducers/graph.ts +++ b/src/client/app/reducers/graph.ts @@ -7,8 +7,8 @@ import { TimeInterval } from '../../../common/TimeInterval'; import { GraphState, ChartTypes, ReadingInterval, MeterOrGroup, LineGraphRate } from '../types/redux/graph'; import { calculateCompareTimeInterval, ComparePeriod, SortingOrder } from '../utils/calculateCompare'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; -import { createSlice } from '@reduxjs/toolkit' -import type { PayloadAction } from '@reduxjs/toolkit' +import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { adminSlice } from './admin'; const defaultState: GraphState = { selectedMeters: [], @@ -103,5 +103,14 @@ export const graphSlice = createSlice({ state.threeD.meterOrGroupID = action.payload.meterOrGroupID state.threeD.meterOrGroup = action.payload.meterOrGroup } + }, + extraReducers: builder => { + builder.addCase(adminSlice.actions.receivePreferences, + (state, action) => { + if (state.selectedAreaUnit == AreaUnitType.none) { + state.selectedAreaUnit = action.payload.defaultAreaUnit + + } + }) } }) diff --git a/src/client/app/reducers/groups.ts b/src/client/app/reducers/groups.ts index 19d9a5f94..4f2db1947 100644 --- a/src/client/app/reducers/groups.ts +++ b/src/client/app/reducers/groups.ts @@ -6,6 +6,7 @@ import * as _ from 'lodash'; import { GroupsState, DisplayMode } from '../types/redux/groups'; import * as t from '../types/redux/groups'; import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { groupsApi } from '../redux/api/groupsApi'; const defaultState: GroupsState = { hasBeenFetchedOnce: false, @@ -48,7 +49,7 @@ export const groupsSlice = createSlice({ childMeters: (state.byGroupID[group.id] && state.byGroupID[group.id].childMeters) ? state.byGroupID[group.id].childMeters : [], selectedGroups: [], selectedMeters: [], - deepMeters: [] + deepMeters: state.byGroupID[group.id]?.deepMeters })); // newGroups is an array: this converts it into a nested object where the key to each group is its ID. // Without this, byGroupID will not be keyed by group ID. @@ -96,152 +97,28 @@ export const groupsSlice = createSlice({ }; } } -}); -// export default function groups(state = defaultState, action: GroupsAction) { -// switch (action.type) { -// // Records if group details have been fetched at least once -// case ActionType.groupsSlice.actions.confirmGroupsFetchedOnce: { -// return { -// ...state, -// hasBeenFetchedOnce: true -// }; -// } -// // Records if all group meter/group children have been fetched at least once. -// // Normally just once but can reset to get it to fetch again. -// case ActionType.groupsSlice.actions.confirmAllGroupsChildrenFetchedOnce: { -// return { -// ...state, -// hasChildrenBeenFetchedOnce: true -// }; -// } -// // The following are reducers related to viewing and fetching groups data -// case ActionType.groupsSlice.actions.requestGroupsDetails: -// return { -// ...state, -// isFetching: true -// }; -// case ActionType.groupsSlice.actions.receiveGroupsDetails: { -// /* -// add new fields to each group object: -// isFetching flag for each group -// arrays to store the IDs of child groups and Meters. We get all other data from other parts of state. - -// NOTE: if you get an error here saying `action.data.map` is not a function, please comment on -// this issue: https://github.com/OpenEnergyDashboard/OED/issues/86 -// */ -// const newGroups = action.data.map(group => ({ -// ...group, -// isFetching: false, -// // Sometimes OED fetches both the details and the child meters/groups as separate actions. Since the order they will happen is -// // uncertain, we need to preserve the child meters/groups if they exist. If not, put empty so no issues when accessing in other -// // places. Note this may be the wrong values but they should refresh quickly once all actions are done. -// childGroups: (state.byGroupID[group.id] && state.byGroupID[group.id].childGroups) ? state.byGroupID[group.id].childGroups : [], -// childMeters: (state.byGroupID[group.id] && state.byGroupID[group.id].childMeters) ? state.byGroupID[group.id].childMeters : [], -// selectedGroups: [], -// selectedMeters: [] -// })); -// // newGroups is an array: this converts it into a nested object where the key to each group is its ID. -// // Without this, byGroupID will not be keyed by group ID. -// const newGroupsByID = _.keyBy(newGroups, 'id'); -// // Note that there is an `isFetching` for groups as a whole AND one for each group. -// return { -// ...state, -// isFetching: false, -// byGroupID: newGroupsByID -// }; -// } - -// case ActionType.groupsSlice.actions.requestGroupChildren: { -// // Make no changes except setting isFetching = true for the group whose children we are fetching. -// return { -// ...state, -// byGroupID: { -// ...state.byGroupID, -// [action.groupID]: { -// ...state.byGroupID[action.groupID], -// isFetching: true -// } -// } - -// }; -// } - -// case ActionType.groupsSlice.actions.receiveGroupChildren: { -// // Set isFetching = false for the group, and set the group's children to the arrays in the response. -// return { -// ...state, -// byGroupID: { -// ...state.byGroupID, -// [action.groupID]: { -// ...state.byGroupID[action.groupID], -// isFetching: false, -// childGroups: action.data.groups, -// childMeters: action.data.meters, -// deepMeters: action.data.deepMeters -// } -// } -// }; -// } - -// // When start fetching all groups meters/groups children. -// case ActionType.groupsSlice.actions.requestAllGroupsChildren: { -// // Note that fetching -// return { -// ...state, -// isFetchingAllChildren: true, -// // When the group children are forced to be re-fetched on creating a new group, we need to indicate -// // here that the children are not yet gotten. This causes the group detail page to redraw when this -// // is finished so the new group has the latest info. -// hasChildrenBeenFetchedOnce: false -// } -// } - -// // When receive all groups meters/groups children. -// case ActionType.groupsSlice.actions.receiveAllGroupsChildren: { -// // Set up temporary state so only change/return once. -// const newState: GroupsState = { -// ...state, -// byGroupID: { -// ...state.byGroupID -// } -// } -// // For each group that received data, set the children meters and groups. -// for (const groupInfo of action.data) { -// // Group id of the current item -// const groupId = groupInfo.groupId; -// // Reset the newState for this group to have child meters/groups. -// newState.byGroupID[groupId].childMeters = groupInfo.childMeters; -// newState.byGroupID[groupId].childGroups = groupInfo.childGroups; -// } -// // Note that not fetching children -// newState.isFetchingAllChildren = false -// // The updated state. -// return newState; -// } - -// case ActionType.ChangeDisplayedGroups: { -// return { -// ...state, -// selectedGroups: action.groupIDs -// }; -// } - -// case ActionType.ConfirmEditedGroup: { -// // Return new state object with updated edited group info. -// return { -// ...state, -// byGroupID: { -// ...state.byGroupID, -// [action.editedGroup.id]: { -// // There is state that is in each group that is not part of the edit information state. -// ...state.byGroupID[action.editedGroup.id], -// ...action.editedGroup -// } -// } -// }; -// } - -// default: -// return state; -// } -// } \ No newline at end of file + // , + // extraReducers: builder => { + // builder.addMatcher(groupsApi.endpoints.getGroups.matchFulfilled, + // (state, { payload }) => { + // const newGroups = payload.map(group => ({ + // ...group, + // isFetching: false, + // // Sometimes OED fetches both the details and the child meters/groups as separate actions. Since the order they will happen is + // // uncertain, we need to preserve the child meters/groups if they exist. If not, put empty so no issues when accessing in other + // // places. Note this may be the wrong values but they should refresh quickly once all actions are done. + // childGroups: (state.byGroupID[group.id] && state.byGroupID[group.id].childGroups) ? state.byGroupID[group.id].childGroups : [], + // childMeters: (state.byGroupID[group.id] && state.byGroupID[group.id].childMeters) ? state.byGroupID[group.id].childMeters : [], + // selectedGroups: [], + // selectedMeters: [], + // deepMeters: state.byGroupID[group.id]?.deepMeters ? state.byGroupID[group.id].deepMeters : [] + // })); + // // newGroups is an array: this converts it into a nested object where the key to each group is its ID. + // // Without this, byGroupID will not be keyed by group ID. + // state.isFetching = false; + // // TODO FIX TYPES HERE Weird interaction here + // state.byGroupID = _.keyBy(newGroups, 'id'); + // } + // ) + // } +}); \ No newline at end of file diff --git a/src/client/app/reducers/meters.ts b/src/client/app/reducers/meters.ts index 0b41a8e2b..983e56549 100644 --- a/src/client/app/reducers/meters.ts +++ b/src/client/app/reducers/meters.ts @@ -5,6 +5,7 @@ import * as _ from 'lodash'; import { MetersState } from '../types/redux/meters'; import { durationFormat } from '../utils/durationFormat'; import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { metersApi } from '../redux/api/metersApi'; import * as t from '../types/redux/meters' const defaultState: MetersState = { @@ -47,5 +48,13 @@ export const metersSlice = createSlice({ deleteSubmittedMeter: (state, action: PayloadAction) => { state.submitting.splice(state.submitting.indexOf(action.payload)); } + }, + extraReducers: builder => { + builder.addMatcher( + metersApi.endpoints.getMeters.matchFulfilled, + (state, { payload }) => { + state.byMeterID = payload + } + ) } }); \ No newline at end of file diff --git a/src/client/app/redux/api/baseApi.ts b/src/client/app/redux/api/baseApi.ts index 0de221159..c5f60b61b 100644 --- a/src/client/app/redux/api/baseApi.ts +++ b/src/client/app/redux/api/baseApi.ts @@ -5,5 +5,7 @@ export const baseApi = createApi({ reducerPath: 'api', baseQuery: fetchBaseQuery({ baseUrl: baseHref }), // Initially no defined endpoints, Use rtk query's injectEndpoints - endpoints: () => ({}) + endpoints: () => ({}), + // Keed Data in Cache for 10 Minutes + keepUnusedDataFor: 600 }) \ No newline at end of file diff --git a/src/client/app/redux/api/groupsApi.ts b/src/client/app/redux/api/groupsApi.ts index dd4070ca8..1b42d8f5d 100644 --- a/src/client/app/redux/api/groupsApi.ts +++ b/src/client/app/redux/api/groupsApi.ts @@ -1,9 +1,11 @@ import { baseApi } from './baseApi' -import { GroupData } from '../../types/redux/groups' +import { GroupDetailsData } from '../../types/redux/groups' export const groupsApi = baseApi.injectEndpoints({ endpoints: builder => ({ - getGroups: builder.query({ query: () => 'api/groups' }) + getGroups: builder.query({ + query: () => 'api/groups' + }) }) }) diff --git a/src/client/app/redux/selectors/threeDSelectors.ts b/src/client/app/redux/selectors/threeDSelectors.ts index f3de7fe66..b3c2441d3 100644 --- a/src/client/app/redux/selectors/threeDSelectors.ts +++ b/src/client/app/redux/selectors/threeDSelectors.ts @@ -4,22 +4,19 @@ import { selectGroupInfo } from '../../redux/api/groupsApi'; import { RootState } from '../../store' import { MeterOrGroup } from '../../types/redux/graph' import { AreaUnitType } from '../../utils/getAreaUnitConversion'; -import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatability'; +import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; import { ThreeDReadingApiParams } from '../api/readingsApi' +import { selectGraphUnitID, selectGraphTimeInterval } from '../selectors/uiSelectors' // Common Fine Grained selectors const selectThreeDMeterOrGroupID = (state: RootState) => state.graph.threeD.meterOrGroupID; const selectThreeDMeterOrGroup = (state: RootState) => state.graph.threeD.meterOrGroup; -const selectGraphTimeInterval = (state: RootState) => state.graph.timeInterval; -const selectGraphUnitID = (state: RootState) => state.graph.selectedUnit; -const selectThreeDReadingInterval = (state: RootState) => state.graph.threeD.readingInterval; -const selectMeterData = (state: RootState) => selectMeterInfo(state).data -const selectGroupData = (state: RootState) => selectGroupInfo(state).data +export const selectThreeDReadingInterval = (state: RootState) => state.graph.threeD.readingInterval; // Memoized Selectors export const selectThreeDComponentInfo = createSelector( - [selectThreeDMeterOrGroupID, selectThreeDMeterOrGroup, selectMeterData, selectGroupData], - (id, meterOrGroup, meterData, groupData) => { + [selectThreeDMeterOrGroupID, selectThreeDMeterOrGroup, selectMeterInfo, selectGroupInfo], + (id, meterOrGroup, { data: meterData }, { data: groupData }) => { //Default Values let meterOrGroupName = 'Unselected Meter or Group' let isAreaCompatible = true; diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts new file mode 100644 index 000000000..797da3909 --- /dev/null +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -0,0 +1,140 @@ +/* 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 { UnitRepresentType } from '../../types/redux/units' +import { metersInGroup, unitsCompatibleWithMeters } from '../../utils/determineCompatibleUnits'; +import { getSelectOptionsByItem } from '../../components/ChartDataSelectComponent' + + +import { createSelector } from '@reduxjs/toolkit'; + +export const selectSelectedMeters = (state: RootState) => state.graph.selectedMeters; +export const selectSelectedGroups = (state: RootState) => state.graph.selectedGroups; +export const selectCurrentUser = (state: RootState) => state.currentUser; +export const selectGraphTimeInterval = (state: RootState) => state.graph.timeInterval; +export const selectGraphUnitID = (state: RootState) => state.graph.selectedUnit; +export const selectGraphAreaNormalization = (state: RootState) => state.graph.areaNormalization; +export const selectMeterState = (state: RootState) => state.meters; +export const selectGroupState = (state: RootState) => state.groups; +export const selectUnitState = (state: RootState) => state.units; + +export const selectVisibleMetersAndGroups = createSelector( + [selectMeterState, selectGroupState, selectCurrentUser], + (meterState, groupState, currentUser) => { + // Holds all meters visible to the user + const visibleMeters = new Set(); + const visibleGroups = new Set(); + + // Get all the meters that this user can see. + if (currentUser.profile?.role === 'admin') { + // Can see all meters + Object.values(meterState.byMeterID).forEach(meter => { + visibleMeters.add(meter.id); + }); + Object.values(groupState.byGroupID).forEach(group => { + visibleGroups.add(group.id); + }); + } + else { + // Regular user or not logged in so only add displayable meters + Object.values(meterState.byMeterID).forEach(meter => { + if (meter.displayable) { + visibleMeters.add(meter.id); + } + }); + Object.values(groupState.byGroupID).forEach(group => { + if (group.displayable) { + visibleGroups.add(group.id); + } + }); + } + return { meters: visibleMeters, groups: visibleGroups } + } +); + +export const selectMetersAndGroupsCompatibility = createSelector( + [selectVisibleMetersAndGroups, selectMeterState, selectGroupState, selectUnitState, selectGraphUnitID, selectGraphAreaNormalization], + (visible, meterState, groupState, unitState, graphUnitID, graphAreaNorm) => { + // meters and groups that can graph + const compatibleMeters = new Set(); + const compatibleGroups = new Set(); + + // meters and groups that cannot graph. + const incompatibleMeters = new Set(); + const incompatibleGroups = new Set(); + + if (graphUnitID === -99) { + // No unit is selected then no meter/group should be selected. + // In this case, every meter is valid (provided it has a default graphic unit) + // If the meter/group has a default graphic unit set then it can graph, otherwise it cannot. + visible.meters.forEach(meterId => { + const meterGraphingUnit = meterState.byMeterID[meterId].defaultGraphicUnit; + if (meterGraphingUnit === -99) { + //Default graphic unit is not set + incompatibleMeters.add(meterId); + } + else { + //Default graphic unit is set + if (graphAreaNorm && unitState.units[meterGraphingUnit] && unitState.units[meterGraphingUnit].unitRepresent === UnitRepresentType.raw) { + // area normalization is enabled and meter type is raw + incompatibleMeters.add(meterId); + } else { + compatibleMeters.add(meterId); + } + } + }); + visible.groups.forEach(groupId => { + const groupGraphingUnit = groupState.byGroupID[groupId].defaultGraphicUnit; + if (groupGraphingUnit === -99) { + //Default graphic unit is not set + incompatibleGroups.add(groupId); + } + else { + //Default graphic unit is set + if (graphAreaNorm && unitState.units[groupGraphingUnit] && + unitState.units[groupGraphingUnit].unitRepresent === UnitRepresentType.raw) { + // area normalization is enabled and meter type is raw + incompatibleGroups.add(groupId); + } else { + compatibleGroups.add(groupId); + } + } + }); + } else { + // A unit is selected + // For each meter get all of its compatible units + // Then, check if the selected unit exists in that set of compatible units + visible.meters.forEach(meterId => { + // Get the set of units compatible with the current meter + const compatibleUnits = unitsCompatibleWithMeters(new Set([meterId])); + if (compatibleUnits.has(graphUnitID)) { + // The selected unit is part of the set of compatible units with this meter + compatibleMeters.add(meterId); + } + else { + // The selected unit is not part of the compatible units set for this meter + incompatibleMeters.add(meterId); + } + }); + visible.groups.forEach(groupId => { + // Get the set of units compatible with the current group (through its deepMeters attribute) + // TODO If a meter in a group is not visible to this user then it is not in Redux state and this fails. + const compatibleUnits = unitsCompatibleWithMeters(metersInGroup(groupId)); + if (compatibleUnits.has(graphUnitID)) { + // The selected unit is part of the set of compatible units with this group + compatibleGroups.add(groupId); + } + else { + // The selected unit is not part of the compatible units set for this group + incompatibleGroups.add(groupId); + } + }); + } + const finalMeters = getSelectOptionsByItem(compatibleMeters, incompatibleMeters, meterState); + const finalGroups = getSelectOptionsByItem(compatibleGroups, incompatibleGroups, groupState); + return { finalMeters, finalGroups } + } +) + diff --git a/src/client/app/store.ts b/src/client/app/store.ts index 61b9c3af5..1a9179c6b 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -10,7 +10,7 @@ import { baseApi } from './redux/api/baseApi'; export const store = configureStore({ reducer: rootReducer, middleware: getDefaultMiddleware => getDefaultMiddleware({ - // immutableCheck: false, + immutableCheck: false, serializableCheck: false }).concat(baseApi.middleware) }); diff --git a/src/client/app/types/redux/groups.ts b/src/client/app/types/redux/groups.ts index f07ceb5ea..5070e98e0 100644 --- a/src/client/app/types/redux/groups.ts +++ b/src/client/app/types/redux/groups.ts @@ -81,7 +81,10 @@ export interface StatefulEditable { dirty: boolean; submitted?: boolean; } +export interface GroupDataByID { + [groupID: number]: GroupDefinition; +} export interface GroupsState { hasBeenFetchedOnce: boolean; // If all groups child meters/groups are in state. @@ -89,9 +92,7 @@ export interface GroupsState { isFetching: boolean; // If fetching all groups child meters/groups. isFetchingAllChildren: boolean; - byGroupID: { - [groupID: number]: GroupDefinition; - }; + byGroupID: GroupDataByID selectedGroups: number[]; // TODO groupInEditing: GroupDefinition & StatefulEditable | StatefulEditable; displayMode: DisplayMode; diff --git a/src/client/app/utils/dateRangeCompatability.ts b/src/client/app/utils/dateRangeCompatibility.ts similarity index 100% rename from src/client/app/utils/dateRangeCompatability.ts rename to src/client/app/utils/dateRangeCompatibility.ts From 988b2eaa46cdb4dc43abe0b083d4b4c2e33f677a Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Tue, 19 Sep 2023 21:53:17 +0000 Subject: [PATCH 010/131] Fix Groups Query for deep meters --- src/client/app/reducers/groups.ts | 51 ++++++++++++++++--------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/src/client/app/reducers/groups.ts b/src/client/app/reducers/groups.ts index 4f2db1947..2297adefc 100644 --- a/src/client/app/reducers/groups.ts +++ b/src/client/app/reducers/groups.ts @@ -49,7 +49,7 @@ export const groupsSlice = createSlice({ childMeters: (state.byGroupID[group.id] && state.byGroupID[group.id].childMeters) ? state.byGroupID[group.id].childMeters : [], selectedGroups: [], selectedMeters: [], - deepMeters: state.byGroupID[group.id]?.deepMeters + deepMeters: group.deepMeters ? group.deepMeters : [] })); // newGroups is an array: this converts it into a nested object where the key to each group is its ID. // Without this, byGroupID will not be keyed by group ID. @@ -96,29 +96,30 @@ export const groupsSlice = createSlice({ ...action.payload }; } + }, + extraReducers: builder => { + builder.addMatcher(groupsApi.endpoints.getGroups.matchFulfilled, + (state, { payload }) => { + const newGroups = payload.map(group => ({ + ...group, + isFetching: false, + // Sometimes OED fetches both the details and the child meters/groups as separate actions. Since the order they will happen is + // uncertain, we need to preserve the child meters/groups if they exist. If not, put empty so no issues when accessing in other + // places. Note this may be the wrong values but they should refresh quickly once all actions are done. + childGroups: (state.byGroupID[group.id] && state.byGroupID[group.id].childGroups) ? state.byGroupID[group.id].childGroups : [], + childMeters: (state.byGroupID[group.id] && state.byGroupID[group.id].childMeters) ? state.byGroupID[group.id].childMeters : [], + selectedGroups: [], + selectedMeters: [], + + // line added due to conflicting typing. TS Warns about potential undefined deepMeters + deepMeters: group.deepMeters ? group.deepMeters : [] + })); + // newGroups is an array: this converts it into a nested object where the key to each group is its ID. + // Without this, byGroupID will not be keyed by group ID. + state.isFetching = false; + // TODO FIX TYPES HERE Weird interaction here + state.byGroupID = _.keyBy(newGroups, 'id'); + } + ) } - // , - // extraReducers: builder => { - // builder.addMatcher(groupsApi.endpoints.getGroups.matchFulfilled, - // (state, { payload }) => { - // const newGroups = payload.map(group => ({ - // ...group, - // isFetching: false, - // // Sometimes OED fetches both the details and the child meters/groups as separate actions. Since the order they will happen is - // // uncertain, we need to preserve the child meters/groups if they exist. If not, put empty so no issues when accessing in other - // // places. Note this may be the wrong values but they should refresh quickly once all actions are done. - // childGroups: (state.byGroupID[group.id] && state.byGroupID[group.id].childGroups) ? state.byGroupID[group.id].childGroups : [], - // childMeters: (state.byGroupID[group.id] && state.byGroupID[group.id].childMeters) ? state.byGroupID[group.id].childMeters : [], - // selectedGroups: [], - // selectedMeters: [], - // deepMeters: state.byGroupID[group.id]?.deepMeters ? state.byGroupID[group.id].deepMeters : [] - // })); - // // newGroups is an array: this converts it into a nested object where the key to each group is its ID. - // // Without this, byGroupID will not be keyed by group ID. - // state.isFetching = false; - // // TODO FIX TYPES HERE Weird interaction here - // state.byGroupID = _.keyBy(newGroups, 'id'); - // } - // ) - // } }); \ No newline at end of file From fcbe73daf591ceea1202e48f744fc749a90c4c85 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Tue, 19 Sep 2023 21:56:13 +0000 Subject: [PATCH 011/131] fix RPDSelect --- src/client/app/components/ReadingsPerDaySelectComponent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/app/components/ReadingsPerDaySelectComponent.tsx b/src/client/app/components/ReadingsPerDaySelectComponent.tsx index 11801f597..f8d5b2387 100644 --- a/src/client/app/components/ReadingsPerDaySelectComponent.tsx +++ b/src/client/app/components/ReadingsPerDaySelectComponent.tsx @@ -32,9 +32,9 @@ export default function ReadingsPerDaySelect() { const { data, isFetching } = readingsApi.endpoints.threeD.useQuery(queryArgs, { skip: shouldSkip }); let actualReadingInterval = ReadingInterval.Hourly - if (data && data.zData[0][0]) { + if (data && data.zData.length) { // Special Case: When no compatible data available, data returned is from api is -999 - if (data.zData[0][0] < 0) { + if (data.zData[0][0] && data.zData[0][0] < 0) { actualReadingInterval = ReadingInterval.Incompatible; } else { const startTS = moment.utc(data.xData[0].startTimestamp); From 3ee5052638e6dd748d4a6a74709aa447c9e9c58b Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Wed, 20 Sep 2023 00:34:12 +0000 Subject: [PATCH 012/131] wip --- .../components/ChartDataSelectComponent.tsx | 17 +++++++---------- .../app/components/InitializationComponent.tsx | 10 ++++------ src/client/app/redux/selectors/uiSelectors.ts | 18 ++++++++++++++---- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/client/app/components/ChartDataSelectComponent.tsx b/src/client/app/components/ChartDataSelectComponent.tsx index b81b40772..824f646a0 100644 --- a/src/client/app/components/ChartDataSelectComponent.tsx +++ b/src/client/app/components/ChartDataSelectComponent.tsx @@ -25,8 +25,8 @@ import { MetersState } from 'types/redux/meters'; import { GroupsState } from 'types/redux/groups'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; import { graphSlice } from '../reducers/graph'; -// import { selectMetersAndGroupsCompatibility, selectVisibleMetersAndGroups } from '../redux/selectors/uiSelectors' -// import { useAppSelector } from '../redux/hooks'; +import { selectMetersAndGroupsCompatibilityWithCurrentUnit, selectVisibleMetersAndGroups } from '../redux/selectors/uiSelectors' +import { useAppSelector } from '../redux/hooks'; /** * A component which allows the user to select which data should be displayed on the chart. * @returns Chart data select element @@ -35,11 +35,10 @@ export default function ChartDataSelectComponent() { // Must specify type if using ThunkDispatch const dispatch: Dispatch = useDispatch(); const intl = useIntl(); - // TESTING SELECTORS - // const visibleMetersAndGroups = useAppSelector(state => selectVisibleMetersAndGroups(state)) - // const meterNGroupCompat = useAppSelector(state => selectMetersAndGroupsCompatibility(state)) - // console.log('visibleMetersAndGroups', visibleMetersAndGroups) - // console.log('meterNGroupCompat', meterNGroupCompat) + const visibleMetersAndGroups = useAppSelector(state => selectVisibleMetersAndGroups(state)) + const meterNGroupCompat = useAppSelector(state => selectMetersAndGroupsCompatibilityWithCurrentUnit(state)) + console.log('visibleMetersAndGroups', visibleMetersAndGroups) + console.log('meterNGroupCompat', meterNGroupCompat) const dataProps = useSelector((state: State) => { const allMeters = state.meters.byMeterID; const allGroups = state.groups.byGroupID; @@ -203,9 +202,7 @@ export default function ChartDataSelectComponent() { // The selectedUnit becomes the unit of the group selected. Note is should always be set (not -99) since // those groups should not have been visible. The only exception is if there are no selected groups but // then this loop does not run. The loop is assumed to only run once in this case. - // dispatch(changeSelectedUnit(state.groups.byGroupID[groupID].defaultGraphicUnit)); - - + dispatch(changeSelectedUnit(state.groups.byGroupID[groupID].defaultGraphicUnit)); } compatibleSelectedGroups.push({ // For groups we display the name since no identifier. diff --git a/src/client/app/components/InitializationComponent.tsx b/src/client/app/components/InitializationComponent.tsx index 317aad5fb..f249a03fc 100644 --- a/src/client/app/components/InitializationComponent.tsx +++ b/src/client/app/components/InitializationComponent.tsx @@ -6,7 +6,6 @@ import * as React from 'react'; import { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { State } from '../types/redux/state'; -import { fetchMetersDetails, fetchMetersDetailsIfNeeded } from '../actions/meters'; import { ConversionArray } from '../types/conversionArray'; import { fetchPreferencesIfNeeded } from '../actions/admin'; import { fetchMapsDetails } from '../actions/map'; @@ -17,7 +16,6 @@ import { Slide, ToastContainer } from 'react-toastify'; import { metersApi } from '../redux/api/metersApi'; import { groupsApi } from '../redux/api/groupsApi'; import 'react-toastify/dist/ReactToastify.css'; -import { fetchGroupsDetailsIfNeeded } from '../actions/groups'; /** * Initializes OED redux with needed details @@ -26,12 +24,10 @@ import { fetchGroupsDetailsIfNeeded } from '../actions/groups'; export default function InitializationComponent() { const dispatch: Dispatch = useDispatch(); - metersApi.endpoints.getMeters.useQuery(); + const { refetch: refetchMeters } = metersApi.endpoints.getMeters.useQuery(); groupsApi.endpoints.getGroups.useQuery(); // Only run once by making it depend on an empty array. useEffect(() => { - // dispatch(fetchMetersDetailsIfNeeded()); - dispatch(fetchGroupsDetailsIfNeeded()); dispatch(fetchPreferencesIfNeeded()); dispatch(fetchMapsDetails()); dispatch(fetchUnitsDetailsIfNeeded()); @@ -45,7 +41,9 @@ export default function InitializationComponent() { // Because of this must re-fetch the entire meters table if the user changes const currentUser = useSelector((state: State) => state.currentUser.profile); useEffect(() => { - dispatch(fetchMetersDetails()); + // TODO REDO WITH TAG INVALIDATION AND PROPER AUTH HEADERS + refetchMeters() + // dispatch(fetchMetersDetails()); }, [currentUser]); return ( diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts index 797da3909..a0005dc9d 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -9,6 +9,7 @@ import { getSelectOptionsByItem } from '../../components/ChartDataSelectComponen import { createSelector } from '@reduxjs/toolkit'; +import { SelectOption } from '../../types/items'; export const selectSelectedMeters = (state: RootState) => state.graph.selectedMeters; export const selectSelectedGroups = (state: RootState) => state.graph.selectedGroups; @@ -54,7 +55,7 @@ export const selectVisibleMetersAndGroups = createSelector( } ); -export const selectMetersAndGroupsCompatibility = createSelector( +export const selectMetersAndGroupsCompatibilityWithCurrentUnit = createSelector( [selectVisibleMetersAndGroups, selectMeterState, selectGroupState, selectUnitState, selectGraphUnitID, selectGraphAreaNormalization], (visible, meterState, groupState, unitState, graphUnitID, graphAreaNorm) => { // meters and groups that can graph @@ -132,9 +133,18 @@ export const selectMetersAndGroupsCompatibility = createSelector( } }); } - const finalMeters = getSelectOptionsByItem(compatibleMeters, incompatibleMeters, meterState); - const finalGroups = getSelectOptionsByItem(compatibleGroups, incompatibleGroups, groupState); - return { finalMeters, finalGroups } + const meterSelectOptions = { compatibleMeters, incompatibleMeters }; + const groupSelectOptions = { compatibleGroups, incompatibleGroups }; + return { meterSelectOptions, groupSelectOptions } + } +) + +export const selectCompatibleSelectedMetersAndGroups = createSelector( + [selectSelectedMeters, selectSelectedGroups], + (selectedMeters, selectedGroups) => { + const compatibleSelectedMeters: SelectOption[] = []; + + } ) From 0600dc2643bdc01c729bc2eb56f45160189524c9 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Wed, 20 Sep 2023 04:45:45 +0000 Subject: [PATCH 013/131] Initial ChartDataSelector Refactor --- .../components/ChartDataSelectComponent.tsx | 7 +- .../app/redux/selectors/threeDSelectors.ts | 4 +- src/client/app/redux/selectors/uiSelectors.ts | 261 +++++++++++++++++- 3 files changed, 251 insertions(+), 21 deletions(-) diff --git a/src/client/app/components/ChartDataSelectComponent.tsx b/src/client/app/components/ChartDataSelectComponent.tsx index 824f646a0..6693690b2 100644 --- a/src/client/app/components/ChartDataSelectComponent.tsx +++ b/src/client/app/components/ChartDataSelectComponent.tsx @@ -25,8 +25,7 @@ import { MetersState } from 'types/redux/meters'; import { GroupsState } from 'types/redux/groups'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; import { graphSlice } from '../reducers/graph'; -import { selectMetersAndGroupsCompatibilityWithCurrentUnit, selectVisibleMetersAndGroups } from '../redux/selectors/uiSelectors' -import { useAppSelector } from '../redux/hooks'; + /** * A component which allows the user to select which data should be displayed on the chart. * @returns Chart data select element @@ -35,10 +34,6 @@ export default function ChartDataSelectComponent() { // Must specify type if using ThunkDispatch const dispatch: Dispatch = useDispatch(); const intl = useIntl(); - const visibleMetersAndGroups = useAppSelector(state => selectVisibleMetersAndGroups(state)) - const meterNGroupCompat = useAppSelector(state => selectMetersAndGroupsCompatibilityWithCurrentUnit(state)) - console.log('visibleMetersAndGroups', visibleMetersAndGroups) - console.log('meterNGroupCompat', meterNGroupCompat) const dataProps = useSelector((state: State) => { const allMeters = state.meters.byMeterID; const allGroups = state.groups.byGroupID; diff --git a/src/client/app/redux/selectors/threeDSelectors.ts b/src/client/app/redux/selectors/threeDSelectors.ts index b3c2441d3..80ead32a7 100644 --- a/src/client/app/redux/selectors/threeDSelectors.ts +++ b/src/client/app/redux/selectors/threeDSelectors.ts @@ -27,7 +27,7 @@ export const selectThreeDComponentInfo = createSelector( const meterInfo = meterData[id] meterOrGroupName = meterInfo.identifier; isAreaCompatible = meterInfo.area !== 0 && meterInfo.areaUnit !== AreaUnitType.none; - } else if (meterOrGroup === MeterOrGroup.meters && groupData) { + } else if (meterOrGroup === MeterOrGroup.groups && groupData) { const groupInfo = groupData[id]; meterOrGroupName = groupInfo.name; isAreaCompatible = groupInfo.area !== 0 && groupInfo.areaUnit !== AreaUnitType.none; @@ -36,7 +36,7 @@ export const selectThreeDComponentInfo = createSelector( } return { meterOrGroupID: id, - // meterOrGroup: meterOrGroup, + meterOrGroup: meterOrGroup, meterOrGroupName: meterOrGroupName, isAreaCompatible: isAreaCompatible } diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts index a0005dc9d..748728e23 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -2,14 +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 { createSelector } from '@reduxjs/toolkit'; +import * as _ from 'lodash'; +import { getSelectOptionsByItem } from '../../components/ChartDataSelectComponent'; import { RootState } from '../../store'; -import { UnitRepresentType } from '../../types/redux/units' +import { DataType } from '../../types/Datasources'; +import { ChartTypes } from '../../types/redux/graph'; +import { DisplayableType, UnitData, UnitRepresentType, UnitType } from '../../types/redux/units'; +import { + CartesianPoint, Dimensions, calculateScaleFromEndpoints, gpsToUserGrid, + itemDisplayableOnMap, itemMapInfoOk, normalizeImageDimensions +} from '../../utils/calibration'; import { metersInGroup, unitsCompatibleWithMeters } from '../../utils/determineCompatibleUnits'; -import { getSelectOptionsByItem } from '../../components/ChartDataSelectComponent' - +import { AreaUnitType } from '../../utils/getAreaUnitConversion'; -import { createSelector } from '@reduxjs/toolkit'; -import { SelectOption } from '../../types/items'; export const selectSelectedMeters = (state: RootState) => state.graph.selectedMeters; export const selectSelectedGroups = (state: RootState) => state.graph.selectedGroups; @@ -17,9 +23,12 @@ export const selectCurrentUser = (state: RootState) => state.currentUser; export const selectGraphTimeInterval = (state: RootState) => state.graph.timeInterval; export const selectGraphUnitID = (state: RootState) => state.graph.selectedUnit; export const selectGraphAreaNormalization = (state: RootState) => state.graph.areaNormalization; +export const selectChartToRender = (state: RootState) => state.graph.chartToRender; + export const selectMeterState = (state: RootState) => state.meters; export const selectGroupState = (state: RootState) => state.groups; export const selectUnitState = (state: RootState) => state.units; +export const selectMapState = (state: RootState) => state.maps; export const selectVisibleMetersAndGroups = createSelector( [selectMeterState, selectGroupState, selectCurrentUser], @@ -55,7 +64,7 @@ export const selectVisibleMetersAndGroups = createSelector( } ); -export const selectMetersAndGroupsCompatibilityWithCurrentUnit = createSelector( +export const selectMeterGroupUnitCompatibility = createSelector( [selectVisibleMetersAndGroups, selectMeterState, selectGroupState, selectUnitState, selectGraphUnitID, selectGraphAreaNormalization], (visible, meterState, groupState, unitState, graphUnitID, graphAreaNorm) => { // meters and groups that can graph @@ -133,18 +142,244 @@ export const selectMetersAndGroupsCompatibilityWithCurrentUnit = createSelector( } }); } - const meterSelectOptions = { compatibleMeters, incompatibleMeters }; - const groupSelectOptions = { compatibleGroups, incompatibleGroups }; - return { meterSelectOptions, groupSelectOptions } + + return { compatibleMeters, incompatibleMeters, compatibleGroups, incompatibleGroups } + } +) + +export const selectMeterGroupAreaAndMapCompatibility = createSelector( + selectMeterGroupUnitCompatibility, + selectGraphAreaNormalization, + selectChartToRender, + selectMeterState, + selectGroupState, + selectMapState, + (unitCompat, areaNormalization, chartToRender, meterState, groupState, mapState) => { + // store meters which are found to be incompatible. + const incompatibleMeters = new Set(); + const incompatibleGroups = new Set(); + + const compatibleMeters = new Set(); + const compatibleGroups = new Set(); + + // only run this check if area normalization is on + if (areaNormalization) { + // filter out any meter or group that is area incompatible. + unitCompat.compatibleMeters.forEach(meterID => { + // do not allow meter to be selected if it has zero area or no area unit + if (meterState.byMeterID[meterID].area === 0 || meterState.byMeterID[meterID].areaUnit === AreaUnitType.none) { + incompatibleMeters.add(meterID); + } else { + compatibleMeters.add(meterID); + } + }); + unitCompat.compatibleGroups.forEach(groupID => { + // do not allow group to be selected if it has zero area or no area unit + if (groupState.byGroupID[groupID].area === 0 || groupState.byGroupID[groupID].areaUnit === AreaUnitType.none) { + incompatibleGroups.add(groupID); + } else { + compatibleGroups.add(groupID); + } + }); + } + + 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 + }; + // Determine the dimensions so within the Plotly coordinates on the user map. + const imageDimensionNormalized = normalizeImageDimensions(imageDimensions); + // The following is needed to get the map scale. Now that the system accepts maps that are not + // pointed north, it would be better to store the origin GPS and the scale factor instead of + // the origin and opposite GPS. For now, not going to change but could if redo DB and interface + // for maps. + // 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 axes parallel to the map axes. + // 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. + // This is the origin & opposite from the calibration. It is the lower, left + // 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; + unitCompat.compatibleMeters.forEach(meterID => { + // This meter's GPS value. + const gps = meterState.byMeterID[meterID].gps; + if (origin !== undefined && opposite !== undefined && gps !== undefined && gps !== null) { + // 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); + // 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) && + itemDisplayableOnMap(imageDimensionNormalized, meterGPSInUserGrid))) { + incompatibleMeters.add(meterID); + } else { + compatibleMeters.add(meterID); + } + } else { + // Lack info on this map so skip. This is mostly done since TS complains about the undefined possibility. + incompatibleMeters.add(meterID); + } + }); + // The below code follows the logic for meters shown above. See comments above for clarification on the below code. + unitCompat.compatibleGroups.forEach(groupID => { + const gps = groupState.byGroupID[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) && + itemDisplayableOnMap(imageDimensionNormalized, groupGPSInUserGrid))) { + incompatibleGroups.add(groupID); + } else { + compatibleGroups.add(groupID); + } + } else { + incompatibleGroups.add(groupID); + } + }); + } + return { compatibleMeters, incompatibleMeters, compatibleGroups, incompatibleGroups } + } +) + +export const selectMeterGroupSelectData = createSelector( + selectMeterGroupAreaAndMapCompatibility, + selectMeterState, + selectGroupState, + (stateCompatibility, meterState, groupState) => { + // Retrieve select options from meter sets + const meterSelectOption = getSelectOptionsByItem(stateCompatibility.compatibleMeters, stateCompatibility.incompatibleMeters, meterState); + // Retrieve select options from group sets + const groupSelectOption = getSelectOptionsByItem(stateCompatibility.compatibleGroups, stateCompatibility.incompatibleGroups, groupState); + + return { meterSelectOption, groupSelectOption } + } +) +/** + * Filters all units that are of type meter or displayable type none from the redux state, as well as admin only units if the user is not an admin. + * @param state - current redux state + * @returns an array of UnitData + */ +export const selectVisibleUnitOrSuffixState = createSelector( + selectUnitState, + selectCurrentUser, + (unitState, currentUser) => { + let visibleUnitsOrSuffixes; + if (currentUser.profile?.role === 'admin') { + // User is an admin, allow all units to be seen + visibleUnitsOrSuffixes = _.filter(unitState.units, (o: UnitData) => { + return (o.typeOfUnit == UnitType.unit || o.typeOfUnit == UnitType.suffix) && o.displayable != DisplayableType.none; + }); + } + else { + // User is not an admin, do not allow for admin units to be seen + visibleUnitsOrSuffixes = _.filter(unitState.units, (o: UnitData) => { + return (o.typeOfUnit == UnitType.unit || o.typeOfUnit == UnitType.suffix) && o.displayable == DisplayableType.all; + }); + } + return visibleUnitsOrSuffixes; + } +) + +export const selectUnitSelectData = createSelector( + selectUnitState, + selectVisibleUnitOrSuffixState, + selectSelectedMeters, + selectSelectedGroups, + selectGraphAreaNormalization, + (unitState, visibleUnitsOrSuffixes, selectedMeters, selectedGroups, areaNormalization) => { + // Holds all units that are compatible with selected meters/groups + const compatibleUnits = new Set(); + // Holds all units that are not compatible with selected meters/groups + const incompatibleUnits = new Set(); + + // Holds all selected meters, including those retrieved from groups + const allSelectedMeters = new Set(); + + // Get for all meters + selectedMeters.forEach(meter => { + allSelectedMeters.add(meter); + }); + // Get for all groups + selectedGroups.forEach(group => { + // Get for all deep meters in group + metersInGroup(group).forEach(meter => { + allSelectedMeters.add(meter); + }); + }); + + if (allSelectedMeters.size == 0) { + // No meters/groups are selected. This includes the case where the selectedUnit is -99. + // Every unit is okay/compatible in this case so skip the work needed below. + // Filter the units to be displayed by user status and displayable type + visibleUnitsOrSuffixes.forEach(unit => { + if (areaNormalization && unit.unitRepresent === UnitRepresentType.raw) { + incompatibleUnits.add(unit.id); + } else { + compatibleUnits.add(unit.id); + } + }); + } else { + // Some meter or group is selected + // Retrieve set of units compatible with list of selected meters and/or groups + const units = unitsCompatibleWithMeters(allSelectedMeters); + + // Loop over all units (they must be of type unit or suffix - case 1) + visibleUnitsOrSuffixes.forEach(o => { + // Control displayable ones (case 2) + if (units.has(o.id)) { + // Should show as compatible (case 3) + compatibleUnits.add(o.id); + } else { + // Should show as incompatible (case 4) + incompatibleUnits.add(o.id); + } + }); + } + // Ready to display unit. Put selectable ones before non-selectable ones. + const finalUnits = getSelectOptionsByItem(compatibleUnits, incompatibleUnits, unitState); + return finalUnits; } ) -export const selectCompatibleSelectedMetersAndGroups = createSelector( - [selectSelectedMeters, selectSelectedGroups], - (selectedMeters, selectedGroups) => { - const compatibleSelectedMeters: SelectOption[] = []; +export const selectMeterGroupAreaCompatibility = createSelector( + selectMeterState, + selectGroupState, + (meterState, groupState) => { + // store meters which are found to be incompatible. + const incompatibleMeters = new Set(); + const incompatibleGroups = new Set(); + const compatibleMeters = new Set(); + const compatibleGroups = new Set(); + + Object.values(meterState.byMeterID).forEach(meter => { + // do not allow meter to be selected if it has zero area or no area unit + if (meterState.byMeterID[meter.id].area === 0 || meterState.byMeterID[meter.id].areaUnit === AreaUnitType.none) { + incompatibleMeters.add(meter.id); + } else { + compatibleMeters.add(meter.id); + } + }); + Object.values(groupState.byGroupID).forEach(group => { + // do not allow group to be selected if it has zero area or no area unit + if (groupState.byGroupID[group.id].area === 0 || groupState.byGroupID[group.id].areaUnit === AreaUnitType.none) { + incompatibleGroups.add(group.id); + } else { + compatibleGroups.add(group.id); + } + }); + return { compatibleMeters, incompatibleMeters, compatibleGroups, incompatibleGroups } } ) From 0445f17a949affb38e5077718159f6ab2b7a52e2 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Thu, 21 Sep 2023 05:26:52 +0000 Subject: [PATCH 014/131] Incremental Changes --Selects working with refactored & memo'd Chart DataSelectors --- src/client/app/actions/graph.ts | 4 +- .../components/ChartDataSelectComponent.tsx | 33 ++- .../MeterAndGroupSelectComponent.tsx | 86 ++++++- .../app/components/UnitSelectComponent.tsx | 89 +++++++ src/client/app/index.tsx | 4 +- src/client/app/reducers/graph.ts | 69 +++++- src/client/app/redux/api/readingsApi.ts | 6 +- .../app/redux/selectors/threeDSelectors.ts | 14 +- src/client/app/redux/selectors/uiSelectors.ts | 220 +++++++++++++++--- src/client/app/types/items.ts | 7 +- src/client/app/types/redux/graph.ts | 8 +- 11 files changed, 467 insertions(+), 73 deletions(-) create mode 100644 src/client/app/components/UnitSelectComponent.tsx diff --git a/src/client/app/actions/graph.ts b/src/client/app/actions/graph.ts index 740c5a2f9..a761af5c8 100644 --- a/src/client/app/actions/graph.ts +++ b/src/client/app/actions/graph.ts @@ -162,11 +162,11 @@ export function updateThreeDReadingInterval(readingInterval: t.ReadingInterval): }; } -export function updateThreeDMeterOrGroupInfo(meterOrGroupID: t.MeterOrGroupID, meterOrGroup: t.MeterOrGroup) { +export function updateThreeDMeterOrGroupInfo(meterOrGroupID: t.MeterOrGroupID | undefined, meterOrGroup: t.MeterOrGroup) { return graphSlice.actions.updateThreeDMeterOrGroupInfo({ meterOrGroupID, meterOrGroup }); } -export function changeMeterOrGroupInfo(meterOrGroupID: t.MeterOrGroupID, meterOrGroup: t.MeterOrGroup = t.MeterOrGroup.meters): Thunk { +export function changeMeterOrGroupInfo(meterOrGroupID: t.MeterOrGroupID | undefined, meterOrGroup: t.MeterOrGroup = t.MeterOrGroup.meters): Thunk { // Meter ID can be null, however meterOrGroup defaults to meters a null check on ID can be sufficient return (dispatch: Dispatch) => { dispatch(updateThreeDMeterOrGroupInfo(meterOrGroupID, meterOrGroup)); diff --git a/src/client/app/components/ChartDataSelectComponent.tsx b/src/client/app/components/ChartDataSelectComponent.tsx index 6693690b2..d56d0a7f0 100644 --- a/src/client/app/components/ChartDataSelectComponent.tsx +++ b/src/client/app/components/ChartDataSelectComponent.tsx @@ -25,6 +25,10 @@ import { MetersState } from 'types/redux/meters'; import { GroupsState } from 'types/redux/groups'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; import { graphSlice } from '../reducers/graph'; +import UnitSelectComponent from './UnitSelectComponent'; +import MeterAndGroupSelectComponent from './MeterAndGroupSelectComponent'; +import { useAppSelector } from '../redux/hooks'; +import { selectMeterGroupSelectData } from '../redux/selectors/uiSelectors'; /** * A component which allows the user to select which data should be displayed on the chart. @@ -34,6 +38,7 @@ export default function ChartDataSelectComponent() { // Must specify type if using ThunkDispatch const dispatch: Dispatch = useDispatch(); const intl = useIntl(); + const selectTestOpts = useAppSelector(state => selectMeterGroupSelectData(state)) const dataProps = useSelector((state: State) => { const allMeters = state.meters.byMeterID; const allGroups = state.groups.byGroupID; @@ -267,11 +272,14 @@ export default function ChartDataSelectComponent() { threeDState } }); - - + console.log('HERE WE ARE') + console.log(dataProps.sortedGroups) + console.log(dataProps.compatibleSelectedGroups) + console.log(selectTestOpts) return (

+ Ref: :

@@ -300,7 +308,11 @@ export default function ChartDataSelectComponent() { }} />
+
+ +

+ Ref: :

@@ -338,7 +350,11 @@ export default function ChartDataSelectComponent() { }} /> +
+ +

+ Ref: :

@@ -359,7 +375,7 @@ export default function ChartDataSelectComponent() { dispatch(graphSlice.actions.updateSelectedMeters([])); dispatch(graphSlice.actions.updateSelectedUnit(-99)); // Sync threeD state. - dispatch(changeMeterOrGroupInfo(null)); + dispatch(changeMeterOrGroupInfo(undefined)); } else if (newSelectedUnitOptions.length === 1) { dispatch(changeSelectedUnit(newSelectedUnitOptions[0].value)); } else if (newSelectedUnitOptions.length > 1) { dispatch(changeSelectedUnit(newSelectedUnitOptions[1].value)); } @@ -368,6 +384,9 @@ export default function ChartDataSelectComponent() { }} /> +
+ +
); } @@ -681,21 +700,21 @@ export function getSelectOptionsByItem(compatibleItems: Set, incompatibl * @param state The state to check * @returns Whether or not this is a UnitsState */ -function instanceOfUnitsState(state: any): state is UnitsState { return 'units' in state; } +export function instanceOfUnitsState(state: any): state is UnitsState { return 'units' in state; } /** * Helper function to determine what type of state was passed in * @param state The state to check * @returns Whether or not this is a MetersState */ -function instanceOfMetersState(state: any): state is MetersState { return 'byMeterID' in state; } +export function instanceOfMetersState(state: any): state is MetersState { return 'byMeterID' in state; } /** * Helper function to determine what type of state was passed in * @param state The state to check * @returns Whether or not this is a GroupsState */ -function instanceOfGroupsState(state: any): state is GroupsState { return 'byGroupID' in state; } +export function instanceOfGroupsState(state: any): state is GroupsState { return 'byGroupID' in state; } /** * 3D helper function used to keep 3D redux state in sync with dropdown menus @@ -728,7 +747,7 @@ function syncThreeDState( dispatch(changeMeterOrGroupInfo(addedMeterOrGroup, meterOrGroup)); } else if (meterOrGroupRemoved && meterOrGroupIsSelected) { // reset currently active threeD Meter or group when it is removed and is currently active. - dispatch(changeMeterOrGroupInfo(null)); + dispatch(changeMeterOrGroupInfo(undefined)); } } diff --git a/src/client/app/components/MeterAndGroupSelectComponent.tsx b/src/client/app/components/MeterAndGroupSelectComponent.tsx index 31ffa96ba..40efea398 100644 --- a/src/client/app/components/MeterAndGroupSelectComponent.tsx +++ b/src/client/app/components/MeterAndGroupSelectComponent.tsx @@ -2,16 +2,88 @@ * 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 * as React from 'react'; +import Select, { ActionMeta, MultiValue } from 'react-select'; +import makeAnimated from 'react-select/animated'; +import { Badge } from 'reactstrap'; +import { GroupedOption, SelectOption } from 'types/items'; +import { graphSlice } from '../reducers/graph'; +import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { selectMeterGroupSelectData } from '../redux/selectors/uiSelectors'; +import { MeterOrGroup } from '../types/redux/graph'; +import translate from '../utils/translate'; + +const animatedComponents = makeAnimated(); /** - * @returns A React-Select component for UI Options Panel + * Creates a React-Select component for the UI Options Panel. + * @param props - Helps differentiate between meter or group options + * @returns A React-Select component. */ -export default function MeterAndGroupSelectComponent() { +export default function MeterAndGroupSelectComponent(props: MeterAndGroupSelectProps) { + const dispatch = useAppDispatch(); + const meterAndGroupSelectOptions = useAppSelector(state => selectMeterGroupSelectData(state)); + // const selectedMeters = useAppSelector(state => selectSelectedMeters(state)) + // const selectedGroups = useAppSelector(state => selectSelectedGroups(state)) + // console.log(meterAndGroupSelectOptions) + const { meterOrGroup } = props; + + // Set the current component's appropriate meter or group update from the graphSlice's Payload-Action Creator + const updateSelectedMetersOrGroups = meterOrGroup === MeterOrGroup.meters ? + graphSlice.actions.updateSelectedMetersFromSelect + : + graphSlice.actions.updateSelectedGroupsFromSelect + + const value = meterOrGroup === MeterOrGroup.meters ? + meterAndGroupSelectOptions.compatibleSelectedMeters + : + meterAndGroupSelectOptions.compatibleSelectedGroups + + // Set the current component's appropriate meter or group SelectOption + const options = meterOrGroup === MeterOrGroup.meters ? + meterAndGroupSelectOptions.meterGroupedOptions + : + meterAndGroupSelectOptions.groupsGroupedOptions + + const onChange = (newValues: MultiValue, meta: ActionMeta) => { + console.log('newValues', newValues, 'meta', meta); + const newMetersOrGroups = newValues.map((option: SelectOption) => option.value); + dispatch(updateSelectedMetersOrGroups({ newMetersOrGroups, meta })) + } return ( - <> - Hello, M n G Select! - + + + ) +} + +{/* { + // TODO I don't quite understand why the component results in an array of size 2 when updating state + // For now I have hardcoded a fix that allows units to be selected over other units without clicking the x button + if (newSelectedUnitOptions.length === 0) { + // Update the selected meters and groups to empty to avoid graphing errors + // The update selected meters/groups functions are essentially the same as the change functions + // However, they do not attempt to graph. + dispatch(graphSlice.actions.updateSelectedGroups([])); + dispatch(graphSlice.actions.updateSelectedMeters([])); + dispatch(graphSlice.actions.updateSelectedUnit(-99)); + // Sync threeD state. + dispatch(changeMeterOrGroupInfo(null)); + } + else if (newSelectedUnitOptions.length === 1) { dispatch(changeSelectedUnit(newSelectedUnitOptions[0].value)); } + else if (newSelectedUnitOptions.length > 1) { dispatch(changeSelectedUnit(newSelectedUnitOptions[1].value)); } + // This should not happen + else { dispatch(changeSelectedUnit(-99)); } +}} +/> */} + +// const labelStyle: React.CSSProperties = { +// fontWeight: 'bold', +// margin: 0 +// }; \ No newline at end of file diff --git a/src/client/app/index.tsx b/src/client/app/index.tsx index 60a948f6d..8187bdec6 100644 --- a/src/client/app/index.tsx +++ b/src/client/app/index.tsx @@ -20,7 +20,9 @@ const container = document.getElementById('root'); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const root = createRoot(container!); root.render( - + + // + ); diff --git a/src/client/app/reducers/graph.ts b/src/client/app/reducers/graph.ts index e69366361..86254ed35 100644 --- a/src/client/app/reducers/graph.ts +++ b/src/client/app/reducers/graph.ts @@ -9,6 +9,8 @@ import { calculateCompareTimeInterval, ComparePeriod, SortingOrder } from '../ut import { AreaUnitType } from '../utils/getAreaUnitConversion'; import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { adminSlice } from './admin'; +import { ActionMeta } from 'react-select'; +import { SelectOption } from '../types/items'; const defaultState: GraphState = { selectedMeters: [], @@ -30,8 +32,8 @@ const defaultState: GraphState = { renderOnce: false, showMinMax: false, threeD: { - meterOrGroupID: null, - meterOrGroup: MeterOrGroup.meters, + meterOrGroupID: undefined, + meterOrGroup: undefined, readingInterval: ReadingInterval.Hourly } }; @@ -95,13 +97,72 @@ export const graphSlice = createSlice({ updateLineGraphRate: (state, action: PayloadAction) => { state.lineGraphRate = action.payload }, - updateThreeDReadingInterval: (state, action: PayloadAction) => { state.threeD.readingInterval = action.payload }, - updateThreeDMeterOrGroupInfo: (state, action: PayloadAction<{ meterOrGroupID: number | null, meterOrGroup: MeterOrGroup }>) => { + updateThreeDMeterOrGroupInfo: (state, action: PayloadAction<{ meterOrGroupID: number | undefined, meterOrGroup: MeterOrGroup }>) => { state.threeD.meterOrGroupID = action.payload.meterOrGroupID state.threeD.meterOrGroup = action.payload.meterOrGroup + }, + updateSelectedMetersFromSelect: (state, action: PayloadAction<{ newMetersOrGroups: number[], meta: ActionMeta }>) => { + // Destructure payload + const { newMetersOrGroups, meta } = action.payload; + + // Used to check if value has been added or removed + const addedMeterOrGroupID = meta.option?.value; + const addedMeterOrGroup = meta.option?.meterOrGroup; + + const removedMeterOrGroupID = meta.removedValue?.value; + const removedMeterOrGroup = meta.removedValue?.meterOrGroup; + const clearedMeterOrGroups = meta.removedValues; + console.log('METAAAAAAAAAAA', meta) + + // If no meters selected, and no area unit, we should update unit to default graphic unit + // const shouldUpdateUnit = !state.selectedGroups.length && !state.selectedMeters.length && state.selectedUnit === -99 + // If meterMeter added then and should update unit, update unit. + // TODO graphic unit is currently snuck into the select option, find an alternative pattern + // state.selectedUnit = addedMeterOrGroupID && !shouldUpdateUnit ? state.selectedUnit : meta. + + // TODO SELECT bug in reducer + // Determine If meter or group was modified then update appropriately + const meterOrGroup = addedMeterOrGroup ? addedMeterOrGroup : removedMeterOrGroup; + if (clearedMeterOrGroups) { + const isAMeter = clearedMeterOrGroups[0].meterOrGroup === MeterOrGroup.meters + isAMeter ? + state.selectedMeters = [] + : + state.selectedGroups = [] + } else if (meterOrGroup && meterOrGroup === MeterOrGroup.meters) { + state.selectedMeters = newMetersOrGroups + } else { + state.selectedGroups = newMetersOrGroups + } + + // When a meter or group is selected/added, make it the currently active in 3D state. + if (addedMeterOrGroupID && addedMeterOrGroup && state.chartToRender === ChartTypes.threeD) { + // TODO Currently only tracks when on 3d, Verify that this is the desired behavior + state.threeD.meterOrGroupID = addedMeterOrGroupID; + state.threeD.meterOrGroup = addedMeterOrGroup; + addedMeterOrGroup === MeterOrGroup.meters ? + state.selectedMeters = newMetersOrGroups + : + state.selectedGroups = newMetersOrGroups + } + + // Reset Currently Selected 3D Meter Or Group if it has been removed from any page + if ( + // meterOrGroup was removed + removedMeterOrGroupID && removedMeterOrGroup && + // Removed meterOrGroup is the currently active on the 3D page + removedMeterOrGroupID === state.threeD.meterOrGroupID && removedMeterOrGroup === state.threeD.meterOrGroup + ) { + state.threeD.meterOrGroupID = undefined + state.threeD.meterOrGroup = undefined + + } + }, + updateSelectedGroupsFromSelect: (state, action: PayloadAction<{ newMetersOrGroups: number[], meta: ActionMeta }>) => { + state.selectedGroups = action.payload.newMetersOrGroups } }, extraReducers: builder => { diff --git a/src/client/app/redux/api/readingsApi.ts b/src/client/app/redux/api/readingsApi.ts index 5be6e5681..2650067c5 100644 --- a/src/client/app/redux/api/readingsApi.ts +++ b/src/client/app/redux/api/readingsApi.ts @@ -4,7 +4,7 @@ import { MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; export type ThreeDReadingApiParams = { - meterID: number; + meterOrGroupID: number; timeInterval: string; unitID: number; readingInterval: ReadingInterval; @@ -14,9 +14,9 @@ export type ThreeDReadingApiParams = { export const readingsApi = baseApi.injectEndpoints({ endpoints: builder => ({ threeD: builder.query({ - query: ({ meterID, timeInterval, unitID, readingInterval, meterOrGroup }) => { + query: ({ meterOrGroupID, timeInterval, unitID, readingInterval, meterOrGroup }) => { const endpoint = `/api/unitReadings/threeD/${meterOrGroup}/` - const args = `${meterID}?timeInterval=${timeInterval.toString()}&graphicUnitId=${unitID}&readingInterval=${readingInterval}` + const args = `${meterOrGroupID}?timeInterval=${timeInterval.toString()}&graphicUnitId=${unitID}&readingInterval=${readingInterval}` return `${endpoint}${args}` } }) diff --git a/src/client/app/redux/selectors/threeDSelectors.ts b/src/client/app/redux/selectors/threeDSelectors.ts index 80ead32a7..d142cf31b 100644 --- a/src/client/app/redux/selectors/threeDSelectors.ts +++ b/src/client/app/redux/selectors/threeDSelectors.ts @@ -1,12 +1,10 @@ import { createSelector } from '@reduxjs/toolkit'; -import { selectMeterInfo } from '../../redux/api/metersApi'; -import { selectGroupInfo } from '../../redux/api/groupsApi'; import { RootState } from '../../store' import { MeterOrGroup } from '../../types/redux/graph' import { AreaUnitType } from '../../utils/getAreaUnitConversion'; import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; import { ThreeDReadingApiParams } from '../api/readingsApi' -import { selectGraphUnitID, selectGraphTimeInterval } from '../selectors/uiSelectors' +import { selectGraphUnitID, selectGraphTimeInterval, selectMeterState, selectGroupState } from '../selectors/uiSelectors' // Common Fine Grained selectors const selectThreeDMeterOrGroupID = (state: RootState) => state.graph.threeD.meterOrGroupID; @@ -15,8 +13,8 @@ export const selectThreeDReadingInterval = (state: RootState) => state.graph.thr // Memoized Selectors export const selectThreeDComponentInfo = createSelector( - [selectThreeDMeterOrGroupID, selectThreeDMeterOrGroup, selectMeterInfo, selectGroupInfo], - (id, meterOrGroup, { data: meterData }, { data: groupData }) => { + [selectThreeDMeterOrGroupID, selectThreeDMeterOrGroup, selectMeterState, selectGroupState], + (id, meterOrGroup, meterData, groupData) => { //Default Values let meterOrGroupName = 'Unselected Meter or Group' let isAreaCompatible = true; @@ -24,11 +22,11 @@ export const selectThreeDComponentInfo = createSelector( if (id) { // Get Meter or Group's info if (meterOrGroup === MeterOrGroup.meters && meterData) { - const meterInfo = meterData[id] + const meterInfo = meterData.byMeterID[id] meterOrGroupName = meterInfo.identifier; isAreaCompatible = meterInfo.area !== 0 && meterInfo.areaUnit !== AreaUnitType.none; } else if (meterOrGroup === MeterOrGroup.groups && groupData) { - const groupInfo = groupData[id]; + const groupInfo = groupData.byGroupID[id]; meterOrGroupName = groupInfo.name; isAreaCompatible = groupInfo.area !== 0 && groupInfo.areaUnit !== AreaUnitType.none; } @@ -52,7 +50,7 @@ export const selectThreeDQueryArgs = createSelector( selectThreeDMeterOrGroup, (id, timeInterval, unitID, readingInterval, meterOrGroup) => { return { - meterID: id, + meterOrGroupID: id, timeInterval: roundTimeIntervalForFetch(timeInterval).toString(), unitID: unitID, readingInterval: readingInterval, diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts index 748728e23..144ec674f 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -4,17 +4,20 @@ import { createSelector } from '@reduxjs/toolkit'; import * as _ from 'lodash'; -import { getSelectOptionsByItem } from '../../components/ChartDataSelectComponent'; import { RootState } from '../../store'; import { DataType } from '../../types/Datasources'; -import { ChartTypes } from '../../types/redux/graph'; -import { DisplayableType, UnitData, UnitRepresentType, UnitType } from '../../types/redux/units'; +import { ChartTypes, MeterOrGroup } from '../../types/redux/graph'; +import { DisplayableType, UnitData, UnitRepresentType, UnitType, UnitsState } from '../../types/redux/units'; import { CartesianPoint, Dimensions, calculateScaleFromEndpoints, gpsToUserGrid, itemDisplayableOnMap, itemMapInfoOk, normalizeImageDimensions } from '../../utils/calibration'; import { metersInGroup, unitsCompatibleWithMeters } from '../../utils/determineCompatibleUnits'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; +import { instanceOfGroupsState, instanceOfMetersState, instanceOfUnitsState } from '../../components/ChartDataSelectComponent'; +import { SelectOption, GroupedOption } from '../../types/items'; +import { MetersState } from '../../types/redux/meters'; +import { GroupsState } from '../../types/redux/groups'; export const selectSelectedMeters = (state: RootState) => state.graph.selectedMeters; @@ -24,7 +27,6 @@ export const selectGraphTimeInterval = (state: RootState) => state.graph.timeInt export const selectGraphUnitID = (state: RootState) => state.graph.selectedUnit; export const selectGraphAreaNormalization = (state: RootState) => state.graph.areaNormalization; export const selectChartToRender = (state: RootState) => state.graph.chartToRender; - export const selectMeterState = (state: RootState) => state.meters; export const selectGroupState = (state: RootState) => state.groups; export const selectUnitState = (state: RootState) => state.units; @@ -147,42 +149,42 @@ export const selectMeterGroupUnitCompatibility = createSelector( } ) -export const selectMeterGroupAreaAndMapCompatibility = createSelector( +export const selectMeterGroupStateCompatability = createSelector( selectMeterGroupUnitCompatibility, selectGraphAreaNormalization, selectChartToRender, selectMeterState, selectGroupState, selectMapState, - (unitCompat, areaNormalization, chartToRender, meterState, groupState, mapState) => { - // store meters which are found to be incompatible. - const incompatibleMeters = new Set(); - const incompatibleGroups = new Set(); - - const compatibleMeters = new Set(); - const compatibleGroups = new Set(); + selectSelectedMeters, + selectSelectedGroups, + selectGraphUnitID, + (unitCompat, areaNormalization, chartToRender, meterState, groupState, mapState, selectedMeters, selectedGroups, selectedUnitID) => { + // Deep Copy previous selector's values, and update as needed based on current state, like area norm, and map, etc. + const currentIncompatibleMeters = new Set(Array.from(unitCompat.incompatibleMeters)); + const currentIncompatibleGroups = new Set(Array.from(unitCompat.incompatibleGroups)); + const currentCompatibleMeters = new Set(Array.from(unitCompat.compatibleMeters)); + const currentCompatibleGroups = new Set(Array.from(unitCompat.compatibleGroups)); // only run this check if area normalization is on if (areaNormalization) { - // filter out any meter or group that is area incompatible. unitCompat.compatibleMeters.forEach(meterID => { // do not allow meter to be selected if it has zero area or no area unit if (meterState.byMeterID[meterID].area === 0 || meterState.byMeterID[meterID].areaUnit === AreaUnitType.none) { - incompatibleMeters.add(meterID); - } else { - compatibleMeters.add(meterID); + currentCompatibleMeters.delete(meterID) + currentIncompatibleMeters.add(meterID); } }); unitCompat.compatibleGroups.forEach(groupID => { // do not allow group to be selected if it has zero area or no area unit if (groupState.byGroupID[groupID].area === 0 || groupState.byGroupID[groupID].areaUnit === AreaUnitType.none) { - incompatibleGroups.add(groupID); - } else { - compatibleGroups.add(groupID); + currentIncompatibleGroups.add(groupID); + currentCompatibleGroups.delete(groupID); } }); } + // ony run this check if we are displaying a map chart if (chartToRender === ChartTypes.map && mapState.selectedMap !== 0) { const mp = mapState.byMapID[mapState.selectedMap]; // filter meters; @@ -221,15 +223,16 @@ export const selectMeterGroupAreaAndMapCompatibility = createSelector( const meterGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, mp.northAngle); if (!(itemMapInfoOk(meterID, DataType.Meter, mp, gps) && itemDisplayableOnMap(imageDimensionNormalized, meterGPSInUserGrid))) { - incompatibleMeters.add(meterID); - } else { - compatibleMeters.add(meterID); + currentIncompatibleMeters.add(meterID); + currentCompatibleMeters.delete(meterID); } } else { // Lack info on this map so skip. This is mostly done since TS complains about the undefined possibility. - incompatibleMeters.add(meterID); + currentIncompatibleMeters.add(meterID); + currentCompatibleMeters.delete(meterID); } }); + // The below code follows the logic for meters shown above. See comments above for clarification on the below code. unitCompat.compatibleGroups.forEach(groupID => { const gps = groupState.byGroupID[groupID].gps; @@ -238,32 +241,112 @@ export const selectMeterGroupAreaAndMapCompatibility = createSelector( const groupGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, mp.northAngle); if (!(itemMapInfoOk(groupID, DataType.Group, mp, gps) && itemDisplayableOnMap(imageDimensionNormalized, groupGPSInUserGrid))) { - incompatibleGroups.add(groupID); - } else { - compatibleGroups.add(groupID); + currentIncompatibleGroups.add(groupID); + currentCompatibleGroups.delete(groupID); } } else { - incompatibleGroups.add(groupID); + currentIncompatibleGroups.add(groupID); + currentCompatibleGroups.delete(groupID); } }); } - return { compatibleMeters, incompatibleMeters, compatibleGroups, incompatibleGroups } + + // Calculate final compatible meters and groups for dropdown + const compatibleSelectedMeters = new Set(); + selectedMeters.forEach(meterID => { + // don't include meters that can't be graphed with current settings + if (!currentIncompatibleMeters.has(meterID)) { + compatibleSelectedMeters.add(meterID); + if (selectedUnitID == -99) { + // dispatch(changeSelectedUnit(state.meters.byMeterID[meterID].defaultGraphicUnit)); + console.log('TODO FIX ME. MOVE ME TO SELECT LOGIC THERE should be no dispatches inside of selectors') + // If the selected unit is -99 then there is not graphic unit yet. In this case you can only select a + // meter that has a default graphic unit because that will become the selected unit. This should only + // happen if no meter or group is yet selected. + // If no unit is set then this should always be the first meter (or group) selected. + // The selectedUnit becomes the unit of the meter selected. Note is should always be set (not -99) since + // those meters should not have been visible. The only exception is if there are no selected meters but + // then this loop does not run. The loop is assumed to only run once in this case. + } + } + }); + + + const compatibleSelectedGroups = new Set(); + selectedGroups.forEach(groupID => { + // don't include groups that can't be graphed with current settings + if (!currentIncompatibleGroups.has(groupID)) { + // If the selected unit is -99 then there is no graphic unit yet. In this case you can only select a + // group that has a default graphic unit because that will become the selected unit. This should only + // happen if no meter or group is yet selected. + if (selectedUnitID == -99) { + // If no unit is set then this should always be the first group (or meter) selected. + // The selectedUnit becomes the unit of the group selected. Note is should always be set (not -99) since + // those groups should not have been visible. The only exception is if there are no selected groups but + // then this loop does not run. The loop is assumed to only run once in this case. + // dispatch(changeSelectedUnit(state.groups.byGroupID[groupID].defaultGraphicUnit)); + console.log('TODO FIX ME. MOVE ME TO graphSliceLogic LOGIC THERE should be no dispatches inside of selectors') + + } + compatibleSelectedGroups.add(groupID); + } + }); + // console.log(compatibleSelectedMeters, currentIncompatibleMeters, compatibleSelectedGroups, currentIncompatibleGroups) + + return { + compatibleSelectedMeters, + compatibleSelectedGroups, + currentCompatibleMeters, + currentCompatibleGroups, + currentIncompatibleMeters, + currentIncompatibleGroups + } } ) export const selectMeterGroupSelectData = createSelector( - selectMeterGroupAreaAndMapCompatibility, + selectMeterGroupStateCompatability, selectMeterState, selectGroupState, (stateCompatibility, meterState, groupState) => { - // Retrieve select options from meter sets - const meterSelectOption = getSelectOptionsByItem(stateCompatibility.compatibleMeters, stateCompatibility.incompatibleMeters, meterState); - // Retrieve select options from group sets - const groupSelectOption = getSelectOptionsByItem(stateCompatibility.compatibleGroups, stateCompatibility.incompatibleGroups, groupState); + // The Multiselect's current selected value(s) + const compatibleSelectedMeters = getSelectOptionsByItem(stateCompatibility.compatibleSelectedMeters, true, meterState) + const compatibleSelectedGroups = getSelectOptionsByItem(stateCompatibility.compatibleSelectedGroups, true, groupState) - return { meterSelectOption, groupSelectOption } + // The Multiselect's options are grouped as compatible and imcompatible. + // get pairs + const currentCompatibleMeters = getSelectOptionsByItem(stateCompatibility.currentCompatibleMeters, true, meterState) + const currentIncompatibleMeters = getSelectOptionsByItem(stateCompatibility.currentIncompatibleMeters, false, meterState) + + const currentCompatibleGroups = getSelectOptionsByItem(stateCompatibility.currentCompatibleGroups, true, groupState) + const currentIncompatibleGroups = getSelectOptionsByItem(stateCompatibility.currentIncompatibleGroups, false, groupState) + + + const meterGroupedOptions: GroupedOption[] = [ + { + label: 'Meters', + options: currentCompatibleMeters + }, + { + label: 'Incompatible Meters', + options: currentIncompatibleMeters + } + ] + const groupsGroupedOptions: GroupedOption[] = [ + { + label: 'Options', + options: currentCompatibleGroups + }, + { + label: 'Incompatible Options', + options: currentIncompatibleGroups + } + ] + console.log('Where Am i Even', meterGroupedOptions, groupsGroupedOptions); + return { meterGroupedOptions, groupsGroupedOptions, compatibleSelectedMeters, compatibleSelectedGroups } } ) + /** * Filters all units that are of type meter or displayable type none from the redux state, as well as admin only units if the user is not an admin. * @param state - current redux state @@ -346,8 +429,19 @@ export const selectUnitSelectData = createSelector( }); } // Ready to display unit. Put selectable ones before non-selectable ones. - const finalUnits = getSelectOptionsByItem(compatibleUnits, incompatibleUnits, unitState); - return finalUnits; + const compatibleUnitsOptions = getSelectOptionsByItem(compatibleUnits, true, unitState); + const incompatibleUnitsOptions = getSelectOptionsByItem(incompatibleUnits, false, unitState); + const unitsGroupedOptions: GroupedOption[] = [ + { + label: 'Units', + options: compatibleUnitsOptions + }, + { + label: 'Incompatible Units', + options: incompatibleUnitsOptions + } + ] + return unitsGroupedOptions } ) @@ -382,4 +476,58 @@ export const selectMeterGroupAreaCompatibility = createSelector( return { compatibleMeters, incompatibleMeters, compatibleGroups, incompatibleGroups } } ) +/** + * Returns a set of SelectOptions based on the type of state passed in and sets the visibility. + * Visibility is determined by which set the items are contained in. + * @param items - items to retrieve select options for + * @param isCompatible - determines the group option + * @param state - current redux state, must be one of UnitsState, MetersState, or GroupsState + * @returns list of selectOptions of the given item + */ +export function getSelectOptionsByItem(items: Set, isCompatible: boolean, state: UnitsState | MetersState | GroupsState) { + // TODO Refactor origina + // redefined here for testing. + // Holds the label of the select item, set dynamically according to the type of item passed in + let label = ''; + let meterOrGroup: MeterOrGroup | undefined; + + //The final list of select options to be displayed + const itemOptions: SelectOption[] = []; + + //Loop over each itemId and create an activated select option + items.forEach(itemId => { + // Perhaps in the future this can be done differently + // Loop over the state type to see what state was passed in (units, meter, group, etc) + // Set the label correctly based on the type of state + // If this is converted to a switch statement the instanceOf function needs to be called twice + // Once for the initial state type check, again because the interpreter (for some reason) needs to be sure that the property exists in the object + // If else statements do not suffer from this + if (instanceOfUnitsState(state)) { + label = state.units[itemId].identifier; + } + else if (instanceOfMetersState(state)) { + label = state.byMeterID[itemId].identifier; + meterOrGroup = MeterOrGroup.meters + } + else if (instanceOfGroupsState(state)) { + label = state.byGroupID[itemId].name; + meterOrGroup = MeterOrGroup.groups + } + else { label = ''; } + // TODO This is a bit of a hack. When an admin logs in they may not have the new state so label is null. + // This should clear once the state is loaded. + label = label === null ? '' : label; + itemOptions.push({ + value: itemId, + label: label, + // If option is compatible then ! not disabled + isDisabled: !isCompatible, + meterOrGroup: meterOrGroup + } as SelectOption + ); + }); + const sortedOptions = _.sortBy(itemOptions, item => item.label.toLowerCase(), 'asc') + + return sortedOptions +} diff --git a/src/client/app/types/items.ts b/src/client/app/types/items.ts index 649794de2..3f629bca6 100644 --- a/src/client/app/types/items.ts +++ b/src/client/app/types/items.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 { ChartTypes } from '../types/redux/graph'; +import { ChartTypes, MeterOrGroup } from '../types/redux/graph'; import { LanguageTypes } from './redux/i18n'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; @@ -15,6 +15,11 @@ export interface SelectOption { isDisabled?: boolean; labelIdForTranslate?: string; style?: React.CSSProperties; + meterOrGroup?: MeterOrGroup +} +export interface GroupedOption { + label: string; + options: SelectOption[] } /** diff --git a/src/client/app/types/redux/graph.ts b/src/client/app/types/redux/graph.ts index d7706f513..5febb78c2 100644 --- a/src/client/app/types/redux/graph.ts +++ b/src/client/app/types/redux/graph.ts @@ -44,14 +44,14 @@ export interface LineGraphRate { rate: number } -export type MeterOrGroupID = number | null; +export type MeterOrGroupID = number; export enum MeterOrGroup { meters = 'meters', groups = 'groups' } export enum ByMeterOrGroup { meters = 'byMeterID', groups = 'byGroupID' } -export type MeterOrGroupPill = {meterOrGroupID: number, isDisabled: boolean, meterOrGroup: MeterOrGroup} +export type MeterOrGroupPill = { meterOrGroupID: number, isDisabled: boolean, meterOrGroup: MeterOrGroup } export interface ThreeDState { - meterOrGroupID: MeterOrGroupID; - meterOrGroup: MeterOrGroup; + meterOrGroupID: MeterOrGroupID | undefined; + meterOrGroup: MeterOrGroup | undefined; readingInterval: ReadingInterval; } From c8acbe864ec889b60f3e3c8bcc57daa6dbc04e38 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Thu, 28 Sep 2023 18:27:14 +0000 Subject: [PATCH 015/131] Mutations React Router Bump --- package-lock.json | 48 +++ package.json | 1 + .../components/ChartDataSelectComponent.tsx | 42 +-- .../app/components/HeaderButtonsComponent.tsx | 2 +- src/client/app/components/HeaderComponent.tsx | 2 +- .../components/InitializationComponent.tsx | 49 +-- src/client/app/components/LoginComponent.tsx | 188 ++++------ .../MeterAndGroupSelectComponent.tsx | 8 +- .../app/components/RouteComponentWIP.tsx | 205 +++++++++++ .../app/components/UnitSelectComponent.tsx | 38 +- .../components/UnsavedWarningComponent.tsx | 3 +- .../components/admin/PreferencesComponent.tsx | 2 +- .../components/admin/UsersDetailComponent.tsx | 2 +- .../users/CreateUserLinkButtonComponent.tsx | 2 +- .../users/ManageUsersLinkButtonComponent.tsx | 2 +- .../csv/MetersCSVUploadComponent.tsx | 2 +- .../app/components/maps/MapViewComponent.tsx | 4 +- .../components/maps/MapsDetailComponent.tsx | 22 +- .../unit/EditUnitModalComponent.tsx | 2 +- .../MapCalibrationChartDisplayContainer.ts | 2 +- src/client/app/index.tsx | 16 +- src/client/app/reducers/currentUser.ts | 64 ++-- src/client/app/reducers/graph.ts | 30 +- src/client/app/reducers/groups.ts | 1 + src/client/app/redux/api/authApi.ts | 64 ++++ src/client/app/redux/api/baseApi.ts | 16 +- src/client/app/redux/api/groupsApi.ts | 3 +- src/client/app/redux/api/metersApi.ts | 5 +- src/client/app/redux/api/userApi.ts | 14 + .../app/redux/selectors/authSelectors.ts | 2 + src/client/app/redux/selectors/uiSelectors.ts | 345 +++++++++--------- src/client/app/types/items.ts | 3 +- src/client/app/types/redux/currentUser.ts | 19 +- .../app/utils/determineCompatibleUnits.ts | 2 +- src/client/app/utils/token.ts | 7 + src/client/app/utils/translate.ts | 7 +- 36 files changed, 754 insertions(+), 470 deletions(-) create mode 100644 src/client/app/components/RouteComponentWIP.tsx create mode 100644 src/client/app/redux/api/authApi.ts create mode 100644 src/client/app/redux/api/userApi.ts create mode 100644 src/client/app/redux/selectors/authSelectors.ts diff --git a/package-lock.json b/package-lock.json index df101d435..7d522dd1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "react-plotly.js": "~2.6.0", "react-redux": "~8.1.2", "react-router-dom": "~5.3.0", + "react-router-dom-v5-compat": "~6.16.0", "react-select": "~5.7.4", "react-toastify": "~9.1.3", "react-tooltip": "~4.2.20", @@ -2559,6 +2560,14 @@ } } }, + "node_modules/@remix-run/router": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.9.0.tgz", + "integrity": "sha512-bV63itrKBC0zdT27qYm6SDZHlkXwFL1xMBuhkn+X7l0+IIhNaH5wuuvZKp6eKhCD4KFhujhfhCT1YxXW6esUIA==", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@sinonjs/commons": { "version": "1.8.6", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", @@ -10383,6 +10392,45 @@ "react": ">=15" } }, + "node_modules/react-router-dom-v5-compat": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/react-router-dom-v5-compat/-/react-router-dom-v5-compat-6.16.0.tgz", + "integrity": "sha512-MfjB9qYZVnWUEHENFa+XpVU5qbDPkqpGvjaF/8nHCnCkfy5pxYajrZrmRmaR4v6Ehtu7GAx59JFTSoQGhLu1+g==", + "dependencies": { + "history": "^5.3.0", + "react-router": "6.16.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8", + "react-router-dom": "4 || 5" + } + }, + "node_modules/react-router-dom-v5-compat/node_modules/history": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", + "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", + "dependencies": { + "@babel/runtime": "^7.7.6" + } + }, + "node_modules/react-router-dom-v5-compat/node_modules/react-router": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.16.0.tgz", + "integrity": "sha512-VT4Mmc4jj5YyjpOi5jOf0I+TYzGpvzERy4ckNSvSh2RArv8LLoCxlsZ2D+tc7zgjxcY34oTz2hZaeX5RVprKqA==", + "dependencies": { + "@remix-run/router": "1.9.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, "node_modules/react-router-dom/node_modules/history": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", diff --git a/package.json b/package.json index 0f1d6db8d..ee2811142 100644 --- a/package.json +++ b/package.json @@ -105,6 +105,7 @@ "react-plotly.js": "~2.6.0", "react-redux": "~8.1.2", "react-router-dom": "~5.3.0", + "react-router-dom-v5-compat": "~6.16.0", "react-select": "~5.7.4", "react-toastify": "~9.1.3", "react-tooltip": "~4.2.20", diff --git a/src/client/app/components/ChartDataSelectComponent.tsx b/src/client/app/components/ChartDataSelectComponent.tsx index d56d0a7f0..3f8ff9515 100644 --- a/src/client/app/components/ChartDataSelectComponent.tsx +++ b/src/client/app/components/ChartDataSelectComponent.tsx @@ -4,31 +4,31 @@ import * as _ from 'lodash'; import * as React from 'react'; -import MultiSelectComponent from './MultiSelectComponent'; -import { SelectOption } from '../types/items'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import TooltipMarkerComponent from './TooltipMarkerComponent'; -import { useSelector, useDispatch } from 'react-redux'; -import { State } from '../types/redux/state'; -import { ChartTypes, MeterOrGroup } from '../types/redux/graph'; +import { useDispatch, useSelector } from 'react-redux'; +import { GroupsState } from 'types/redux/groups'; +import { MetersState } from 'types/redux/meters'; +import { changeMeterOrGroupInfo, changeSelectedGroups, changeSelectedMeters, changeSelectedUnit } from '../actions/graph'; +import { graphSlice } from '../reducers/graph'; import { DataType } from '../types/Datasources'; +import { SelectOption } from '../types/items'; +import { Dispatch } from '../types/redux/actions'; +import { ChartTypes, MeterOrGroup } from '../types/redux/graph'; +import { State } from '../types/redux/state'; +import { DisplayableType, UnitData, UnitRepresentType, UnitsState, UnitType } from '../types/redux/units'; import { - CartesianPoint, Dimensions, normalizeImageDimensions, calculateScaleFromEndpoints, - itemDisplayableOnMap, itemMapInfoOk, gpsToUserGrid + calculateScaleFromEndpoints, + CartesianPoint, Dimensions, + gpsToUserGrid, + itemDisplayableOnMap, itemMapInfoOk, + normalizeImageDimensions } from '../utils/calibration'; -import { changeSelectedGroups, changeSelectedMeters, changeSelectedUnit, changeMeterOrGroupInfo } from '../actions/graph'; -import { DisplayableType, UnitData, UnitRepresentType, UnitType } from '../types/redux/units' import { metersInGroup, unitsCompatibleWithMeters } from '../utils/determineCompatibleUnits'; -import { Dispatch } from '../types/redux/actions'; -import { UnitsState } from '../types/redux/units'; -import { MetersState } from 'types/redux/meters'; -import { GroupsState } from 'types/redux/groups'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; -import { graphSlice } from '../reducers/graph'; -import UnitSelectComponent from './UnitSelectComponent'; import MeterAndGroupSelectComponent from './MeterAndGroupSelectComponent'; -import { useAppSelector } from '../redux/hooks'; -import { selectMeterGroupSelectData } from '../redux/selectors/uiSelectors'; +import MultiSelectComponent from './MultiSelectComponent'; +import TooltipMarkerComponent from './TooltipMarkerComponent'; +import UnitSelectComponent from './UnitSelectComponent'; /** * A component which allows the user to select which data should be displayed on the chart. @@ -38,7 +38,6 @@ export default function ChartDataSelectComponent() { // Must specify type if using ThunkDispatch const dispatch: Dispatch = useDispatch(); const intl = useIntl(); - const selectTestOpts = useAppSelector(state => selectMeterGroupSelectData(state)) const dataProps = useSelector((state: State) => { const allMeters = state.meters.byMeterID; const allGroups = state.groups.byGroupID; @@ -272,10 +271,7 @@ export default function ChartDataSelectComponent() { threeDState } }); - console.log('HERE WE ARE') - console.log(dataProps.sortedGroups) - console.log(dataProps.compatibleSelectedGroups) - console.log(selectTestOpts) + return (

diff --git a/src/client/app/components/HeaderButtonsComponent.tsx b/src/client/app/components/HeaderButtonsComponent.tsx index 303e20c65..33d97f9cc 100644 --- a/src/client/app/components/HeaderButtonsComponent.tsx +++ b/src/client/app/components/HeaderButtonsComponent.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { useState, useEffect } from 'react'; -import { Link } from 'react-router-dom'; +import { Link } from 'react-router-dom-v5-compat'; import { FormattedMessage } from 'react-intl'; import getPage from '../utils/getPage'; import translate from '../utils/translate'; diff --git a/src/client/app/components/HeaderComponent.tsx b/src/client/app/components/HeaderComponent.tsx index 95600e890..fb4665050 100644 --- a/src/client/app/components/HeaderComponent.tsx +++ b/src/client/app/components/HeaderComponent.tsx @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { Link } from 'react-router-dom'; +import { Link } from 'react-router-dom-v5-compat'; import LogoComponent from './LogoComponent'; import MenuModalComponent from './MenuModalComponent'; import HeaderButtonsComponent from './HeaderButtonsComponent'; diff --git a/src/client/app/components/InitializationComponent.tsx b/src/client/app/components/InitializationComponent.tsx index f249a03fc..9b2e53ea5 100644 --- a/src/client/app/components/InitializationComponent.tsx +++ b/src/client/app/components/InitializationComponent.tsx @@ -4,30 +4,45 @@ import * as React from 'react'; import { useEffect } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { State } from '../types/redux/state'; -import { ConversionArray } from '../types/conversionArray'; +import { useDispatch } from 'react-redux'; +import { Slide, ToastContainer } from 'react-toastify'; +import 'react-toastify/dist/ReactToastify.css'; +import { Dispatch } from 'types/redux/actions'; import { fetchPreferencesIfNeeded } from '../actions/admin'; +import { fetchConversionsDetailsIfNeeded } from '../actions/conversions'; import { fetchMapsDetails } from '../actions/map'; import { fetchUnitsDetailsIfNeeded } from '../actions/units'; -import { fetchConversionsDetailsIfNeeded } from '../actions/conversions'; -import { Dispatch } from 'types/redux/actions'; -import { Slide, ToastContainer } from 'react-toastify'; -import { metersApi } from '../redux/api/metersApi'; import { groupsApi } from '../redux/api/groupsApi'; -import 'react-toastify/dist/ReactToastify.css'; +import { metersApi } from '../redux/api/metersApi'; +// import { userApi } from '../redux/api/userApi'; +import { authApi } from '../redux/api/authApi'; +import { ConversionArray } from '../types/conversionArray'; +import { getToken, hasToken } from '../utils/token'; /** * Initializes OED redux with needed details * @returns Initialization JSX element */ export default function InitializationComponent() { - const dispatch: Dispatch = useDispatch(); - const { refetch: refetchMeters } = metersApi.endpoints.getMeters.useQuery(); - groupsApi.endpoints.getGroups.useQuery(); + // QueryHooks derived by api endpoint definitions + // These useQuery hooks subscribe to the store, and automatically fetch and cache data to the store. + metersApi.useGetMetersQuery(); + // metersApi.endpoints.getMeters.useQuery(); Another way to access the same hooks + groupsApi.useGetGroupsQuery(); + // groupsApi.endpoints.getGroups.useQuery(); Another way to access the same hook + const [verifyTokenTrigger] = authApi.useVerifyTokenMutation() + + // There are many derived hooks each with different use cases. Read More @ https://redux-toolkit.js.org/rtk-query/api/created-api/hooks#hooks-overview + // Only run once by making it depend on an empty array. useEffect(() => { + // If user has token from prior logins verify, and fetch user details if valid. + if (hasToken()) { + // use the verify token mutation, + verifyTokenTrigger(getToken()) + } + dispatch(fetchPreferencesIfNeeded()); dispatch(fetchMapsDetails()); dispatch(fetchUnitsDetailsIfNeeded()); @@ -35,20 +50,8 @@ export default function InitializationComponent() { ConversionArray.fetchPik(); }, []); - // Rerender the route component if the user state changes - // This is necessary because of how the meters route works - // If the user is not an admin, the formatMeterForResponse function sets many of the fetched values to null - // Because of this must re-fetch the entire meters table if the user changes - const currentUser = useSelector((state: State) => state.currentUser.profile); - useEffect(() => { - // TODO REDO WITH TAG INVALIDATION AND PROPER AUTH HEADERS - refetchMeters() - // dispatch(fetchMetersDetails()); - }, [currentUser]); - return (

- {/* { notificationSystem = c; }} /> */}
); diff --git a/src/client/app/components/LoginComponent.tsx b/src/client/app/components/LoginComponent.tsx index dbef4f341..bfc9fe791 100644 --- a/src/client/app/components/LoginComponent.tsx +++ b/src/client/app/components/LoginComponent.tsx @@ -3,137 +3,83 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { browserHistory } from '../utils/history'; -import { injectIntl, FormattedMessage, WrappedComponentProps } from 'react-intl'; -import { Input, Button, Form, Label, FormGroup } from 'reactstrap'; +import { useRef, useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Button, Form, FormGroup, Input, Label } from 'reactstrap'; import FooterContainer from '../containers/FooterContainer'; +import { authApi } from '../redux/api/authApi'; import { showErrorNotification } from '../utils/notifications'; -import { verificationApi } from '../utils/api'; import translate from '../utils/translate'; -import { User } from '../types/items'; import HeaderComponent from './HeaderComponent'; +import { useNavigate } from 'react-router-dom-v5-compat'; -interface LoginState { - email: string; - password: string; -} - -interface LoginProps { - saveCurrentUser(profile: User): any; -} - -type LoginPropsWithIntl = LoginProps & WrappedComponentProps; - -class LoginComponent extends React.Component { - private inputEmail: HTMLInputElement | HTMLTextAreaElement | null; - - constructor(props: LoginPropsWithIntl) { - super(props); - this.state = { email: '', password: '' }; - this.handleEmailChange = this.handleEmailChange.bind(this); - this.handlePasswordChange = this.handlePasswordChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.saveCurrentUser = this.saveCurrentUser.bind(this); - } - - /** - * @returns JSX to create the login panel - */ - public render() { - const formStyle = { - maxWidth: '500px', - margin: 'auto', - width: '50%' - }; +/** + * @returns The login page for users or admins. + */ +export default function LoginComponent() { + const navigate = useNavigate(); - return ( -
- -
- - - { this.inputEmail = c; }} - value={this.state.email} - onChange={this.handleEmailChange} - /> - - - - - - - - -
- ); - } + // Local State + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); - /** - * Sets the email state whenever the user changes the email input field - * @param e The event fired - */ - private handleEmailChange(e: React.ChangeEvent) { - this.setState({ email: e.target.value }); - } + // Html Element Reference used for focus() + const inputRef = useRef(null); - /** - * Sets the password state whenever the user changes the password input field - * @param e The event fired - */ - private handlePasswordChange(e: React.ChangeEvent) { - this.setState({ password: e.target.value }); - } + // Grab the derived loginMutation from the API + // The naming of the returned objects is arbitrary + const [login] = authApi.useLoginMutation() - private saveCurrentUser(profile: User) { - this.props.saveCurrentUser(profile); + const handleSubmit = async (event: React.MouseEvent) => { + event.preventDefault(); + const response = await login({ email: 'test@example.com', password: 'password' }).unwrap() + console.log('response ', response) + inputRef.current?.focus() + showErrorNotification(translate('failed.logging.in')); + navigate('/') } - /** - * Makes a GET request to the login api whenever the user click the submit button, then clears the state - * If the request is successful, the JWT auth token is stored in local storage and the app routes to the admin page - * @param e The event fired - */ - private handleSubmit(e: React.MouseEvent) { - e.preventDefault(); - (async () => { - try { - const loginResponse = await verificationApi.login(this.state.email, this.state.password); - localStorage.setItem('token', loginResponse.token); - this.saveCurrentUser({ email: loginResponse.email, role: loginResponse.role }); - browserHistory.push('/'); - } catch (err) { - if (err.response && err.response.status === 401) { - showErrorNotification(translate('invalid.email.password')); - } else { - // If there was a problem other than a lack of authorization, the user can't fix it. - // This is an irrecoverable state, so just throw an error and let the user know something went wrong - showErrorNotification(translate('failed.logging.in')); - throw err; - } - if (this.inputEmail !== null) { - this.inputEmail.focus(); - } - } - })(); - this.setState({ email: '', password: '' }); - } + return ( +
+ +
+ + + setEmail(e.target.value)} + /> + + + + setPassword(e.target.value)} + innerRef={inputRef} + /> + + + + +
+ ) } -export default injectIntl(LoginComponent); \ No newline at end of file +const formStyle = { + maxWidth: '500px', + margin: 'auto', + width: '50%' +} \ No newline at end of file diff --git a/src/client/app/components/MeterAndGroupSelectComponent.tsx b/src/client/app/components/MeterAndGroupSelectComponent.tsx index 40efea398..f2a36bf88 100644 --- a/src/client/app/components/MeterAndGroupSelectComponent.tsx +++ b/src/client/app/components/MeterAndGroupSelectComponent.tsx @@ -35,9 +35,9 @@ export default function MeterAndGroupSelectComponent(props: MeterAndGroupSelectP graphSlice.actions.updateSelectedGroupsFromSelect const value = meterOrGroup === MeterOrGroup.meters ? - meterAndGroupSelectOptions.compatibleSelectedMeters + meterAndGroupSelectOptions.selectedMeterValues : - meterAndGroupSelectOptions.compatibleSelectedGroups + meterAndGroupSelectOptions.selectedGroupValues // Set the current component's appropriate meter or group SelectOption const options = meterOrGroup === MeterOrGroup.meters ? @@ -46,13 +46,12 @@ export default function MeterAndGroupSelectComponent(props: MeterAndGroupSelectP meterAndGroupSelectOptions.groupsGroupedOptions const onChange = (newValues: MultiValue, meta: ActionMeta) => { - console.log('newValues', newValues, 'meta', meta); const newMetersOrGroups = newValues.map((option: SelectOption) => option.value); dispatch(updateSelectedMetersOrGroups({ newMetersOrGroups, meta })) } return ( - value={selectedUnitOption} options={unitSelectOptions} onChange={onChange} - isClearable={true} + formatGroupLabel={formatGroupLabel} + isClearable /> ) } +const groupStyles: React.CSSProperties = { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between' +}; + +const formatGroupLabel = (data: GroupedOption) => { + return ( + < div style={groupStyles} > + {data.label} + {data.options.length} +
+ + ) +} {/* { private fileInput: React.RefObject; diff --git a/src/client/app/components/maps/MapViewComponent.tsx b/src/client/app/components/maps/MapViewComponent.tsx index 8c9a2b061..3165e31a2 100644 --- a/src/client/app/components/maps/MapViewComponent.tsx +++ b/src/client/app/components/maps/MapViewComponent.tsx @@ -4,12 +4,12 @@ import * as React from 'react'; import { Button } from 'reactstrap'; -import { Link } from 'react-router-dom'; +import { Link } from 'react-router-dom-v5-compat'; import { hasToken } from '../../utils/token'; import { FormattedMessage, injectIntl, WrappedComponentProps } from 'react-intl'; import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map'; import * as moment from 'moment'; -import store from '../../index'; +import { store } from '../../store'; import { fetchMapsDetails, submitEditedMaps, confirmEditedMaps } from '../../actions/map'; import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; diff --git a/src/client/app/components/maps/MapsDetailComponent.tsx b/src/client/app/components/maps/MapsDetailComponent.tsx index b6be2009a..a49d91812 100644 --- a/src/client/app/components/maps/MapsDetailComponent.tsx +++ b/src/client/app/components/maps/MapsDetailComponent.tsx @@ -8,10 +8,10 @@ import { FormattedMessage } from 'react-intl'; import { hasToken } from '../../utils/token'; import FooterContainer from '../../containers/FooterContainer'; import MapViewContainer from '../../containers/maps/MapViewContainer'; -import {Link} from 'react-router-dom'; +import { Link } from 'react-router-dom-v5-compat'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import store from '../../index'; +import { store } from '../../store'; import UnsavedWarningContainer from '../../containers/UnsavedWarningContainer'; import HeaderComponent from '../../components/HeaderComponent'; import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; @@ -77,17 +77,17 @@ export default class MapsDetailComponent extends React.Component
{hasToken() && } - {hasToken() && } + {hasToken() && } {hasToken() && } - {hasToken() && } - {hasToken() && } - {hasToken() && } - {hasToken() && } + {hasToken() && } + {hasToken() && } + {hasToken() && } + {hasToken() && } - { this.props.maps.map(mapID => - ( ))} + {this.props.maps.map(mapID => + ())}
this.props.createNewMap()}> @@ -100,14 +100,14 @@ export default class MapsDetailComponent extends React.Component

- { hasToken() && } + }
diff --git a/src/client/app/components/unit/EditUnitModalComponent.tsx b/src/client/app/components/unit/EditUnitModalComponent.tsx index fe96be549..6ff870708 100644 --- a/src/client/app/components/unit/EditUnitModalComponent.tsx +++ b/src/client/app/components/unit/EditUnitModalComponent.tsx @@ -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 * as React from 'react'; -import store from '../../index'; +import {store} from '../../store'; //Realize that * is already imported from react import { useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; diff --git a/src/client/app/containers/maps/MapCalibrationChartDisplayContainer.ts b/src/client/app/containers/maps/MapCalibrationChartDisplayContainer.ts index f0f358790..1d8ee2980 100644 --- a/src/client/app/containers/maps/MapCalibrationChartDisplayContainer.ts +++ b/src/client/app/containers/maps/MapCalibrationChartDisplayContainer.ts @@ -8,7 +8,7 @@ import { State } from '../../types/redux/state'; import * as plotly from 'plotly.js'; import { CartesianPoint, Dimensions, normalizeImageDimensions } from '../../utils/calibration'; import { updateCurrentCartesian } from '../../actions/map'; -import store from '../../index'; +import {store} from '../../store'; import { CalibrationSettings } from '../../types/redux/map'; import Locales from '../../types/locales' diff --git a/src/client/app/index.tsx b/src/client/app/index.tsx index 8187bdec6..834ffb26a 100644 --- a/src/client/app/index.tsx +++ b/src/client/app/index.tsx @@ -7,24 +7,20 @@ import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux'; import { store } from './store' import 'bootstrap/dist/css/bootstrap.css'; -import RouteContainer from './containers/RouteContainer'; +// import RouteContainer from './containers/RouteContainer'; +import RouteComponent from './components/RouteComponentWIP'; import './styles/index.css'; -import initScript from './initScript'; - -// Store information that would rarely change throughout using OED into the Redux store when the application first mounts. -store.dispatch(initScript()); // Renders the entire application, starting with RouteComponent, into the root div -// Provides the Redux store to all child components const container = document.getElementById('root'); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const root = createRoot(container!); root.render( - - // + // Provides the Redux store to all child components - + {/* */} + ); -export default store; diff --git a/src/client/app/reducers/currentUser.ts b/src/client/app/reducers/currentUser.ts index 2ab0b4f3e..0cf2d8d18 100644 --- a/src/client/app/reducers/currentUser.ts +++ b/src/client/app/reducers/currentUser.ts @@ -2,16 +2,22 @@ * 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 { CurrentUserState } from '../types/redux/currentUser'; -import { createSlice } from '@reduxjs/toolkit' -import type { PayloadAction } from '@reduxjs/toolkit' +import type { PayloadAction } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; import { User } from '../types/items'; - +import { CurrentUserState } from '../types/redux/currentUser'; +import { userApi } from '../redux/api/userApi'; +import { authApi } from '../redux/api/authApi'; +import { setToken } from '../utils/token'; /* * Defines store interactions when version related actions are dispatched to the store. */ -const defaultState: CurrentUserState = { isFetching: false, profile: null }; +const defaultState: CurrentUserState = { + isFetching: false, + profile: null, + token: null +}; export const currentUserSlice = createSlice({ name: 'currentUser', @@ -26,31 +32,27 @@ export const currentUserSlice = createSlice({ }, clearCurrentUser: state => { state.profile = null + }, + setUserToken: (state, action: PayloadAction) => { + state.token = action.payload } + }, + extraReducers: builder => { + builder + .addMatcher( + userApi.endpoints.getUserDetails.matchFulfilled, + (state, api) => { + state.profile = api.payload + } + ) + .addMatcher( + authApi.endpoints.login.matchFulfilled, + (state, api) => { + // User has logged in update state, and write to local storage + state.profile = { email: api.payload.email, role: api.payload.role } + state.token = api.payload.token + setToken(state.token) + } + ) } -}) -// export default function profile(state = defaultState, action: CurrentUserAction): CurrentUserState { -// switch (action.type) { -// case ActionType.RequestCurrentUser: -// // When the current user's profile is requested, indicate app is fetching data from API -// return { -// ...state, -// isFetching: true -// }; -// case ActionType.ReceiveCurrentUser: -// // When the current user's profile is received, update the store with result from API -// return { -// ...state, -// isFetching: false, -// profile: action.data -// }; -// case ActionType.ClearCurrentUser: -// // Removes the current user from the redux store. -// return { -// ...state, -// profile: null -// } -// default: -// return state; -// } -// } \ No newline at end of file +}) \ No newline at end of file diff --git a/src/client/app/reducers/graph.ts b/src/client/app/reducers/graph.ts index 86254ed35..4009e8af5 100644 --- a/src/client/app/reducers/graph.ts +++ b/src/client/app/reducers/graph.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 { PayloadAction, createSlice } from '@reduxjs/toolkit'; import * as moment from 'moment'; +import { ActionMeta } from 'react-select'; import { TimeInterval } from '../../../common/TimeInterval'; -import { GraphState, ChartTypes, ReadingInterval, MeterOrGroup, LineGraphRate } from '../types/redux/graph'; -import { calculateCompareTimeInterval, ComparePeriod, SortingOrder } from '../utils/calculateCompare'; +import { SelectOption } from '../types/items'; +import { ChartTypes, GraphState, LineGraphRate, MeterOrGroup, ReadingInterval } from '../types/redux/graph'; +import { ComparePeriod, SortingOrder, calculateCompareTimeInterval } from '../utils/calculateCompare'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; -import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { adminSlice } from './admin'; -import { ActionMeta } from 'react-select'; -import { SelectOption } from '../types/items'; const defaultState: GraphState = { selectedMeters: [], @@ -52,7 +52,16 @@ export const graphSlice = createSlice({ state.selectedGroups = action.payload }, updateSelectedUnit: (state, action: PayloadAction) => { - state.selectedUnit = action.payload + // If Payload is defined, update selectedUnit + if (action.payload) { + state.selectedUnit = action.payload + } else { + // If NewValue is undefined, the current Unit has been cleared + // Reset groups and meters, and selected unit + state.selectedUnit = -99 + state.selectedMeters = [] + state.selectedGroups = [] + } }, updateSelectedAreaUnit: (state, action: PayloadAction) => { state.selectedAreaUnit = action.payload @@ -115,7 +124,6 @@ export const graphSlice = createSlice({ const removedMeterOrGroupID = meta.removedValue?.value; const removedMeterOrGroup = meta.removedValue?.meterOrGroup; const clearedMeterOrGroups = meta.removedValues; - console.log('METAAAAAAAAAAA', meta) // If no meters selected, and no area unit, we should update unit to default graphic unit // const shouldUpdateUnit = !state.selectedGroups.length && !state.selectedMeters.length && state.selectedUnit === -99 @@ -123,15 +131,13 @@ export const graphSlice = createSlice({ // TODO graphic unit is currently snuck into the select option, find an alternative pattern // state.selectedUnit = addedMeterOrGroupID && !shouldUpdateUnit ? state.selectedUnit : meta. - // TODO SELECT bug in reducer // Determine If meter or group was modified then update appropriately const meterOrGroup = addedMeterOrGroup ? addedMeterOrGroup : removedMeterOrGroup; if (clearedMeterOrGroups) { + // use the first index of cleared items to check for meter or group const isAMeter = clearedMeterOrGroups[0].meterOrGroup === MeterOrGroup.meters - isAMeter ? - state.selectedMeters = [] - : - state.selectedGroups = [] + // if a meter clear meters, else clear groups + isAMeter ? state.selectedMeters = [] : state.selectedGroups = [] } else if (meterOrGroup && meterOrGroup === MeterOrGroup.meters) { state.selectedMeters = newMetersOrGroups } else { diff --git a/src/client/app/reducers/groups.ts b/src/client/app/reducers/groups.ts index 2297adefc..a8635e40a 100644 --- a/src/client/app/reducers/groups.ts +++ b/src/client/app/reducers/groups.ts @@ -111,6 +111,7 @@ export const groupsSlice = createSlice({ selectedGroups: [], selectedMeters: [], + // TODO Verify this reducer. // line added due to conflicting typing. TS Warns about potential undefined deepMeters deepMeters: group.deepMeters ? group.deepMeters : [] })); diff --git a/src/client/app/redux/api/authApi.ts b/src/client/app/redux/api/authApi.ts new file mode 100644 index 000000000..376d1d1a3 --- /dev/null +++ b/src/client/app/redux/api/authApi.ts @@ -0,0 +1,64 @@ +import { currentUserSlice } from '../../reducers/currentUser'; +import { User } from '../../types/items'; +import { deleteToken } from '../../utils/token'; +import { baseApi } from './baseApi'; +import { userApi } from './userApi'; + +type LoginResponse = User & { + token: string +}; + +export const authApi = baseApi.injectEndpoints({ + endpoints: builder => ({ + login: builder.mutation({ + query: loginArgs => ({ + url: 'api/login', + method: 'POST', + body: loginArgs + }), + // When this mutation is successful, cache stores with these tags are marked as invalid + // 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'] + }), + verifyToken: builder.mutation({ + query: queryArgs => ({ + url: 'api/verification', + method: 'POST', + body: { token: queryArgs } + }), + // Optional endpoint property that does additional logic when the query is initiated. + onQueryStarted: async (queryArgs, { dispatch, queryFulfilled }) => { + // wait for the initial query (verifyToken) to finish + await queryFulfilled + .then(async () => { + // 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(queryArgs)) + + // Get userDetails with verified token in headers + const response = dispatch(userApi.endpoints.getUserDetails.initiate()); + // Next time the endpoint is queried it should be should be re-fetched, not pulled from the cache + // Subscriptions are handled automatically by hooks, but not when called via 'dispatch(endpoint.initiate())' + // Manually unsubscribe from the cache via the returned promise's .unsubscribe() method + response.unsubscribe(); + // The returned response is the thunk's promise which internally handles the request's promise. + // Use unwrap to get the original request's promise. + await response.unwrap().catch(e => { throw (e) }) + + // if no error thrown user is now logged in and cache(s) may be out of date due to potential admin privileges etc. + // manually invalidate potentially out of date cache stores + dispatch(baseApi.util.invalidateTags(['MeterData', 'GroupData'])) + // If subscriptions to these tagged endpoints exist, they will automatically re-fetch. + // Otherwise subsequent requests will bypass and overwrite cache + }) + .catch(() => { + // User had a token that isn't valid or getUserDetails threw an error. + // Assume token is invalid. Delete if any + deleteToken() + }) + } + }) + }) +}) \ No newline at end of file diff --git a/src/client/app/redux/api/baseApi.ts b/src/client/app/redux/api/baseApi.ts index c5f60b61b..569739083 100644 --- a/src/client/app/redux/api/baseApi.ts +++ b/src/client/app/redux/api/baseApi.ts @@ -1,11 +1,23 @@ 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 }), + baseQuery: fetchBaseQuery({ + baseUrl: baseHref, + prepareHeaders: (headers, { getState }) => { + // For each api call attempt to set the JWT token in the request header + const state = getState() as RootState; + if (state.currentUser.token) { + headers.set('token', state.currentUser.token) + } + } + }), + tagTypes: ['MeterData', 'GroupData'], // Initially no defined endpoints, Use rtk query's injectEndpoints endpoints: () => ({}), - // Keed Data in Cache for 10 Minutes + // Keep Data in Cache for 10 Minutes (600 seconds) keepUnusedDataFor: 600 }) \ No newline at end of file diff --git a/src/client/app/redux/api/groupsApi.ts b/src/client/app/redux/api/groupsApi.ts index 1b42d8f5d..2d95f0694 100644 --- a/src/client/app/redux/api/groupsApi.ts +++ b/src/client/app/redux/api/groupsApi.ts @@ -4,7 +4,8 @@ import { GroupDetailsData } from '../../types/redux/groups' export const groupsApi = baseApi.injectEndpoints({ endpoints: builder => ({ getGroups: builder.query({ - query: () => 'api/groups' + query: () => 'api/groups', + providesTags: ['GroupData'] }) }) }) diff --git a/src/client/app/redux/api/metersApi.ts b/src/client/app/redux/api/metersApi.ts index 657fc6dc6..1d47a593e 100644 --- a/src/client/app/redux/api/metersApi.ts +++ b/src/client/app/redux/api/metersApi.ts @@ -8,10 +8,13 @@ export const metersApi = baseApi.injectEndpoints({ endpoints: builder => ({ getMeters: builder.query({ query: () => 'api/meters', + // Optional endpoint property that can transform incoming api responses if needed transformResponse: (response: MeterData[]) => { response.forEach(meter => { meter.readingFrequency = durationFormat(meter.readingFrequency) }); return _.keyBy(response, meter => meter.id) - } + }, + // Tags used for invalidation by mutation requests. + providesTags: ['MeterData'] }) }) }) diff --git a/src/client/app/redux/api/userApi.ts b/src/client/app/redux/api/userApi.ts new file mode 100644 index 000000000..2a8f509ab --- /dev/null +++ b/src/client/app/redux/api/userApi.ts @@ -0,0 +1,14 @@ +import { User } from '../../types/items'; +// import { authApi } from './authApi'; +import { baseApi } from './baseApi'; + +export const userApi = baseApi.injectEndpoints({ + endpoints: builder => ({ + getUserDetails: builder.query({ + query: () => 'api/users/token', + // Do not retain response when no subscribers + keepUnusedDataFor: 0 + } + ) + }) +}) \ No newline at end of file diff --git a/src/client/app/redux/selectors/authSelectors.ts b/src/client/app/redux/selectors/authSelectors.ts new file mode 100644 index 000000000..2900e539e --- /dev/null +++ b/src/client/app/redux/selectors/authSelectors.ts @@ -0,0 +1,2 @@ +import { RootState } from '../../store'; +export const selectCurrentUser = (state: RootState) => state.currentUser; diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts index 144ec674f..3e994f627 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -4,9 +4,13 @@ import { createSelector } from '@reduxjs/toolkit'; import * as _ from 'lodash'; +import { instanceOfGroupsState, instanceOfMetersState, instanceOfUnitsState } from '../../components/ChartDataSelectComponent'; import { RootState } from '../../store'; import { DataType } from '../../types/Datasources'; +import { GroupedOption, SelectOption } from '../../types/items'; import { ChartTypes, MeterOrGroup } from '../../types/redux/graph'; +import { GroupsState } from '../../types/redux/groups'; +import { MetersState } from '../../types/redux/meters'; import { DisplayableType, UnitData, UnitRepresentType, UnitType, UnitsState } from '../../types/redux/units'; import { CartesianPoint, Dimensions, calculateScaleFromEndpoints, gpsToUserGrid, @@ -14,10 +18,6 @@ import { } from '../../utils/calibration'; import { metersInGroup, unitsCompatibleWithMeters } from '../../utils/determineCompatibleUnits'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; -import { instanceOfGroupsState, instanceOfMetersState, instanceOfUnitsState } from '../../components/ChartDataSelectComponent'; -import { SelectOption, GroupedOption } from '../../types/items'; -import { MetersState } from '../../types/redux/meters'; -import { GroupsState } from '../../types/redux/groups'; export const selectSelectedMeters = (state: RootState) => state.graph.selectedMeters; @@ -66,9 +66,9 @@ export const selectVisibleMetersAndGroups = createSelector( } ); -export const selectMeterGroupUnitCompatibility = createSelector( - [selectVisibleMetersAndGroups, selectMeterState, selectGroupState, selectUnitState, selectGraphUnitID, selectGraphAreaNormalization], - (visible, meterState, groupState, unitState, graphUnitID, graphAreaNorm) => { +export const selectCurrentUnitCompatibility = createSelector( + [selectVisibleMetersAndGroups, selectMeterState, selectGroupState, selectGraphUnitID], + (visible, meterState, groupState, graphUnitID) => { // meters and groups that can graph const compatibleMeters = new Set(); const compatibleGroups = new Set(); @@ -89,12 +89,7 @@ export const selectMeterGroupUnitCompatibility = createSelector( } else { //Default graphic unit is set - if (graphAreaNorm && unitState.units[meterGraphingUnit] && unitState.units[meterGraphingUnit].unitRepresent === UnitRepresentType.raw) { - // area normalization is enabled and meter type is raw - incompatibleMeters.add(meterId); - } else { - compatibleMeters.add(meterId); - } + compatibleMeters.add(meterId); } }); visible.groups.forEach(groupId => { @@ -105,13 +100,7 @@ export const selectMeterGroupUnitCompatibility = createSelector( } else { //Default graphic unit is set - if (graphAreaNorm && unitState.units[groupGraphingUnit] && - unitState.units[groupGraphingUnit].unitRepresent === UnitRepresentType.raw) { - // area normalization is enabled and meter type is raw - incompatibleGroups.add(groupId); - } else { - compatibleGroups.add(groupId); - } + compatibleGroups.add(groupId); } }); } else { @@ -148,42 +137,70 @@ export const selectMeterGroupUnitCompatibility = createSelector( return { compatibleMeters, incompatibleMeters, compatibleGroups, incompatibleGroups } } ) - -export const selectMeterGroupStateCompatability = createSelector( - selectMeterGroupUnitCompatibility, +export const selectCurrentAreaCompatibility = createSelector( + selectCurrentUnitCompatibility, selectGraphAreaNormalization, - selectChartToRender, + selectGraphUnitID, selectMeterState, selectGroupState, - selectMapState, - selectSelectedMeters, - selectSelectedGroups, - selectGraphUnitID, - (unitCompat, areaNormalization, chartToRender, meterState, groupState, mapState, selectedMeters, selectedGroups, selectedUnitID) => { - // Deep Copy previous selector's values, and update as needed based on current state, like area norm, and map, etc. - const currentIncompatibleMeters = new Set(Array.from(unitCompat.incompatibleMeters)); - const currentIncompatibleGroups = new Set(Array.from(unitCompat.incompatibleGroups)); - const currentCompatibleMeters = new Set(Array.from(unitCompat.compatibleMeters)); - const currentCompatibleGroups = new Set(Array.from(unitCompat.compatibleGroups)); + selectUnitState, + (currentUnitCompatibility, areaNormalization, unitID, meterState, groupState, unitState) => { + // Deep Copy previous selector's values, and update as needed based on current Area Normalization setting + const compatibleMeters = new Set(currentUnitCompatibility.compatibleMeters); + const compatibleGroups = new Set(currentUnitCompatibility.compatibleGroups); + + // meters and groups that cannot graph. + const incompatibleMeters = new Set(currentUnitCompatibility.incompatibleMeters); + const incompatibleGroups = new Set(currentUnitCompatibility.incompatibleGroups); // only run this check if area normalization is on if (areaNormalization) { - unitCompat.compatibleMeters.forEach(meterID => { - // do not allow meter to be selected if it has zero area or no area unit - if (meterState.byMeterID[meterID].area === 0 || meterState.byMeterID[meterID].areaUnit === AreaUnitType.none) { - currentCompatibleMeters.delete(meterID) - currentIncompatibleMeters.add(meterID); + compatibleMeters.forEach(meterID => { + const meterGraphingUnit = meterState.byMeterID[meterID].defaultGraphicUnit; + // No unit is selected then no meter/group should be selected if area normalization is enabled and meter type is raw + if ((unitID === -99 && unitState.units[meterGraphingUnit] && unitState.units[meterGraphingUnit].unitRepresent === UnitRepresentType.raw) || + // do not allow meter to be selected if it has zero area or no area unit + meterState.byMeterID[meterID].area === 0 || meterState.byMeterID[meterID].areaUnit === AreaUnitType.none + ) { + incompatibleMeters.add(meterID); } }); - unitCompat.compatibleGroups.forEach(groupID => { - // do not allow group to be selected if it has zero area or no area unit - if (groupState.byGroupID[groupID].area === 0 || groupState.byGroupID[groupID].areaUnit === AreaUnitType.none) { - currentIncompatibleGroups.add(groupID); - currentCompatibleGroups.delete(groupID); + compatibleGroups.forEach(groupID => { + const groupGraphingUnit = groupState.byGroupID[groupID].defaultGraphicUnit; + // No unit is selected then no meter/group should be selected if area normalization is enabled and meter type is raw + + if ((unitID === -99 && unitState.units[groupGraphingUnit] && unitState.units[groupGraphingUnit].unitRepresent === UnitRepresentType.raw) || + // do not allow group to be selected if it has zero area or no area unit + groupState.byGroupID[groupID].area === 0 || groupState.byGroupID[groupID].areaUnit === AreaUnitType.none) { + incompatibleGroups.add(groupID); } }); + // Filter out any new incompatible meters/groups from the compatibility list. + incompatibleMeters.forEach(meterID => compatibleMeters.delete(meterID)) + incompatibleGroups.forEach(groupID => compatibleGroups.delete(groupID)) } + + return { compatibleMeters, incompatibleMeters, compatibleGroups, incompatibleGroups } + } +) + +export const selectChartTypeCompatibility = createSelector( + selectCurrentAreaCompatibility, + selectChartToRender, + selectMeterState, + selectGroupState, + selectMapState, + (areaCompat, chartToRender, meterState, groupState, mapState) => { + // Deep Copy previous selector's values, and update as needed based on current ChartType(s) + const compatibleMeters = new Set(Array.from(areaCompat.compatibleMeters)); + const incompatibleMeters = new Set(Array.from(areaCompat.incompatibleMeters)); + + const compatibleGroups = new Set(Array.from(areaCompat.compatibleGroups)); + const incompatibleGroups = new Set(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]; @@ -211,7 +228,7 @@ export const selectMeterGroupStateCompatability = createSelector( // causes TS to complain about the unknown case so not used. const origin = mp.origin; const opposite = mp.opposite; - unitCompat.compatibleMeters.forEach(meterID => { + compatibleMeters.forEach(meterID => { // This meter's GPS value. const gps = meterState.byMeterID[meterID].gps; if (origin !== undefined && opposite !== undefined && gps !== undefined && gps !== null) { @@ -223,127 +240,91 @@ export const selectMeterGroupStateCompatability = createSelector( const meterGPSInUserGrid: CartesianPoint = gpsToUserGrid(imageDimensionNormalized, gps, origin, scaleOfMap, mp.northAngle); if (!(itemMapInfoOk(meterID, DataType.Meter, mp, gps) && itemDisplayableOnMap(imageDimensionNormalized, meterGPSInUserGrid))) { - currentIncompatibleMeters.add(meterID); - currentCompatibleMeters.delete(meterID); + incompatibleMeters.add(meterID); } } else { // Lack info on this map so skip. This is mostly done since TS complains about the undefined possibility. - currentIncompatibleMeters.add(meterID); - currentCompatibleMeters.delete(meterID); + incompatibleMeters.add(meterID); } }); // The below code follows the logic for meters shown above. See comments above for clarification on the below code. - unitCompat.compatibleGroups.forEach(groupID => { + compatibleGroups.forEach(groupID => { const gps = groupState.byGroupID[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) && itemDisplayableOnMap(imageDimensionNormalized, groupGPSInUserGrid))) { - currentIncompatibleGroups.add(groupID); - currentCompatibleGroups.delete(groupID); + incompatibleGroups.add(groupID); } } else { - currentIncompatibleGroups.add(groupID); - currentCompatibleGroups.delete(groupID); + incompatibleGroups.add(groupID); } }); } + return { + compatibleMeters, + compatibleGroups, + incompatibleMeters, + incompatibleGroups + } + } +) + +export const selectMeterGroupSelectData = createSelector( + selectChartTypeCompatibility, + selectMeterState, + selectGroupState, + selectSelectedMeters, + selectSelectedGroups, + (chartTypeCompatibility, meterState, groupState, selectedMeters, selectedGroups) => { + // Destructure Previous Selectors's values + const { compatibleMeters, incompatibleMeters, compatibleGroups, incompatibleGroups } = chartTypeCompatibility; + // Calculate final compatible meters and groups for dropdown const compatibleSelectedMeters = new Set(); + const incompatibleSelectedMeters = new Set(); selectedMeters.forEach(meterID => { - // don't include meters that can't be graphed with current settings - if (!currentIncompatibleMeters.has(meterID)) { - compatibleSelectedMeters.add(meterID); - if (selectedUnitID == -99) { - // dispatch(changeSelectedUnit(state.meters.byMeterID[meterID].defaultGraphicUnit)); - console.log('TODO FIX ME. MOVE ME TO SELECT LOGIC THERE should be no dispatches inside of selectors') - // If the selected unit is -99 then there is not graphic unit yet. In this case you can only select a - // meter that has a default graphic unit because that will become the selected unit. This should only - // happen if no meter or group is yet selected. - // If no unit is set then this should always be the first meter (or group) selected. - // The selectedUnit becomes the unit of the meter selected. Note is should always be set (not -99) since - // those meters should not have been visible. The only exception is if there are no selected meters but - // then this loop does not run. The loop is assumed to only run once in this case. - } - } + // Sort and populate compatible/incompatible based on previous selector's compatible meters + compatibleMeters.has(meterID) ? compatibleSelectedMeters.add(meterID) : incompatibleSelectedMeters.add(meterID) }); const compatibleSelectedGroups = new Set(); + const incompatibleSelectedGroups = new Set(); selectedGroups.forEach(groupID => { - // don't include groups that can't be graphed with current settings - if (!currentIncompatibleGroups.has(groupID)) { - // If the selected unit is -99 then there is no graphic unit yet. In this case you can only select a - // group that has a default graphic unit because that will become the selected unit. This should only - // happen if no meter or group is yet selected. - if (selectedUnitID == -99) { - // If no unit is set then this should always be the first group (or meter) selected. - // The selectedUnit becomes the unit of the group selected. Note is should always be set (not -99) since - // those groups should not have been visible. The only exception is if there are no selected groups but - // then this loop does not run. The loop is assumed to only run once in this case. - // dispatch(changeSelectedUnit(state.groups.byGroupID[groupID].defaultGraphicUnit)); - console.log('TODO FIX ME. MOVE ME TO graphSliceLogic LOGIC THERE should be no dispatches inside of selectors') - - } - compatibleSelectedGroups.add(groupID); - } + // Sort and populate compatible/incompatible based on previous selector's compatible groups + compatibleGroups.has(groupID) ? compatibleSelectedGroups.add(groupID) : incompatibleSelectedGroups.add(groupID) }); - // console.log(compatibleSelectedMeters, currentIncompatibleMeters, compatibleSelectedGroups, currentIncompatibleGroups) - - return { - compatibleSelectedMeters, - compatibleSelectedGroups, - currentCompatibleMeters, - currentCompatibleGroups, - currentIncompatibleMeters, - currentIncompatibleGroups - } - } -) -export const selectMeterGroupSelectData = createSelector( - selectMeterGroupStateCompatability, - selectMeterState, - selectGroupState, - (stateCompatibility, meterState, groupState) => { // The Multiselect's current selected value(s) - const compatibleSelectedMeters = getSelectOptionsByItem(stateCompatibility.compatibleSelectedMeters, true, meterState) - const compatibleSelectedGroups = getSelectOptionsByItem(stateCompatibility.compatibleSelectedGroups, true, groupState) + const selectedMeterOptions = getSelectOptionsByItem(compatibleSelectedMeters, incompatibleSelectedMeters, meterState) + const selectedGroupOptions = getSelectOptionsByItem(compatibleSelectedGroups, incompatibleSelectedGroups, groupState) - // The Multiselect's options are grouped as compatible and imcompatible. - // get pairs - const currentCompatibleMeters = getSelectOptionsByItem(stateCompatibility.currentCompatibleMeters, true, meterState) - const currentIncompatibleMeters = getSelectOptionsByItem(stateCompatibility.currentIncompatibleMeters, false, meterState) - - const currentCompatibleGroups = getSelectOptionsByItem(stateCompatibility.currentCompatibleGroups, true, groupState) - const currentIncompatibleGroups = getSelectOptionsByItem(stateCompatibility.currentIncompatibleGroups, false, groupState) + // List of options with metadata for react-select + const meterSelectOptions = getSelectOptionsByItem(compatibleMeters, incompatibleMeters, meterState) + const groupSelectOptions = getSelectOptionsByItem(compatibleGroups, incompatibleGroups, groupState) + // currently when selected values are found to be incompatible (by area for example) get removed from selected options. + // in the near future they should instead remain selected but visually appear disabled. + // TODO WRITE CUSTOM SELECT VALUE TO BE ABLE TO UTILIZE THESE Values + // These value(s) is not currently utilized + const selectedMeterValues = selectedMeterOptions.compatible.concat(selectedMeterOptions.incompatible) + const selectedGroupValues = selectedGroupOptions.compatible.concat(selectedGroupOptions.incompatible) + // Format The generated selectOptions into grouped options for the React-Select component const meterGroupedOptions: GroupedOption[] = [ - { - label: 'Meters', - options: currentCompatibleMeters - }, - { - label: 'Incompatible Meters', - options: currentIncompatibleMeters - } + { label: 'Meters', options: meterSelectOptions.compatible }, + { label: 'Incompatible Meters', options: meterSelectOptions.incompatible } ] const groupsGroupedOptions: GroupedOption[] = [ - { - label: 'Options', - options: currentCompatibleGroups - }, - { - label: 'Incompatible Options', - options: currentIncompatibleGroups - } + { label: 'Options', options: groupSelectOptions.compatible }, + { label: 'Incompatible Options', options: groupSelectOptions.incompatible } ] - console.log('Where Am i Even', meterGroupedOptions, groupsGroupedOptions); - return { meterGroupedOptions, groupsGroupedOptions, compatibleSelectedMeters, compatibleSelectedGroups } + + return { meterGroupedOptions, groupsGroupedOptions, selectedMeterValues, selectedGroupValues } } ) @@ -429,73 +410,44 @@ export const selectUnitSelectData = createSelector( }); } // Ready to display unit. Put selectable ones before non-selectable ones. - const compatibleUnitsOptions = getSelectOptionsByItem(compatibleUnits, true, unitState); - const incompatibleUnitsOptions = getSelectOptionsByItem(incompatibleUnits, false, unitState); + const unitOptions = getSelectOptionsByItem(compatibleUnits, incompatibleUnits, unitState); const unitsGroupedOptions: GroupedOption[] = [ { label: 'Units', - options: compatibleUnitsOptions + options: unitOptions.compatible }, { label: 'Incompatible Units', - options: incompatibleUnitsOptions + options: unitOptions.incompatible } ] return unitsGroupedOptions } ) -export const selectMeterGroupAreaCompatibility = createSelector( - selectMeterState, - selectGroupState, - (meterState, groupState) => { - // store meters which are found to be incompatible. - const incompatibleMeters = new Set(); - const incompatibleGroups = new Set(); - const compatibleMeters = new Set(); - const compatibleGroups = new Set(); - - Object.values(meterState.byMeterID).forEach(meter => { - // do not allow meter to be selected if it has zero area or no area unit - if (meterState.byMeterID[meter.id].area === 0 || meterState.byMeterID[meter.id].areaUnit === AreaUnitType.none) { - incompatibleMeters.add(meter.id); - } else { - compatibleMeters.add(meter.id); - } - }); - - Object.values(groupState.byGroupID).forEach(group => { - // do not allow group to be selected if it has zero area or no area unit - if (groupState.byGroupID[group.id].area === 0 || groupState.byGroupID[group.id].areaUnit === AreaUnitType.none) { - incompatibleGroups.add(group.id); - } else { - compatibleGroups.add(group.id); - } - }); - - return { compatibleMeters, incompatibleMeters, compatibleGroups, incompatibleGroups } - } -) /** * Returns a set of SelectOptions based on the type of state passed in and sets the visibility. * Visibility is determined by which set the items are contained in. - * @param items - items to retrieve select options for - * @param isCompatible - determines the group option + * @param compatibleItems - compatible items to make select options for + * @param incompatibleItems - incompatible items to make select options for * @param state - current redux state, must be one of UnitsState, MetersState, or GroupsState - * @returns list of selectOptions of the given item + * @returns Two Lists: Compatible, and Incompatible selectOptions for use as grouped React-Select options */ -export function getSelectOptionsByItem(items: Set, isCompatible: boolean, state: UnitsState | MetersState | GroupsState) { +export function getSelectOptionsByItem(compatibleItems: Set, incompatibleItems: Set, state: UnitsState | MetersState | GroupsState) { // TODO Refactor origina // redefined here for testing. // Holds the label of the select item, set dynamically according to the type of item passed in - let label = ''; - let meterOrGroup: MeterOrGroup | undefined; + //The final list of select options to be displayed - const itemOptions: SelectOption[] = []; + const compatibleItemOptions: SelectOption[] = []; + const incompatibleItemOptions: SelectOption[] = []; //Loop over each itemId and create an activated select option - items.forEach(itemId => { + compatibleItems.forEach(itemId => { + let label = ''; + let meterOrGroup: MeterOrGroup | undefined; + let defaultGraphicUnit: number | undefined; // Perhaps in the future this can be done differently // Loop over the state type to see what state was passed in (units, meter, group, etc) // Set the label correctly based on the type of state @@ -508,26 +460,63 @@ export function getSelectOptionsByItem(items: Set, isCompatible: boolean else if (instanceOfMetersState(state)) { label = state.byMeterID[itemId].identifier; meterOrGroup = MeterOrGroup.meters + defaultGraphicUnit = state.byMeterID[itemId].defaultGraphicUnit; + } + else if (instanceOfGroupsState(state)) { + label = state.byGroupID[itemId].name; + meterOrGroup = MeterOrGroup.groups + defaultGraphicUnit = state.byGroupID[itemId].defaultGraphicUnit; + } + else { label = ''; } + // TODO This is a bit of a hack. When an admin logs in they may not have the new state so label is null. + // This should clear once the state is loaded. + label = label === null ? '' : label; + compatibleItemOptions.push({ + value: itemId, + label: label, + // If option is compatible then not disabled + isDisabled: false, + meterOrGroup: meterOrGroup, + defaultGraphicUnit: defaultGraphicUnit + } as SelectOption + ); + }); + + incompatibleItems.forEach(itemId => { + let label = ''; + let meterOrGroup: MeterOrGroup | undefined; + let defaultGraphicUnit: number | undefined; + if (instanceOfUnitsState(state)) { + label = state.units[itemId].identifier; + } + else if (instanceOfMetersState(state)) { + label = state.byMeterID[itemId].identifier; + meterOrGroup = MeterOrGroup.meters + defaultGraphicUnit = state.byMeterID[itemId].defaultGraphicUnit; + } else if (instanceOfGroupsState(state)) { label = state.byGroupID[itemId].name; meterOrGroup = MeterOrGroup.groups + defaultGraphicUnit = state.byGroupID[itemId].defaultGraphicUnit; } else { label = ''; } // TODO This is a bit of a hack. When an admin logs in they may not have the new state so label is null. // This should clear once the state is loaded. label = label === null ? '' : label; - itemOptions.push({ + incompatibleItemOptions.push({ value: itemId, label: label, - // If option is compatible then ! not disabled - isDisabled: !isCompatible, - meterOrGroup: meterOrGroup + // If option is compatible then not disabled + isDisabled: true, + meterOrGroup: meterOrGroup, + defaultGraphicUnit: defaultGraphicUnit } as SelectOption ); }); - const sortedOptions = _.sortBy(itemOptions, item => item.label.toLowerCase(), 'asc') + const sortedCompatibleOptions = _.sortBy(compatibleItemOptions, item => item.label.toLowerCase(), 'asc') + const sortedIncompatibleOptions = _.sortBy(incompatibleItemOptions, item => item.label.toLowerCase(), 'asc') - return sortedOptions + return { compatible: sortedCompatibleOptions, incompatible: sortedIncompatibleOptions } } diff --git a/src/client/app/types/items.ts b/src/client/app/types/items.ts index 3f629bca6..e7cbabc05 100644 --- a/src/client/app/types/items.ts +++ b/src/client/app/types/items.ts @@ -15,7 +15,8 @@ export interface SelectOption { isDisabled?: boolean; labelIdForTranslate?: string; style?: React.CSSProperties; - meterOrGroup?: MeterOrGroup + meterOrGroup?: MeterOrGroup; + defaultGraphicUnit?: number; } export interface GroupedOption { label: string; diff --git a/src/client/app/types/redux/currentUser.ts b/src/client/app/types/redux/currentUser.ts index be43bfc26..78c79b26f 100644 --- a/src/client/app/types/redux/currentUser.ts +++ b/src/client/app/types/redux/currentUser.ts @@ -5,25 +5,8 @@ // import { ActionType } from './actions'; import { User } from '../items'; -/* -* Defines the action interfaces used in the corresponding reducers. -*/ - -// export interface RequestCurrentUser { -// type: ActionType.RequestCurrentUser; -// } -// export interface ReceiveCurrentUser { -// type: ActionType.ReceiveCurrentUser; -// data: User; -// } - -// export interface ClearCurrentUser { -// type: ActionType.ClearCurrentUser; -// } - -// export type CurrentUserAction = RequestCurrentUser | ReceiveCurrentUser | ClearCurrentUser; - export interface CurrentUserState { isFetching: boolean; profile: User | null; + token: string | null; } diff --git a/src/client/app/utils/determineCompatibleUnits.ts b/src/client/app/utils/determineCompatibleUnits.ts index a5266228e..091d080aa 100644 --- a/src/client/app/utils/determineCompatibleUnits.ts +++ b/src/client/app/utils/determineCompatibleUnits.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 store from '../index'; +import {store} from '../store'; import * as _ from 'lodash'; import { MeterData } from '../types/redux/meters'; import { ConversionArray } from '../types/conversionArray'; diff --git a/src/client/app/utils/token.ts b/src/client/app/utils/token.ts index 9a3ab43d2..b3dda068a 100644 --- a/src/client/app/utils/token.ts +++ b/src/client/app/utils/token.ts @@ -33,3 +33,10 @@ export function hasToken(): boolean { export function deleteToken(): void { localStorage.removeItem('token'); } + +/** + * @param token the token string to save to local storage for returning users. + */ +export function setToken(token: string): void { + localStorage.setItem('token', token); +} diff --git a/src/client/app/utils/translate.ts b/src/client/app/utils/translate.ts index a56bf0880..aaa699fc6 100644 --- a/src/client/app/utils/translate.ts +++ b/src/client/app/utils/translate.ts @@ -4,7 +4,7 @@ import { defineMessages, createIntl, createIntlCache } from 'react-intl'; import localeData from '../translations/data'; -import store from '../index'; +import { store } from '../store'; // TODO This used to be multiple types of: // const enum AsTranslated {} @@ -25,8 +25,7 @@ export default function translate(messageID: string): TranslatedString { // My guess is that the call to store.getState() was too early as the store hadn't finished loading completely // For now, set the default language to english and any component subscribed to the language state should properly re-render if the language changes let lang = 'en'; - if (store) - { + if (store) { lang = store.getState().options.selectedLanguage; } /* @@ -37,5 +36,5 @@ export default function translate(messageID: string): TranslatedString { const messages = (localeData as any)[lang]; const cache = createIntlCache(); const intl = createIntl({ locale: lang, messages }, cache); - return intl.formatMessage(defineMessages({ [messageID]: { id: messageID }})[messageID]) as TranslatedString; + return intl.formatMessage(defineMessages({ [messageID]: { id: messageID } })[messageID]) as TranslatedString; } From 1b6cbe1af8621d70e5893339a00f81047869e5df Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Thu, 28 Sep 2023 23:40:27 +0000 Subject: [PATCH 016/131] Initialization Component Changes - More RTK Queries. --- .../components/InitializationComponent.tsx | 36 +++++++++------- .../MeterAndGroupSelectComponent.tsx | 9 +--- .../app/components/RouteComponentWIP.tsx | 4 +- src/client/app/index.tsx | 6 +-- src/client/app/reducers/admin.ts | 10 ++--- src/client/app/reducers/conversions.ts | 11 ++++- src/client/app/reducers/graph.ts | 16 ++++---- src/client/app/reducers/units.ts | 11 ++++- src/client/app/redux/api/baseApi.ts | 2 +- src/client/app/redux/api/conversionsApi.ts | 41 +++++++++++++++++++ src/client/app/redux/api/preferencesApi.ts | 40 ++++++++++++++++++ src/client/app/redux/api/unitsApi.ts | 25 +++++++++++ 12 files changed, 166 insertions(+), 45 deletions(-) create mode 100644 src/client/app/redux/api/conversionsApi.ts create mode 100644 src/client/app/redux/api/preferencesApi.ts create mode 100644 src/client/app/redux/api/unitsApi.ts diff --git a/src/client/app/components/InitializationComponent.tsx b/src/client/app/components/InitializationComponent.tsx index 9b2e53ea5..7aed89c64 100644 --- a/src/client/app/components/InitializationComponent.tsx +++ b/src/client/app/components/InitializationComponent.tsx @@ -8,32 +8,40 @@ import { useDispatch } from 'react-redux'; import { Slide, ToastContainer } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import { Dispatch } from 'types/redux/actions'; -import { fetchPreferencesIfNeeded } from '../actions/admin'; -import { fetchConversionsDetailsIfNeeded } from '../actions/conversions'; import { fetchMapsDetails } from '../actions/map'; -import { fetchUnitsDetailsIfNeeded } from '../actions/units'; +import { authApi } from '../redux/api/authApi'; +import { conversionsApi } from '../redux/api/conversionsApi'; import { groupsApi } from '../redux/api/groupsApi'; import { metersApi } from '../redux/api/metersApi'; -// import { userApi } from '../redux/api/userApi'; -import { authApi } from '../redux/api/authApi'; +import { preferencesApi } from '../redux/api/preferencesApi'; +import { unitsApi } from '../redux/api/unitsApi'; import { ConversionArray } from '../types/conversionArray'; import { getToken, hasToken } from '../utils/token'; /** - * Initializes OED redux with needed details + * Initializes the app by fetching and subscribing to the store with various queries * @returns Initialization JSX element */ export default function InitializationComponent() { - const dispatch: Dispatch = useDispatch(); // QueryHooks derived by api endpoint definitions - // These useQuery hooks subscribe to the store, and automatically fetch and cache data to the store. + // These useQuery hooks fetch and cache data to the store as soon the component mounts. + // They maintain an active subscription to the store so long as the component remains mounted. + // Since this component lives up near the root of the DOM, these queries will remain subscribed indefinitely + preferencesApi.useGetPreferencesQuery(); + unitsApi.useGetUnitsDetailsQuery(); + conversionsApi.useGetConversionsDetailsQuery(); + conversionsApi.useGetConversionArrayQuery(); metersApi.useGetMetersQuery(); - // metersApi.endpoints.getMeters.useQuery(); Another way to access the same hooks groupsApi.useGetGroupsQuery(); - // groupsApi.endpoints.getGroups.useQuery(); Another way to access the same hook + + // With RTKQuery, Mutations are used for POST, PUT, PATCH, etc. + // The useMutation() hooks return a triggerFunction that can be called to initiate the request. const [verifyTokenTrigger] = authApi.useVerifyTokenMutation() - // There are many derived hooks each with different use cases. Read More @ https://redux-toolkit.js.org/rtk-query/api/created-api/hooks#hooks-overview + // There are many derived hooks each with different use cases + // Read More @ https://redux-toolkit.js.org/rtk-query/api/created-api/hooks#hooks-overview + + const dispatch: Dispatch = useDispatch(); // Only run once by making it depend on an empty array. useEffect(() => { @@ -43,10 +51,10 @@ export default function InitializationComponent() { verifyTokenTrigger(getToken()) } - dispatch(fetchPreferencesIfNeeded()); + // dispatch(fetchPreferencesIfNeeded()); dispatch(fetchMapsDetails()); - dispatch(fetchUnitsDetailsIfNeeded()); - dispatch(fetchConversionsDetailsIfNeeded()); + // dispatch(fetchUnitsDetailsIfNeeded()); + // dispatch(fetchConversionsDetailsIfNeeded()); ConversionArray.fetchPik(); }, []); diff --git a/src/client/app/components/MeterAndGroupSelectComponent.tsx b/src/client/app/components/MeterAndGroupSelectComponent.tsx index f2a36bf88..9787bdc1b 100644 --- a/src/client/app/components/MeterAndGroupSelectComponent.tsx +++ b/src/client/app/components/MeterAndGroupSelectComponent.tsx @@ -23,16 +23,9 @@ const animatedComponents = makeAnimated(); export default function MeterAndGroupSelectComponent(props: MeterAndGroupSelectProps) { const dispatch = useAppDispatch(); const meterAndGroupSelectOptions = useAppSelector(state => selectMeterGroupSelectData(state)); - // const selectedMeters = useAppSelector(state => selectSelectedMeters(state)) - // const selectedGroups = useAppSelector(state => selectSelectedGroups(state)) - // console.log(meterAndGroupSelectOptions) const { meterOrGroup } = props; // Set the current component's appropriate meter or group update from the graphSlice's Payload-Action Creator - const updateSelectedMetersOrGroups = meterOrGroup === MeterOrGroup.meters ? - graphSlice.actions.updateSelectedMetersFromSelect - : - graphSlice.actions.updateSelectedGroupsFromSelect const value = meterOrGroup === MeterOrGroup.meters ? meterAndGroupSelectOptions.selectedMeterValues @@ -47,7 +40,7 @@ export default function MeterAndGroupSelectComponent(props: MeterAndGroupSelectP const onChange = (newValues: MultiValue, meta: ActionMeta) => { const newMetersOrGroups = newValues.map((option: SelectOption) => option.value); - dispatch(updateSelectedMetersOrGroups({ newMetersOrGroups, meta })) + dispatch(graphSlice.actions.updateSelectedMetersOrGroups({ newMetersOrGroups, meta })) } return ( diff --git a/src/client/app/components/RouteComponentWIP.tsx b/src/client/app/components/RouteComponentWIP.tsx index 71b7754b4..206af8344 100644 --- a/src/client/app/components/RouteComponentWIP.tsx +++ b/src/client/app/components/RouteComponentWIP.tsx @@ -8,7 +8,7 @@ import * as queryString from 'query-string'; import * as React from 'react'; import { IntlProvider } from 'react-intl'; import { useDispatch } from 'react-redux'; -import { Redirect, BrowserRouter } from 'react-router-dom'; +import { BrowserRouter, Redirect } from 'react-router-dom'; import { CompatRouter, Route, Routes } from 'react-router-dom-v5-compat'; import { TimeInterval } from '../../../common/TimeInterval'; import { LinkOptions, changeOptionsFromLink } from '../actions/graph'; @@ -28,7 +28,6 @@ import { hasPermissions } from '../utils/hasPermissions'; import { showErrorNotification } from '../utils/notifications'; import translate from '../utils/translate'; import HomeComponent from './HomeComponent'; -import InitializationComponent from './InitializationComponent'; import LoginComponent from './LoginComponent'; import AdminComponent from './admin/AdminComponent'; import ConversionsDetailComponent from './conversion/ConversionsDetailComponent'; @@ -176,7 +175,6 @@ export default function RouteComponent() { const messages = (localeData as any)[lang]; return ( <> - {/* Compatibility layer for transitioning to react-router 6.X diff --git a/src/client/app/index.tsx b/src/client/app/index.tsx index 834ffb26a..3976134ea 100644 --- a/src/client/app/index.tsx +++ b/src/client/app/index.tsx @@ -10,6 +10,7 @@ import 'bootstrap/dist/css/bootstrap.css'; // import RouteContainer from './containers/RouteContainer'; import RouteComponent from './components/RouteComponentWIP'; import './styles/index.css'; +import InitializationComponent from './components/InitializationComponent'; // Renders the entire application, starting with RouteComponent, into the root div const container = document.getElementById('root'); @@ -19,8 +20,7 @@ const root = createRoot(container!); root.render( // Provides the Redux store to all child components - {/* */} + -); - +); \ No newline at end of file diff --git a/src/client/app/reducers/admin.ts b/src/client/app/reducers/admin.ts index e2fd06cec..fbce0f0ce 100644 --- a/src/client/app/reducers/admin.ts +++ b/src/client/app/reducers/admin.ts @@ -2,14 +2,14 @@ * 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 { ChartTypes } from '../types/redux/graph'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import * as moment from 'moment'; +import { PreferenceRequestItem } from '../types/items'; import { AdminState } from '../types/redux/admin'; +import { ChartTypes } from '../types/redux/graph'; import { LanguageTypes } from '../types/redux/i18n'; -import { AreaUnitType } from '../utils/getAreaUnitConversion'; import { durationFormat } from '../utils/durationFormat'; -import * as moment from 'moment'; -import { PreferenceRequestItem } from '../types/items'; -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { AreaUnitType } from '../utils/getAreaUnitConversion'; const defaultState: AdminState = { selectedMeter: null, diff --git a/src/client/app/reducers/conversions.ts b/src/client/app/reducers/conversions.ts index 08a6a77c5..479e484cf 100644 --- a/src/client/app/reducers/conversions.ts +++ b/src/client/app/reducers/conversions.ts @@ -2,9 +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 { ConversionsState } from '../types/redux/conversions'; -import * as t from '../types/redux/conversions'; import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { conversionsApi } from '../redux/api/conversionsApi'; +import * as t from '../types/redux/conversions'; +import { ConversionsState } from '../types/redux/conversions'; const defaultState: ConversionsState = { hasBeenFetchedOnce: false, @@ -71,5 +72,11 @@ export const conversionsSlice = createSlice({ // Remove the ConversionData from the conversions array conversions.splice(conversionDataIndex, 1); } + }, + extraReducers: builder => { + builder.addMatcher(conversionsApi.endpoints.getConversionsDetails.matchFulfilled, + (state, action) => { + state.conversions = action.payload + }) } }); \ No newline at end of file diff --git a/src/client/app/reducers/graph.ts b/src/client/app/reducers/graph.ts index 4009e8af5..bae73d6cc 100644 --- a/src/client/app/reducers/graph.ts +++ b/src/client/app/reducers/graph.ts @@ -10,7 +10,7 @@ import { SelectOption } from '../types/items'; import { ChartTypes, GraphState, LineGraphRate, MeterOrGroup, ReadingInterval } from '../types/redux/graph'; import { ComparePeriod, SortingOrder, calculateCompareTimeInterval } from '../utils/calculateCompare'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; -import { adminSlice } from './admin'; +import { preferencesApi } from '../redux/api/preferencesApi'; const defaultState: GraphState = { selectedMeters: [], @@ -88,12 +88,18 @@ export const graphSlice = createSlice({ toggleAreaNormalization: state => { state.areaNormalization = !state.areaNormalization }, + setAreaNormalization: (state, action: PayloadAction) => { + state.areaNormalization = action.payload + }, toggleShowMinMax: state => { state.showMinMax = !state.showMinMax }, changeBarStacking: state => { state.barStacking = !state.barStacking }, + setBarStacking: (state, action: PayloadAction) => { + state.barStacking = action.payload + }, setHotlinked: (state, action: PayloadAction) => { state.hotlinked = action.payload }, @@ -113,7 +119,7 @@ export const graphSlice = createSlice({ state.threeD.meterOrGroupID = action.payload.meterOrGroupID state.threeD.meterOrGroup = action.payload.meterOrGroup }, - updateSelectedMetersFromSelect: (state, action: PayloadAction<{ newMetersOrGroups: number[], meta: ActionMeta }>) => { + updateSelectedMetersOrGroups: (state, action: PayloadAction<{ newMetersOrGroups: number[], meta: ActionMeta }>) => { // Destructure payload const { newMetersOrGroups, meta } = action.payload; @@ -166,17 +172,13 @@ export const graphSlice = createSlice({ state.threeD.meterOrGroup = undefined } - }, - updateSelectedGroupsFromSelect: (state, action: PayloadAction<{ newMetersOrGroups: number[], meta: ActionMeta }>) => { - state.selectedGroups = action.payload.newMetersOrGroups } }, extraReducers: builder => { - builder.addCase(adminSlice.actions.receivePreferences, + builder.addMatcher(preferencesApi.endpoints.getPreferences.matchFulfilled, (state, action) => { if (state.selectedAreaUnit == AreaUnitType.none) { state.selectedAreaUnit = action.payload.defaultAreaUnit - } }) } diff --git a/src/client/app/reducers/units.ts b/src/client/app/reducers/units.ts index 7d8e13f7b..cfd4dadae 100644 --- a/src/client/app/reducers/units.ts +++ b/src/client/app/reducers/units.ts @@ -1,10 +1,12 @@ /* 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 _ from 'lodash'; -import { UnitsState } from '../types/redux/units'; import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import * as _ from 'lodash'; +import { unitsApi } from '../redux/api/unitsApi'; import * as t from '../types/redux/units'; +import { UnitsState } from '../types/redux/units'; + const defaultState: UnitsState = { hasBeenFetchedOnce: false, isFetching: false, @@ -39,5 +41,10 @@ export const unitsSlice = createSlice({ confirmUnitEdits: (state, action: PayloadAction) => { state.submitting.splice(state.submitting.indexOf(action.payload), 1); } + }, + extraReducers: builder => { + builder.addMatcher(unitsApi.endpoints.getUnitsDetails.matchFulfilled, + (state, action) => { state.units = _.keyBy(action.payload, unit => unit.id) } + ) } }); \ No newline at end of file diff --git a/src/client/app/redux/api/baseApi.ts b/src/client/app/redux/api/baseApi.ts index 569739083..338174024 100644 --- a/src/client/app/redux/api/baseApi.ts +++ b/src/client/app/redux/api/baseApi.ts @@ -15,7 +15,7 @@ export const baseApi = createApi({ } } }), - tagTypes: ['MeterData', 'GroupData'], + tagTypes: ['MeterData', 'GroupData', 'Preferences'], // Initially no defined endpoints, Use rtk query's injectEndpoints endpoints: () => ({}), // Keep Data in Cache for 10 Minutes (600 seconds) diff --git a/src/client/app/redux/api/conversionsApi.ts b/src/client/app/redux/api/conversionsApi.ts new file mode 100644 index 000000000..f0a6a0ddc --- /dev/null +++ b/src/client/app/redux/api/conversionsApi.ts @@ -0,0 +1,41 @@ +import { ConversionData } from '../../types/redux/conversions'; +import { baseApi } from './baseApi'; + +export const conversionsApi = baseApi.injectEndpoints({ + endpoints: builder => ({ + getConversionsDetails: builder.query({ + query: () => 'api/conversions' + }), + addConversion: builder.query({ + query: conversion => ({ + url: 'api/conversions/addConversion', + method: 'POST', + body: { conversion } + }) + }), + deleteConversion: builder.query({ + query: conversion => ({ + url: 'api/conversions/delete', + method: 'POST', + body: { conversion } + }) + }), + editConversion: builder.query({ + query: conversion => ({ + url: 'api/conversions/edit', + method: 'POST', + body: { ...conversion } + }) + }), + getConversionArray: builder.query({ + query: () => 'api/conversion-array' + }), + refresh: builder.mutation({ + query: args => ({ + url: 'api/conversion-array/refresh', + method: 'POST', + body: { redoCik: args.redoCik, refreshReadingViews: args.refreshReadingViews } + }) + }) + }) +}) \ No newline at end of file diff --git a/src/client/app/redux/api/preferencesApi.ts b/src/client/app/redux/api/preferencesApi.ts new file mode 100644 index 000000000..dbb7097b1 --- /dev/null +++ b/src/client/app/redux/api/preferencesApi.ts @@ -0,0 +1,40 @@ +import { updateSelectedLanguage } from '../../actions/options'; +import { graphSlice } from '../../reducers/graph'; +import { PreferenceRequestItem } from '../../types/items'; +import { RootState } from './../../store'; +import { baseApi } from './baseApi'; + + +export const preferencesApi = baseApi.injectEndpoints({ + endpoints: builder => ({ + getPreferences: builder.query({ + query: () => 'api/preferences', + // Tags used for invalidation by mutation requests. + onQueryStarted: async (_arg, { queryFulfilled, getState, dispatch }) => { + try { + const response = await queryFulfilled + const state = getState() as RootState + if (!state.graph.hotlinked) { + dispatch(graphSlice.actions.changeChartToRender(response.data.defaultChartToRender)); + dispatch(graphSlice.actions.setBarStacking(response.data.defaultBarStacking)); + dispatch(graphSlice.actions.setAreaNormalization(response.data.defaultAreaNormalization)); + dispatch(updateSelectedLanguage(response.data.defaultLanguage)); + } + + } catch (e) { + console.log('error', e) + } + + } + }), + submitPreferences: builder.mutation({ + query: preferences => ({ + url: 'api/preferences', + method: 'POST', + body: { preferences } + }), + invalidatesTags: ['Preferences'] + }) + }) +}) + diff --git a/src/client/app/redux/api/unitsApi.ts b/src/client/app/redux/api/unitsApi.ts new file mode 100644 index 000000000..5e1774036 --- /dev/null +++ b/src/client/app/redux/api/unitsApi.ts @@ -0,0 +1,25 @@ +import { UnitData } from '../../types/redux/units'; +import { baseApi } from './baseApi'; + +export const unitsApi = baseApi.injectEndpoints({ + endpoints: builder => ({ + getUnitsDetails: builder.query({ + query: () => 'api/units' + }), + addUnit: builder.mutation({ + query: unitDataArgs => ({ + url: 'api/units/addUnit', + method: 'POST', + body: { unitDataArgs } + }) + }), + editUnit: builder.mutation({ + //TODO VALIDATE BEHAVIOR should invalidate? + query: unitDataArgs => ({ + url: 'api/units/edit', + method: 'POST', + body: { ...unitDataArgs } + }) + }) + }) +}) \ No newline at end of file From 4b967fff6147d909f8025671e78b2235c3c0f977 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Sat, 30 Sep 2023 15:53:35 +0000 Subject: [PATCH 017/131] RouteComponentWip -RouteComponent - Login mutations, and useNavigateHook(); --- .../app/components/HeaderButtonsComponent.tsx | 8 +- .../components/InitializationComponent.tsx | 8 +- src/client/app/components/LoginComponent.tsx | 27 +- .../app/components/RouteComponentWIP.tsx | 350 +++++++++--------- src/client/app/index.tsx | 7 +- src/client/app/reducers/currentUser.ts | 13 +- src/client/app/redux/api/authApi.ts | 2 + src/client/app/redux/api/baseApi.ts | 4 +- src/server/app.js | 3 +- 9 files changed, 232 insertions(+), 190 deletions(-) diff --git a/src/client/app/components/HeaderButtonsComponent.tsx b/src/client/app/components/HeaderButtonsComponent.tsx index 33d97f9cc..54ac22244 100644 --- a/src/client/app/components/HeaderButtonsComponent.tsx +++ b/src/client/app/components/HeaderButtonsComponent.tsx @@ -17,7 +17,7 @@ import { Navbar, Nav, NavLink, UncontrolledDropdown, DropdownToggle, DropdownMen import LanguageSelectorComponent from './LanguageSelectorComponent'; import { toggleOptionsVisibility } from '../actions/graph'; import { BASE_URL } from './TooltipHelpComponent'; -import {currentUserSlice} from '../reducers/currentUser'; +import { currentUserSlice } from '../reducers/currentUser'; import { unsavedWarningSlice } from '../reducers/unsavedWarning'; /** @@ -26,10 +26,14 @@ import { unsavedWarningSlice } from '../reducers/unsavedWarning'; */ export default function HeaderButtonsComponent() { const dispatch = useDispatch(); - // Get the current page so know which one should not be shown in menu. const currentPage = getPage(); + // React router 6 baked in equivalent hook, + // const currentPage = useLocation(); + + + // This is the state model for rendering this page. const defaultState = { // All these values should update before user interacts with them so hide everything until the useEffects diff --git a/src/client/app/components/InitializationComponent.tsx b/src/client/app/components/InitializationComponent.tsx index 7aed89c64..5c8304826 100644 --- a/src/client/app/components/InitializationComponent.tsx +++ b/src/client/app/components/InitializationComponent.tsx @@ -50,12 +50,16 @@ export default function InitializationComponent() { // use the verify token mutation, verifyTokenTrigger(getToken()) } + dispatch(fetchMapsDetails()); + ConversionArray.fetchPik(); + + // Converted to useHooks() + // dispatch(fetchMetersDetailsIfNeeded()); + // dispatch(fetchGroupsDetailsIfNeeded()); // dispatch(fetchPreferencesIfNeeded()); - dispatch(fetchMapsDetails()); // dispatch(fetchUnitsDetailsIfNeeded()); // dispatch(fetchConversionsDetailsIfNeeded()); - ConversionArray.fetchPik(); }, []); return ( diff --git a/src/client/app/components/LoginComponent.tsx b/src/client/app/components/LoginComponent.tsx index bfc9fe791..6fedc6c7b 100644 --- a/src/client/app/components/LoginComponent.tsx +++ b/src/client/app/components/LoginComponent.tsx @@ -8,35 +8,44 @@ import { FormattedMessage } from 'react-intl'; import { Button, Form, FormGroup, Input, Label } from 'reactstrap'; import FooterContainer from '../containers/FooterContainer'; import { authApi } from '../redux/api/authApi'; -import { showErrorNotification } from '../utils/notifications'; +import { showErrorNotification, showSuccessNotification } from '../utils/notifications'; import translate from '../utils/translate'; import HeaderComponent from './HeaderComponent'; import { useNavigate } from 'react-router-dom-v5-compat'; + /** * @returns The login page for users or admins. */ export default function LoginComponent() { - const navigate = useNavigate(); - // Local State const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); // Html Element Reference used for focus() const inputRef = useRef(null); + const navigate = useNavigate(); // Grab the derived loginMutation from the API // The naming of the returned objects is arbitrary - const [login] = authApi.useLoginMutation() + // Equivalent Auto-Derived Method + const [login] = authApi.endpoints.login.useMutation(); // authApi.useLoginMutation() const handleSubmit = async (event: React.MouseEvent) => { event.preventDefault(); - const response = await login({ email: 'test@example.com', password: 'password' }).unwrap() - console.log('response ', response) - inputRef.current?.focus() - showErrorNotification(translate('failed.logging.in')); - navigate('/') + await login({ email: email, password: password }) + .unwrap() + .then(() => { + // No error, success! + // TODO Translate + showSuccessNotification('Login Successful') + navigate('/') + }) + .catch(() => { + // Error on login Mutation + inputRef.current?.focus() + showErrorNotification(translate('failed.logging.in')); + }) } return ( diff --git a/src/client/app/components/RouteComponentWIP.tsx b/src/client/app/components/RouteComponentWIP.tsx index 206af8344..99fe1b54e 100644 --- a/src/client/app/components/RouteComponentWIP.tsx +++ b/src/client/app/components/RouteComponentWIP.tsx @@ -2,31 +2,19 @@ * 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 _ from 'lodash'; -import * as moment from 'moment'; -import * as queryString from 'query-string'; import * as React from 'react'; import { IntlProvider } from 'react-intl'; -import { useDispatch } from 'react-redux'; -import { BrowserRouter, Redirect } from 'react-router-dom'; -import { CompatRouter, Route, Routes } from 'react-router-dom-v5-compat'; -import { TimeInterval } from '../../../common/TimeInterval'; -import { LinkOptions, changeOptionsFromLink } from '../actions/graph'; -import CreateUserContainer from '../containers/admin/CreateUserContainer'; -import UsersDetailContainer from '../containers/admin/UsersDetailContainer'; -import UploadCSVContainer from '../containers/csv/UploadCSVContainer'; -import MapCalibrationContainer from '../containers/maps/MapCalibrationContainer'; -import MapsDetailContainer from '../containers/maps/MapsDetailContainer'; +import { BrowserRouter } from 'react-router-dom'; +import { CompatRouter, Navigate, Outlet, Route, Routes } from 'react-router-dom-v5-compat'; import { useAppSelector } from '../redux/hooks'; import { selectCurrentUser } from '../redux/selectors/authSelectors'; import localeData from '../translations/data'; import { UserRole } from '../types/items'; -import { Dispatch } from '../types/redux/actions'; -import { ChartTypes, LineGraphRate, MeterOrGroup } from '../types/redux/graph'; -import { validateComparePeriod, validateSortingOrder } from '../utils/calculateCompare'; -import { hasPermissions } from '../utils/hasPermissions'; -import { showErrorNotification } from '../utils/notifications'; -import translate from '../utils/translate'; +// import { hasPermissions } from '../utils/hasPermissions'; +import CreateUserContainer from '../containers/admin/CreateUserContainer'; +import UploadCSVContainer from '../containers/csv/UploadCSVContainer'; +import MapCalibrationContainer from '../containers/maps/MapCalibrationContainer'; +import MapsDetailContainer from '../containers/maps/MapsDetailContainer'; import HomeComponent from './HomeComponent'; import LoginComponent from './LoginComponent'; import AdminComponent from './admin/AdminComponent'; @@ -38,166 +26,198 @@ import UnitsDetailComponent from './unit/UnitsDetailComponent'; /** * @returns the router component Currently under migration! */ -export default function RouteComponent() { - const dispatch: Dispatch = useDispatch() +export default function RouteComponentWIP() { const lang = useAppSelector(state => state.options.selectedLanguage) - const currentUser = useAppSelector(selectCurrentUser); - const barStacking = useAppSelector(state => state.graph.barStacking); - const areaNormalization = useAppSelector(state => state.graph.areaNormalization); - const minMax = useAppSelector(state => state.graph.showMinMax); - // selectedLanguage: state.options.selectedLanguage, - const requireAuth = (component: JSX.Element) => { - // If state contains token it has been validated on startup or login. - return !currentUser.token ? - - : - component - } - const requireRole = (requiredRole: UserRole, component: JSX.Element) => { - //user is authenticated if token and role in state. - if (currentUser.token && currentUser.profile?.role && hasPermissions(currentUser.profile.role, requiredRole)) { - // If authenticated, and role requires matched return requested component. - return component - } - return - } - const linkToGraph = (query: string) => { - const queries = queryString.parse(query); - if (!_.isEmpty(queries)) { - try { - const options: LinkOptions = {}; - for (const [key, infoObj] of _.entries(queries)) { - // TODO The upgrade of TypeScript lead to it giving an error for the type of infoObj - // which it thinks is unknown. I'm not sure why and this is code from the history - // package (see modules/@types/history/index.d.ts). What follows is a hack where - // the type is cast to any. This removes the problem and also allowed the removal - // of the ! to avoid calling toString when it is a bad value. I think this is okay - // because the toString documentation indicates it works fine with any type including - // null and unknown. If it does convert then the default case will catch it as an error. - // I want to get rid of this issue so Travis testing is not stopped by this. However, - // we should look into this typing issue more to see what might be a better fix. - const fixTypeIssue: any = infoObj as any; - const info: string = fixTypeIssue.toString(); - // ESLint does not want const params in the one case it is used so put here. - let params; - //TODO validation could be implemented across all cases similar to compare period and sorting order - switch (key) { - case 'meterIDs': - options.meterIDs = info.split(',').map(s => parseInt(s)); - break; - case 'groupIDs': - options.groupIDs = info.split(',').map(s => parseInt(s)); - break; - case 'chartType': - options.chartType = info as ChartTypes; - break; - case 'unitID': - options.unitID = parseInt(info); - break; - case 'rate': - params = info.split(','); - options.rate = { label: params[0], rate: parseFloat(params[1]) } as LineGraphRate; - break; - case 'barDuration': - options.barDuration = moment.duration(parseInt(info), 'days'); - break; - case 'barStacking': - if (barStacking.toString() !== info) { - options.toggleBarStacking = true; - } - break; - case 'areaNormalization': - if (areaNormalization.toString() !== info) { - options.toggleAreaNormalization = true; - } - break; - case 'areaUnit': - options.areaUnit = info; - break; - case 'minMax': - if (minMax.toString() !== info) { - options.toggleMinMax = true; - } - break; - case 'comparePeriod': - options.comparePeriod = validateComparePeriod(info); - break; - case 'compareSortingOrder': - options.compareSortingOrder = validateSortingOrder(info); - break; - case 'optionsVisibility': - options.optionsVisibility = (info === 'true'); - break; - case 'mapID': - options.mapID = (parseInt(info)); - break; - case 'serverRange': - options.serverRange = TimeInterval.fromString(info); - /** - * commented out since days from present feature is not currently used - */ - // const index = info.indexOf('dfp'); - // if (index === -1) { - // options.serverRange = TimeInterval.fromString(info); - // } else { - // const message = info.substring(0, index); - // const stringField = this.getNewIntervalFromMessage(message); - // options.serverRange = TimeInterval.fromString(stringField); - // } - break; - case 'sliderRange': - options.sliderRange = TimeInterval.fromString(info); - break; - case 'meterOrGroupID': - options.meterOrGroupID = parseInt(info); - break; - case 'meterOrGroup': - options.meterOrGroup = info as MeterOrGroup; - break; - case 'readingInterval': - options.readingInterval = parseInt(info); - break; - default: - throw new Error('Unknown query parameter'); - } - } - dispatch(changeOptionsFromLink(options)) - } catch (err) { - showErrorNotification(translate('failed.to.link.graph')); - } - } - // All appropriate state updates should've been executed - // redirect to clear the link - return - } + const messages = (localeData as any)[lang]; return ( <> - {/* Compatibility layer for transitioning to react-router 6.X - Checkout https://github.com/remix-run/react-router/discussions/8753 for details */} + {/* + Compatibility layer for transitioning to react-router 6 Checkout https://github.com/remix-run/react-router/discussions/8753 for details + */} + {/* + The largest barrier to completely transitioning is Reworking the Unsaved warning container/component. + is not compatible with react-router v6, and will need to be completely reworked if this migration goes moves forward. + The UnsavedWarningComponent is use in many of the admin routes, so it is likely that they will also need to be reworked. + */} + } /> } /> - - )} /> - )} /> - )} /> - - )} /> - )} /> - )} /> - []} />)} /> - )} /> - )} /> - } /> + {/* Currently Broken rework needed*/} + // Any Route in this must passthrough the admin outlet which checks for authentication status + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + {/* } /> */} + + // Any Route in this must passthrough the admin outlet which checks for authentication status + }> + } /> + + // Redirect any other invalid route to root + } /> ); -} \ No newline at end of file +} +const AdminOutlet = () => { + const currentUser = useAppSelector(selectCurrentUser); + // If state contains token/ userRole it has been validated on startup or login. + if (currentUser.token && currentUser.profile?.role === UserRole.ADMIN) { + // Outlet returns the requested route's component i.e /amin returns etc, + return + } else { + return + } +} + +// Function that returns a JSX element. Either the requested route's Component, as outlet or back to root +const RoleOutlet = ({ UserRole }: { UserRole: UserRole }) => { + const currentUser = useAppSelector(selectCurrentUser); + // If state contains token it has been validated on startup or login. + if (currentUser.profile?.role === UserRole) { + return + } else { + return + } +} +const NotFound = () => { + return +} + + +// const requireRole = (requiredRole: UserRole, component: JSX.Element) => { +// //user is authenticated if token and role in state. +// if (currentUser.token && currentUser.profile?.role && hasPermissions(currentUser.profile.role, requiredRole)) { +// // If authenticated, and role requires matched return requested component. +// return component +// } +// return +// } + + + +// FIX ME +// const linkToGraph = (query: string) => { +// const queries = queryString.parse(query); +// if (!_.isEmpty(queries)) { +// try { +// const options: LinkOptions = {}; +// for (const [key, infoObj] of _.entries(queries)) { +// // TODO The upgrade of TypeScript lead to it giving an error for the type of infoObj +// // which it thinks is unknown. I'm not sure why and this is code from the history +// // package (see modules/@types/history/index.d.ts). What follows is a hack where +// // the type is cast to any. This removes the problem and also allowed the removal +// // of the ! to avoid calling toString when it is a bad value. I think this is okay +// // because the toString documentation indicates it works fine with any type including +// // null and unknown. If it does convert then the default case will catch it as an error. +// // I want to get rid of this issue so Travis testing is not stopped by this. However, +// // we should look into this typing issue more to see what might be a better fix. +// const fixTypeIssue: any = infoObj as any; +// const info: string = fixTypeIssue.toString(); +// // ESLint does not want const params in the one case it is used so put here. +// let params; +// //TODO validation could be implemented across all cases similar to compare period and sorting order +// switch (key) { +// case 'meterIDs': +// options.meterIDs = info.split(',').map(s => parseInt(s)); +// break; +// case 'groupIDs': +// options.groupIDs = info.split(',').map(s => parseInt(s)); +// break; +// case 'chartType': +// options.chartType = info as ChartTypes; +// break; +// case 'unitID': +// options.unitID = parseInt(info); +// break; +// case 'rate': +// params = info.split(','); +// options.rate = { label: params[0], rate: parseFloat(params[1]) } as LineGraphRate; +// break; +// case 'barDuration': +// options.barDuration = moment.duration(parseInt(info), 'days'); +// break; +// case 'barStacking': +// if (barStacking.toString() !== info) { +// options.toggleBarStacking = true; +// } +// break; +// case 'areaNormalization': +// if (areaNormalization.toString() !== info) { +// options.toggleAreaNormalization = true; +// } +// break; +// case 'areaUnit': +// options.areaUnit = info; +// break; +// case 'minMax': +// if (minMax.toString() !== info) { +// options.toggleMinMax = true; +// } +// break; +// case 'comparePeriod': +// options.comparePeriod = validateComparePeriod(info); +// break; +// case 'compareSortingOrder': +// options.compareSortingOrder = validateSortingOrder(info); +// break; +// case 'optionsVisibility': +// options.optionsVisibility = (info === 'true'); +// break; +// case 'mapID': +// options.mapID = (parseInt(info)); +// break; +// case 'serverRange': +// options.serverRange = TimeInterval.fromString(info); +// /** +// * commented out since days from present feature is not currently used +// */ +// // const index = info.indexOf('dfp'); +// // if (index === -1) { +// // options.serverRange = TimeInterval.fromString(info); +// // } else { +// // const message = info.substring(0, index); +// // const stringField = this.getNewIntervalFromMessage(message); +// // options.serverRange = TimeInterval.fromString(stringField); +// // } +// break; +// case 'sliderRange': +// options.sliderRange = TimeInterval.fromString(info); +// break; +// case 'meterOrGroupID': +// options.meterOrGroupID = parseInt(info); +// break; +// case 'meterOrGroup': +// options.meterOrGroup = info as MeterOrGroup; +// break; +// case 'readingInterval': +// options.readingInterval = parseInt(info); +// break; +// default: +// throw new Error('Unknown query parameter'); +// } +// } +// dispatch(changeOptionsFromLink(options)) +// } catch (err) { +// showErrorNotification(translate('failed.to.link.graph')); +// } +// } +// // All appropriate state updates should've been executed +// // redirect to clear the link +// return +// } diff --git a/src/client/app/index.tsx b/src/client/app/index.tsx index 3976134ea..de348b18c 100644 --- a/src/client/app/index.tsx +++ b/src/client/app/index.tsx @@ -8,7 +8,7 @@ import { Provider } from 'react-redux'; import { store } from './store' import 'bootstrap/dist/css/bootstrap.css'; // import RouteContainer from './containers/RouteContainer'; -import RouteComponent from './components/RouteComponentWIP'; +import RouteComponentWIP from './components/RouteComponentWIP'; import './styles/index.css'; import InitializationComponent from './components/InitializationComponent'; @@ -21,6 +21,9 @@ root.render( // Provides the Redux store to all child components - + {/* Route container is a test of react-router-dom v6 + This update introduces many useful routing hooks which can potentially be useful when migrating the codebase to hooks from Class components. + Very much experimental/ Work in Progress */} + ); \ No newline at end of file diff --git a/src/client/app/reducers/currentUser.ts b/src/client/app/reducers/currentUser.ts index 0cf2d8d18..21d9d9eb9 100644 --- a/src/client/app/reducers/currentUser.ts +++ b/src/client/app/reducers/currentUser.ts @@ -38,21 +38,18 @@ export const currentUserSlice = createSlice({ } }, extraReducers: builder => { + // Extra Reducers that listen for actions or endpoints and execute accordingly to update this slice's state. builder - .addMatcher( - userApi.endpoints.getUserDetails.matchFulfilled, + .addMatcher(userApi.endpoints.getUserDetails.matchFulfilled, (state, api) => { state.profile = api.payload - } - ) - .addMatcher( - authApi.endpoints.login.matchFulfilled, + }) + .addMatcher(authApi.endpoints.login.matchFulfilled, (state, api) => { // User has logged in update state, and write to local storage state.profile = { email: api.payload.email, role: api.payload.role } state.token = api.payload.token setToken(state.token) - } - ) + }) } }) \ No newline at end of file diff --git a/src/client/app/redux/api/authApi.ts b/src/client/app/redux/api/authApi.ts index 376d1d1a3..8b9cfbdf4 100644 --- a/src/client/app/redux/api/authApi.ts +++ b/src/client/app/redux/api/authApi.ts @@ -21,6 +21,8 @@ export const authApi = baseApi.injectEndpoints({ // 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'] + // Listeners (ExtraReducers) for this query: + // currentUserSlice->MatchFulfilled }), verifyToken: builder.mutation({ query: queryArgs => ({ diff --git a/src/client/app/redux/api/baseApi.ts b/src/client/app/redux/api/baseApi.ts index 338174024..18cbc6c06 100644 --- a/src/client/app/redux/api/baseApi.ts +++ b/src/client/app/redux/api/baseApi.ts @@ -8,13 +8,15 @@ export const baseApi = createApi({ baseQuery: fetchBaseQuery({ baseUrl: baseHref, prepareHeaders: (headers, { getState }) => { - // For each api call attempt to set the JWT token in the request header const state = getState() as RootState; + // For each api call attempt to set the JWT token in the request header + // Token placed in store either on startup after validation, or via credentialed login if (state.currentUser.token) { headers.set('token', state.currentUser.token) } } }), + // The types of tags that any injected endpoint may, provide, or invalidate. tagTypes: ['MeterData', 'GroupData', 'Preferences'], // Initially no defined endpoints, Use rtk query's injectEndpoints endpoints: () => ({}), diff --git a/src/server/app.js b/src/server/app.js index a29aac42e..b6999a391 100644 --- a/src/server/app.js +++ b/src/server/app.js @@ -85,7 +85,8 @@ app.use(express.static(path.join(__dirname, '..', 'client', 'public'))); const router = express.Router(); -router.get(/^(\/)(login|admin|groups|createGroup|editGroup|graph|meters|maps|calibration|users|csv|units|conversions)?$/, (req, res) => { +// Accept all other endpoint requests which will be handled by the client router +router.get('*', (req, res) => { fs.readFile(path.resolve(__dirname, '..', 'client', 'index.html'), (err, html) => { const subdir = config.subdir || '/'; let htmlPlusData = html.toString().replace('SUBDIR', subdir); From b7e3fa3d0db2759043e01dc7d2464e3a0fe4c6db Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Sat, 30 Sep 2023 21:42:16 +0000 Subject: [PATCH 018/131] useSelector changes - Admin pages Meter, Group, Unit useSelectorChanges. --- .../components/InitializationComponent.tsx | 29 ++++++-- .../app/components/RouteComponentWIP.tsx | 15 +--- .../groups/GroupsDetailComponent.tsx | 73 ++++--------------- .../meters/MetersDetailComponent.tsx | 66 ++++++----------- .../components/unit/UnitsDetailComponent.tsx | 34 ++++----- src/client/app/reducers/groups.ts | 17 ++++- src/client/app/redux/api/authApi.ts | 2 +- src/client/app/redux/api/baseApi.ts | 2 +- src/client/app/redux/api/groupsApi.ts | 6 +- .../app/redux/selectors/authSelectors.ts | 14 ++++ .../app/redux/selectors/dataSelectors.ts | 39 ++++++++++ .../app/redux/selectors/threeDSelectors.ts | 11 +-- src/client/app/redux/selectors/uiSelectors.ts | 8 +- src/client/app/utils/hasPermissions.ts | 2 + 14 files changed, 159 insertions(+), 159 deletions(-) create mode 100644 src/client/app/redux/selectors/dataSelectors.ts diff --git a/src/client/app/components/InitializationComponent.tsx b/src/client/app/components/InitializationComponent.tsx index 5c8304826..4e67fdeb9 100644 --- a/src/client/app/components/InitializationComponent.tsx +++ b/src/client/app/components/InitializationComponent.tsx @@ -15,6 +15,8 @@ import { groupsApi } from '../redux/api/groupsApi'; import { metersApi } from '../redux/api/metersApi'; import { preferencesApi } from '../redux/api/preferencesApi'; import { unitsApi } from '../redux/api/unitsApi'; +import { useAppSelector } from '../redux/hooks'; +import { selectIsLoggedInAsAdmin } from '../redux/selectors/authSelectors'; import { ConversionArray } from '../types/conversionArray'; import { getToken, hasToken } from '../utils/token'; @@ -23,25 +25,36 @@ import { getToken, hasToken } from '../utils/token'; * @returns Initialization JSX element */ export default function InitializationComponent() { - // QueryHooks derived by api endpoint definitions - // These useQuery hooks fetch and cache data to the store as soon the component mounts. + const dispatch: Dispatch = useDispatch(); + const isAdmin = useAppSelector(state => selectIsLoggedInAsAdmin(state)); + + // With RTKQuery, Mutations are used for POST, PUT, PATCH, etc. + // The useMutation() hooks returns a tuple containing triggerFunction that can be called to initiate the request + // and an optional results object containing derived data related the the executed query. + const [verifyTokenTrigger] = authApi.useVerifyTokenMutation() + + // useQueryHooks derived by api endpoint definitions fetch and cache data to the store as soon the component mounts. // They maintain an active subscription to the store so long as the component remains mounted. - // Since this component lives up near the root of the DOM, these queries will remain subscribed indefinitely + // Since this component lives up near the root of the DOM, these queries will remain subscribed indefinitely by default preferencesApi.useGetPreferencesQuery(); unitsApi.useGetUnitsDetailsQuery(); conversionsApi.useGetConversionsDetailsQuery(); conversionsApi.useGetConversionArrayQuery(); metersApi.useGetMetersQuery(); - groupsApi.useGetGroupsQuery(); - // With RTKQuery, Mutations are used for POST, PUT, PATCH, etc. - // The useMutation() hooks return a triggerFunction that can be called to initiate the request. - const [verifyTokenTrigger] = authApi.useVerifyTokenMutation() + // Use Query hooks return an object with various derived values related to the query's status which can be destructured as flows + const { data: groupData, isFetching: groupDataIsFetching } = groupsApi.useGetGroupsQuery(); + + // Queries can be conditionally fetched based if optional parameter skip is true; + // Skip this query if user is not admin + // When user is an admin, ensure that the initial Group data exists and is not currently fetching + groupsApi.useGetAllGroupsChildrenQuery(undefined, { skip: (!isAdmin || !groupData || groupDataIsFetching) }); + + // There are many derived hooks each with different use cases // Read More @ https://redux-toolkit.js.org/rtk-query/api/created-api/hooks#hooks-overview - const dispatch: Dispatch = useDispatch(); // Only run once by making it depend on an empty array. useEffect(() => { diff --git a/src/client/app/components/RouteComponentWIP.tsx b/src/client/app/components/RouteComponentWIP.tsx index 99fe1b54e..6f846a8c7 100644 --- a/src/client/app/components/RouteComponentWIP.tsx +++ b/src/client/app/components/RouteComponentWIP.tsx @@ -40,8 +40,8 @@ export default function RouteComponentWIP() { Compatibility layer for transitioning to react-router 6 Checkout https://github.com/remix-run/react-router/discussions/8753 for details */} {/* - The largest barrier to completely transitioning is Reworking the Unsaved warning container/component. - is not compatible with react-router v6, and will need to be completely reworked if this migration goes moves forward. + The largest barrier to completely transitioning is Reworking the UnsavedWarningComponent. + is not compatible with react-router v6, and will need to be completely reworked if router-migration goes moves forward. The UnsavedWarningComponent is use in many of the admin routes, so it is likely that they will also need to be reworked. */} @@ -100,17 +100,6 @@ const NotFound = () => { } -// const requireRole = (requiredRole: UserRole, component: JSX.Element) => { -// //user is authenticated if token and role in state. -// if (currentUser.token && currentUser.profile?.role && hasPermissions(currentUser.profile.role, requiredRole)) { -// // If authenticated, and role requires matched return requested component. -// return component -// } -// return -// } - - - // FIX ME // const linkToGraph = (query: string) => { // const queries = queryString.parse(query); diff --git a/src/client/app/components/groups/GroupsDetailComponent.tsx b/src/client/app/components/groups/GroupsDetailComponent.tsx index 06fb4d307..b9b9d9e95 100644 --- a/src/client/app/components/groups/GroupsDetailComponent.tsx +++ b/src/client/app/components/groups/GroupsDetailComponent.tsx @@ -3,77 +3,36 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import FooterContainer from '../../containers/FooterContainer'; -import TooltipMarkerComponent from '../TooltipMarkerComponent'; import { FormattedMessage } from 'react-intl'; +import HeaderComponent from '../../components/HeaderComponent'; +import FooterContainer from '../../containers/FooterContainer'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; -import { useDispatch, useSelector } from 'react-redux'; -import { useEffect } from 'react'; -import { State } from '../../types/redux/state'; -import { fetchAllGroupChildrenIfNeeded, fetchGroupsDetailsIfNeeded } from '../../actions/groups'; -import { fetchMetersDetailsIfNeeded } from '../../actions/meters'; -import { isRoleAdmin } from '../../utils/hasPermissions'; +import { useAppSelector } from '../../redux/hooks'; +import { selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; +import { selectUnitDataById, selectVisibleMetersGroupsDataByID } from '../../redux/selectors/dataSelectors'; +import { GroupDefinition } from '../../types/redux/groups'; import { potentialGraphicUnits } from '../../utils/input'; -import GroupViewComponent from './GroupViewComponent'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateGroupModalComponent from './CreateGroupModalComponent'; -import { GroupDefinition } from 'types/redux/groups'; -import * as _ from 'lodash'; -import HeaderComponent from '../../components/HeaderComponent'; -import { Dispatch } from 'types/redux/actions'; +import GroupViewComponent from './GroupViewComponent'; /** * Defines the groups page card view * @returns Groups page element */ export default function GroupsDetailComponent() { - const dispatch: Dispatch = useDispatch(); - // Groups state - const groupsState = useSelector((state: State) => state.groups.byGroupID); - // Groups state loaded status - const groupsStateLoaded = useSelector((state: State) => state.groups.hasBeenFetchedOnce); - // The immediate children of groups is loaded separately. - const groupsAllChildrenLoaded = useSelector((state: State) => state.groups.hasChildrenBeenFetchedOnce); // Check for admin status - const currentUser = useSelector((state: State) => state.currentUser.profile); - const loggedInAsAdmin = (currentUser !== null) && isRoleAdmin(currentUser.role); + const isAdmin = useAppSelector(state => selectIsLoggedInAsAdmin(state)); - // We only want displayable groups if non-admins because they still have - // non-displayable in state. - let visibleGroups; - if (loggedInAsAdmin) { - visibleGroups = groupsState; - } else { - visibleGroups = _.filter(groupsState, (group: GroupDefinition) => { - return group.displayable === true - }); - } + // We only want displayable groups if non-admins because they still have non-displayable in state. + const { visibleGroups } = useAppSelector(state => selectVisibleMetersGroupsDataByID(state)); // Units state - const units = useSelector((state: State) => state.units.units); - // Units state loaded status - const unitsStateLoaded = useSelector((state: State) => state.units.hasBeenFetchedOnce); - // Units state loaded status - const metersStateLoaded = useSelector((state: State) => state.meters.hasBeenFetchedOnce); - - useEffect(() => { - // Note each modal is created for each group when the details are created so get all state now for all groups. - // Get meter details if needed - dispatch(fetchMetersDetailsIfNeeded()) - // Makes async call to groups API for groups details if one has not already been made somewhere else, stores group ids in state - dispatch(fetchGroupsDetailsIfNeeded()); - // TODO Is there a good way to integrate this into the actions so it must work correctly? - // You need the basic group state loaded since going to modify. - if (groupsStateLoaded) { - // Get all groups' meter and group immediate children into state. Since all modals done for all groups - // we get them all here. - dispatch(fetchAllGroupChildrenIfNeeded()); - } - // In case the group state was not yet loaded you need to do this again. - }, [groupsStateLoaded, groupsAllChildrenLoaded]); + const unitDataById = useAppSelector(state => selectUnitDataById(state)); // Possible graphic units to use - const possibleGraphicUnits = potentialGraphicUnits(units); + const possibleGraphicUnits = potentialGraphicUnits(unitDataById); const titleStyle: React.CSSProperties = { textAlign: 'center' @@ -83,7 +42,7 @@ export default function GroupsDetailComponent() { display: 'inline-block', fontSize: '50%', // Switch help depending if admin or not. - tooltipGroupView: loggedInAsAdmin ? 'help.admin.groupview' : 'help.groups.groupview' + tooltipGroupView: isAdmin ? 'help.admin.groupview' : 'help.groups.groupview' }; return ( @@ -99,7 +58,7 @@ export default function GroupsDetailComponent() { - {loggedInAsAdmin && groupsStateLoaded && + {isAdmin &&
{/* The actual button for create is inside this component. */} < CreateGroupModalComponent @@ -107,7 +66,7 @@ export default function GroupsDetailComponent() { />
} - {groupsAllChildrenLoaded && groupsStateLoaded && unitsStateLoaded && metersStateLoaded && + {
{/* Create a GroupViewComponent for each groupData in Groups State after sorting by name */} {Object.values(visibleGroups) diff --git a/src/client/app/components/meters/MetersDetailComponent.tsx b/src/client/app/components/meters/MetersDetailComponent.tsx index 3abb7f217..8cf043595 100644 --- a/src/client/app/components/meters/MetersDetailComponent.tsx +++ b/src/client/app/components/meters/MetersDetailComponent.tsx @@ -2,70 +2,46 @@ * 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 _ from 'lodash'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; +import HeaderComponent from '../../components/HeaderComponent'; import FooterContainer from '../../containers/FooterContainer'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; -import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import { State } from '../../types/redux/state'; -import { useDispatch, useSelector } from 'react-redux'; -import { useEffect } from 'react'; -import { fetchMetersDetailsIfNeeded } from '../../actions/meters'; -import { isRoleAdmin } from '../../utils/hasPermissions'; -import MeterViewComponent from './MeterViewComponent'; -import CreateMeterModalComponent from './CreateMeterModalComponent'; -import { MeterData } from 'types/redux/meters'; +import { useAppSelector } from '../../redux/hooks'; +import { selectCurrentUser, selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; +import { selectUnitDataById, selectVisibleMetersGroupsDataByID } from '../../redux/selectors/dataSelectors'; import '../../styles/card-page.css'; +import { MeterData } from '../../types/redux/meters'; import { UnitData, UnitType } from '../../types/redux/units'; -import * as _ from 'lodash'; -import { potentialGraphicUnits, noUnitTranslated } from '../../utils/input'; -import HeaderComponent from '../../components/HeaderComponent'; -import { Dispatch } from 'types/redux/actions'; +import { noUnitTranslated, potentialGraphicUnits } from '../../utils/input'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; +import CreateMeterModalComponent from './CreateMeterModalComponent'; +import MeterViewComponent from './MeterViewComponent'; /** * Defines the meters page card view * @returns Meters page element */ export default function MetersDetailComponent() { - - const dispatch: Dispatch = useDispatch(); - - useEffect(() => { - // Makes async call to Meters API for Meters details if one has not already been made somewhere else, stores Meter ids in state - dispatch(fetchMetersDetailsIfNeeded()); - }, []); - - // Meters state - const MetersState = useSelector((state: State) => state.meters.byMeterID); - // Meters state loaded status - const metersStateLoaded = useSelector((state: State) => state.meters.hasBeenFetchedOnce); // current user state - const currentUserState = useSelector((state: State) => state.currentUser); + const currentUserState = useAppSelector(state => selectCurrentUser(state)); // Check for admin status - const currentUser = currentUserState.profile; - const loggedInAsAdmin = (currentUser !== null) && isRoleAdmin(currentUser.role); + const isAdmin = useAppSelector(state => selectIsLoggedInAsAdmin(state)); // We only want displayable meters if non-admins because they still have // non-displayable in state. - let visibleMeters; - if (loggedInAsAdmin) { - visibleMeters = MetersState; - } else { - visibleMeters = _.filter(MetersState, (meter: MeterData) => { - return meter.displayable === true - }); - } + const { visibleMeters } = useAppSelector(state => selectVisibleMetersGroupsDataByID(state)); // Units state - const units = useSelector((state: State) => state.units.units); - // Units state loaded status - const unitsStateLoaded = useSelector((state: State) => state.units.hasBeenFetchedOnce); + const unitDataById = useAppSelector(state => selectUnitDataById(state)); + // TODO? Convert into Selector? // Possible Meter Units to use let possibleMeterUnits = new Set(); // The meter unit can be any unit of type meter. - Object.values(units).forEach(unit => { + Object.values(unitDataById).forEach(unit => { if (unit.typeOfUnit == UnitType.meter) { possibleMeterUnits.add(unit); } @@ -76,7 +52,7 @@ export default function MetersDetailComponent() { possibleMeterUnits.add(noUnitTranslated()); // Possible graphic units to use - const possibleGraphicUnits = potentialGraphicUnits(units); + const possibleGraphicUnits = potentialGraphicUnits(unitDataById); const titleStyle: React.CSSProperties = { textAlign: 'center' @@ -86,7 +62,7 @@ export default function MetersDetailComponent() { display: 'inline-block', fontSize: '50%', // Switch help depending if admin or not. - tooltipMeterView: loggedInAsAdmin ? 'help.admin.meterview' : 'help.meters.meterview' + tooltipMeterView: isAdmin ? 'help.admin.meterview' : 'help.meters.meterview' }; return ( @@ -101,7 +77,7 @@ export default function MetersDetailComponent() {
- {loggedInAsAdmin && metersStateLoaded && unitsStateLoaded && + {isAdmin &&
{/* The actual button for create is inside this component. */}
} - {metersStateLoaded && unitsStateLoaded && + {
{/* Create a MeterViewComponent for each MeterData in Meters State after sorting by identifier */} {Object.values(visibleMeters) @@ -127,6 +103,6 @@ export default function MetersDetailComponent() { }
- + ); } diff --git a/src/client/app/components/unit/UnitsDetailComponent.tsx b/src/client/app/components/unit/UnitsDetailComponent.tsx index 06e126df1..22bdcdf01 100644 --- a/src/client/app/components/unit/UnitsDetailComponent.tsx +++ b/src/client/app/components/unit/UnitsDetailComponent.tsx @@ -1,21 +1,20 @@ /* 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 { FormattedMessage } from 'react-intl'; +import { useSelector } from 'react-redux'; +import { UnitData } from 'types/redux/units'; +import HeaderComponent from '../../components/HeaderComponent'; +import SpinnerComponent from '../../components/SpinnerComponent'; import FooterContainer from '../../containers/FooterContainer'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; -import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import * as React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { useEffect } from 'react'; +import { useAppSelector } from '../../redux/hooks'; +import { selectUnitDataById } from '../../redux/selectors/dataSelectors'; import { State } from '../../types/redux/state'; -import { fetchUnitsDetailsIfNeeded } from '../../actions/units'; -import UnitViewComponent from './UnitViewComponent'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateUnitModalComponent from './CreateUnitModalComponent'; -import { UnitData } from 'types/redux/units'; -import SpinnerComponent from '../../components/SpinnerComponent'; -import HeaderComponent from '../../components/HeaderComponent'; -import { Dispatch } from 'types/redux/actions'; +import UnitViewComponent from './UnitViewComponent'; /** * Defines the units page card view @@ -23,18 +22,11 @@ import { Dispatch } from 'types/redux/actions'; */ export default function UnitsDetailComponent() { // The route stops you from getting to this page if not an admin. - - const dispatch: Dispatch = useDispatch(); - - useEffect(() => { - // Makes async call to units API for units details if one has not already been made somewhere else, stores unit ids in state - dispatch(fetchUnitsDetailsIfNeeded()); - }, []); - const isUpdatingCikAndDBViews = useSelector((state: State) => state.admin.isUpdatingCikAndDBViews); //Units state - const unitsState = useSelector((state: State) => state.units.units); + const unitDataById = useAppSelector(state => selectUnitDataById(state)); + const titleStyle: React.CSSProperties = { textAlign: 'center' @@ -49,7 +41,7 @@ export default function UnitsDetailComponent() { return (
- { isUpdatingCikAndDBViews ? ( + {isUpdatingCikAndDBViews ? (
@@ -72,7 +64,7 @@ export default function UnitsDetailComponent() {
{/* Create a UnitViewComponent for each UnitData in Units State after sorting by identifier */} - {Object.values(unitsState) + {Object.values(unitDataById) .sort((unitA: UnitData, unitB: UnitData) => (unitA.identifier.toLowerCase() > unitB.identifier.toLowerCase()) ? 1 : ((unitB.identifier.toLowerCase() > unitA.identifier.toLowerCase()) ? -1 : 0)) .map(unitData => ())} diff --git a/src/client/app/reducers/groups.ts b/src/client/app/reducers/groups.ts index a8635e40a..7774c067e 100644 --- a/src/client/app/reducers/groups.ts +++ b/src/client/app/reducers/groups.ts @@ -97,6 +97,8 @@ export const groupsSlice = createSlice({ }; } }, + // TODO Much of this logic is duplicated due to migration trying not to change too much at once. + // When no longer needed remove base reducers if applicable, or delete slice entirely and rely solely on api cache extraReducers: builder => { builder.addMatcher(groupsApi.endpoints.getGroups.matchFulfilled, (state, { payload }) => { @@ -120,7 +122,18 @@ export const groupsSlice = createSlice({ state.isFetching = false; // TODO FIX TYPES HERE Weird interaction here state.byGroupID = _.keyBy(newGroups, 'id'); - } - ) + }) + .addMatcher(groupsApi.endpoints.getAllGroupsChildren.matchFulfilled, + (state, action) => { + // For each group that received data, set the children meters and groups. + for (const groupInfo of action.payload) { + // Group id of the current item + const groupId = groupInfo.groupId; + // Reset the newState for this group to have child meters/groups. + state.byGroupID[groupId].childMeters = groupInfo.childMeters; + state.byGroupID[groupId].childGroups = groupInfo.childGroups; + } + }) + } }); \ No newline at end of file diff --git a/src/client/app/redux/api/authApi.ts b/src/client/app/redux/api/authApi.ts index 8b9cfbdf4..d78b3210e 100644 --- a/src/client/app/redux/api/authApi.ts +++ b/src/client/app/redux/api/authApi.ts @@ -24,7 +24,7 @@ export const authApi = baseApi.injectEndpoints({ // Listeners (ExtraReducers) for this query: // currentUserSlice->MatchFulfilled }), - verifyToken: builder.mutation({ + verifyToken: builder.mutation<{ success: boolean }, string>({ query: queryArgs => ({ url: 'api/verification', method: 'POST', diff --git a/src/client/app/redux/api/baseApi.ts b/src/client/app/redux/api/baseApi.ts index 18cbc6c06..54eda82f9 100644 --- a/src/client/app/redux/api/baseApi.ts +++ b/src/client/app/redux/api/baseApi.ts @@ -17,7 +17,7 @@ export const baseApi = createApi({ } }), // The types of tags that any injected endpoint may, provide, or invalidate. - tagTypes: ['MeterData', 'GroupData', 'Preferences'], + tagTypes: ['MeterData', 'GroupData', 'GroupChildrenData', 'Preferences'], // Initially no defined endpoints, Use rtk query's injectEndpoints endpoints: () => ({}), // Keep Data in Cache for 10 Minutes (600 seconds) diff --git a/src/client/app/redux/api/groupsApi.ts b/src/client/app/redux/api/groupsApi.ts index 2d95f0694..34ef9e955 100644 --- a/src/client/app/redux/api/groupsApi.ts +++ b/src/client/app/redux/api/groupsApi.ts @@ -1,11 +1,15 @@ import { baseApi } from './baseApi' -import { GroupDetailsData } from '../../types/redux/groups' +import { GroupChildren, GroupDetailsData } from '../../types/redux/groups' export const groupsApi = baseApi.injectEndpoints({ endpoints: builder => ({ getGroups: builder.query({ query: () => 'api/groups', providesTags: ['GroupData'] + }), + getAllGroupsChildren: builder.query({ + query: () => 'api/groups/allChildren', + providesTags: ['GroupChildrenData'] }) }) }) diff --git a/src/client/app/redux/selectors/authSelectors.ts b/src/client/app/redux/selectors/authSelectors.ts index 2900e539e..0d411a1a4 100644 --- a/src/client/app/redux/selectors/authSelectors.ts +++ b/src/client/app/redux/selectors/authSelectors.ts @@ -1,2 +1,16 @@ +import { createSelector } from '@reduxjs/toolkit'; import { RootState } from '../../store'; +import { UserRole } from '../../types/items'; + export const selectCurrentUser = (state: RootState) => state.currentUser; + +// Memoized Selectors for stable obj reference from derived Values +export const selectIsLoggedInAsAdmin = createSelector( + selectCurrentUser, + currentUser => { + // True of token in state, and has Admin Role. + // Token If token is in state, it has been validated upon app initialization, or by login verification + // Type looked weird without boolean + return (currentUser.token && currentUser.profile?.role === UserRole.ADMIN) as boolean + } +) diff --git a/src/client/app/redux/selectors/dataSelectors.ts b/src/client/app/redux/selectors/dataSelectors.ts new file mode 100644 index 000000000..8ad0ca9fc --- /dev/null +++ b/src/client/app/redux/selectors/dataSelectors.ts @@ -0,0 +1,39 @@ +import { createSelector } from '@reduxjs/toolkit'; +import * as _ from 'lodash'; +import { RootState } from '../../store'; +import { GroupDefinition } from '../../types/redux/groups'; +import { MeterData } from '../../types/redux/meters'; +import { selectIsLoggedInAsAdmin } from './authSelectors'; + + +export const selectMeterDataByID = (state: RootState) => state.meters.byMeterID; +export const selectGroupDataByID = (state: RootState) => state.groups.byGroupID; +export const selectUnitDataById = (state: RootState) => state.units.units; + +export const selectMeterState = (state: RootState) => state.meters; +export const selectGroupState = (state: RootState) => state.groups; +export const selectUnitState = (state: RootState) => state.units; +export const selectMapState = (state: RootState) => state.maps; + +export const selectVisibleMetersGroupsDataByID = createSelector( + selectMeterDataByID, + selectGroupDataByID, + selectIsLoggedInAsAdmin, + (meterDataByID, groupDataByID, isAdmin) => { + let visibleMeters; + let visibleGroups; + if (isAdmin) { + visibleMeters = meterDataByID + visibleGroups = groupDataByID; + } else { + visibleMeters = _.filter(meterDataByID, (meter: MeterData) => { + return meter.displayable === true + }); + visibleGroups = _.filter(groupDataByID, (group: GroupDefinition) => { + return group.displayable === true + }); + } + + return { visibleMeters, visibleGroups } + } +) \ No newline at end of file diff --git a/src/client/app/redux/selectors/threeDSelectors.ts b/src/client/app/redux/selectors/threeDSelectors.ts index d142cf31b..83a569c0a 100644 --- a/src/client/app/redux/selectors/threeDSelectors.ts +++ b/src/client/app/redux/selectors/threeDSelectors.ts @@ -1,10 +1,11 @@ import { createSelector } from '@reduxjs/toolkit'; -import { RootState } from '../../store' -import { MeterOrGroup } from '../../types/redux/graph' -import { AreaUnitType } from '../../utils/getAreaUnitConversion'; +import { RootState } from '../../store'; +import { MeterOrGroup } from '../../types/redux/graph'; import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; -import { ThreeDReadingApiParams } from '../api/readingsApi' -import { selectGraphUnitID, selectGraphTimeInterval, selectMeterState, selectGroupState } from '../selectors/uiSelectors' +import { AreaUnitType } from '../../utils/getAreaUnitConversion'; +import { ThreeDReadingApiParams } from '../api/readingsApi'; +import { selectGraphTimeInterval, selectGraphUnitID } from '../selectors/uiSelectors'; +import { selectGroupState, selectMeterState } from './dataSelectors'; // Common Fine Grained selectors const selectThreeDMeterOrGroupID = (state: RootState) => state.graph.threeD.meterOrGroupID; diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts index 3e994f627..73d23cdc6 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -18,19 +18,17 @@ import { } from '../../utils/calibration'; import { metersInGroup, unitsCompatibleWithMeters } from '../../utils/determineCompatibleUnits'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; +import { selectCurrentUser } from './authSelectors'; +import { selectGroupState, selectMapState, selectMeterState, selectUnitState } from './dataSelectors'; export const selectSelectedMeters = (state: RootState) => state.graph.selectedMeters; export const selectSelectedGroups = (state: RootState) => state.graph.selectedGroups; -export const selectCurrentUser = (state: RootState) => state.currentUser; export const selectGraphTimeInterval = (state: RootState) => state.graph.timeInterval; export const selectGraphUnitID = (state: RootState) => state.graph.selectedUnit; export const selectGraphAreaNormalization = (state: RootState) => state.graph.areaNormalization; export const selectChartToRender = (state: RootState) => state.graph.chartToRender; -export const selectMeterState = (state: RootState) => state.meters; -export const selectGroupState = (state: RootState) => state.groups; -export const selectUnitState = (state: RootState) => state.units; -export const selectMapState = (state: RootState) => state.maps; + export const selectVisibleMetersAndGroups = createSelector( [selectMeterState, selectGroupState, selectCurrentUser], diff --git a/src/client/app/utils/hasPermissions.ts b/src/client/app/utils/hasPermissions.ts index b95d80720..f3133caa9 100644 --- a/src/client/app/utils/hasPermissions.ts +++ b/src/client/app/utils/hasPermissions.ts @@ -21,5 +21,7 @@ export function hasPermissions(user: UserRole, compareTo: UserRole): boolean { * @returns Whether or not user is an admin */ export function isRoleAdmin(user: UserRole): boolean { + // TODO Already Converted to a selector + // migrate all references to this method to use selectIsLoggedInAsAdmin from authSelectors.ts return user === UserRole.ADMIN; } From 8b5765169f4b0419f5fb430faa958c16126fbc49 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Sun, 1 Oct 2023 19:15:21 +0000 Subject: [PATCH 019/131] Routing Changes to Admin and Role Routes. --- src/client/app/actions/graph.ts | 22 +- .../app/components/RouteComponentWIP.tsx | 307 ++++++++++-------- src/client/app/reducers/graph.ts | 12 + 3 files changed, 192 insertions(+), 149 deletions(-) diff --git a/src/client/app/actions/graph.ts b/src/client/app/actions/graph.ts index a761af5c8..3424b798b 100644 --- a/src/client/app/actions/graph.ts +++ b/src/client/app/actions/graph.ts @@ -3,22 +3,22 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as moment from 'moment'; -import { fetchMetersDetailsIfNeeded } from './meters'; -import { fetchGroupsDetailsIfNeeded } from './groups'; -import { fetchNeededLineReadings } from './lineReadings'; -import { fetchNeededBarReadings } from './barReadings'; -import { fetchNeededCompareReadings } from './compareReadings'; import { TimeInterval } from '../../../common/TimeInterval'; -import { Dispatch, Thunk, GetState } from '../types/redux/actions'; -import { State } from '../types/redux/state'; +import { graphSlice } from '../reducers/graph'; +import { Dispatch, GetState, Thunk } from '../types/redux/actions'; import * as t from '../types/redux/graph'; import * as m from '../types/redux/map'; +import { State } from '../types/redux/state'; import { ComparePeriod, SortingOrder } from '../utils/calculateCompare'; +import { AreaUnitType } from '../utils/getAreaUnitConversion'; +import { fetchNeededBarReadings } from './barReadings'; +import { fetchNeededCompareReadings } from './compareReadings'; +import { fetchGroupsDetailsIfNeeded } from './groups'; +import { fetchNeededLineReadings } from './lineReadings'; +import { changeSelectedMap } from './map'; import { fetchNeededMapReadings } from './mapReadings'; -import { changeSelectedMap, fetchMapsDetails } from './map'; +import { fetchMetersDetailsIfNeeded } from './meters'; import { fetchUnitsDetailsIfNeeded } from './units'; -import { AreaUnitType } from '../utils/getAreaUnitConversion'; -import { graphSlice } from '../reducers/graph'; export function setHotlinkedAsync(hotlinked: boolean): Thunk { return (dispatch: Dispatch) => { @@ -256,7 +256,7 @@ export function changeOptionsFromLink(options: LinkOptions) { } if (options.mapID) { // TODO here and elsewhere should be IfNeeded but need to check that all state updates are done when edit, etc. - dispatchFirst.push(fetchMapsDetails()); + // TODO Not currently working with RTK migration dispatchSecond.push(changeSelectedMap(options.mapID)); } if (options.readingInterval) { diff --git a/src/client/app/components/RouteComponentWIP.tsx b/src/client/app/components/RouteComponentWIP.tsx index 6f846a8c7..492119e7d 100644 --- a/src/client/app/components/RouteComponentWIP.tsx +++ b/src/client/app/components/RouteComponentWIP.tsx @@ -2,21 +2,31 @@ * 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 } from '@reduxjs/toolkit'; +import * as moment from 'moment'; import * as React from 'react'; import { IntlProvider } from 'react-intl'; import { BrowserRouter } from 'react-router-dom'; -import { CompatRouter, Navigate, Outlet, Route, Routes } from 'react-router-dom-v5-compat'; -import { useAppSelector } from '../redux/hooks'; -import { selectCurrentUser } from '../redux/selectors/authSelectors'; -import localeData from '../translations/data'; -import { UserRole } from '../types/items'; -// import { hasPermissions } from '../utils/hasPermissions'; +import { CompatRouter, Navigate, Outlet, Route, Routes, useSearchParams } from 'react-router-dom-v5-compat'; +import { TimeInterval } from '../../../common/TimeInterval'; import CreateUserContainer from '../containers/admin/CreateUserContainer'; import UploadCSVContainer from '../containers/csv/UploadCSVContainer'; import MapCalibrationContainer from '../containers/maps/MapCalibrationContainer'; import MapsDetailContainer from '../containers/maps/MapsDetailContainer'; +import { graphSlice } from '../reducers/graph'; +import { baseApi } from '../redux/api/baseApi'; +import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { selectCurrentUser, selectIsLoggedInAsAdmin } from '../redux/selectors/authSelectors'; +import localeData from '../translations/data'; +import { UserRole } from '../types/items'; +import { ChartTypes, LineGraphRate, MeterOrGroup } from '../types/redux/graph'; +import { validateComparePeriod, validateSortingOrder } from '../utils/calculateCompare'; +import { AreaUnitType } from '../utils/getAreaUnitConversion'; +import { showErrorNotification } from '../utils/notifications'; +import translate from '../utils/translate'; import HomeComponent from './HomeComponent'; import LoginComponent from './LoginComponent'; +import SpinnerComponent from './SpinnerComponent'; import AdminComponent from './admin/AdminComponent'; import ConversionsDetailComponent from './conversion/ConversionsDetailComponent'; import GroupsDetailComponent from './groups/GroupsDetailComponent'; @@ -36,20 +46,17 @@ export default function RouteComponentWIP() { <> - {/* - Compatibility layer for transitioning to react-router 6 Checkout https://github.com/remix-run/react-router/discussions/8753 for details + {/* Compatibility layer for transitioning to react-router 6 Checkout https://github.com/remix-run/react-router/discussions/8753 */} + {/* + The largest barrier to completely transitioning is Reworking the UnsavedWarningComponent. + is not compatible with react-router v6, and will need to be completely reworked if router-migration goes moves forward. + The UnsavedWarningComponent is use in many of the admin routes, so it is likely that they will also need to be reworked. */} - {/* - The largest barrier to completely transitioning is Reworking the UnsavedWarningComponent. - is not compatible with react-router v6, and will need to be completely reworked if router-migration goes moves forward. - The UnsavedWarningComponent is use in many of the admin routes, so it is likely that they will also need to be reworked. - */} - } /> } /> - {/* Currently Broken rework needed*/} - // Any Route in this must passthrough the admin outlet which checks for authentication status + } /> + {/* // Any Route in this must passthrough the admin outlet which checks for authentication status */} }> } /> } /> @@ -61,11 +68,10 @@ export default function RouteComponentWIP() { } /> {/* } /> */} - // Any Route in this must passthrough the admin outlet which checks for authentication status }> } /> - // Redirect any other invalid route to root + {/* // Redirect any other invalid route to root */} } /> @@ -74,139 +80,164 @@ export default function RouteComponentWIP() { ); } + +const useWaitForInit = () => { + const dispatch = useAppDispatch(); + const isAdmin = useAppSelector(state => selectIsLoggedInAsAdmin(state)); + const currentUser = useAppSelector(state => selectCurrentUser(state)); + const [initComplete, setInitComplete] = React.useState(false); + + React.useEffect(() => { + // Initialization sequence if not navigating here from the ui. + // E.g entering 'localhost:3000/groups' into the browser nav bar etc.. + const waitForInit = async () => { + await Promise.all(dispatch(baseApi.util.getRunningQueriesThunk())) + setInitComplete(true) + // TODO Fix crashing in components on startup if data has yet to be returned, for now readyToNav works. + // This Could be avoided if these components were written to handle such cases upon startup + } + + waitForInit(); + }, []); + return { isAdmin, currentUser, initComplete } +} + const AdminOutlet = () => { - const currentUser = useAppSelector(selectCurrentUser); - // If state contains token/ userRole it has been validated on startup or login. - if (currentUser.token && currentUser.profile?.role === UserRole.ADMIN) { - // Outlet returns the requested route's component i.e /amin returns etc, + const { isAdmin, initComplete } = useWaitForInit(); + + if (!initComplete) { + // Return a spinner until all init queries return and populate cache with data + return + } + + if (isAdmin) { return - } else { - return } + + // No other cases means user doesn't have the permissions. + return + } // Function that returns a JSX element. Either the requested route's Component, as outlet or back to root const RoleOutlet = ({ UserRole }: { UserRole: UserRole }) => { - const currentUser = useAppSelector(selectCurrentUser); + const { currentUser, initComplete } = useWaitForInit(); // If state contains token it has been validated on startup or login. + if (!initComplete) { + return + } + if (currentUser.profile?.role === UserRole) { return - } else { - return } + return } + const NotFound = () => { - return + return } -// FIX ME -// const linkToGraph = (query: string) => { -// const queries = queryString.parse(query); -// if (!_.isEmpty(queries)) { -// try { -// const options: LinkOptions = {}; -// for (const [key, infoObj] of _.entries(queries)) { -// // TODO The upgrade of TypeScript lead to it giving an error for the type of infoObj -// // which it thinks is unknown. I'm not sure why and this is code from the history -// // package (see modules/@types/history/index.d.ts). What follows is a hack where -// // the type is cast to any. This removes the problem and also allowed the removal -// // of the ! to avoid calling toString when it is a bad value. I think this is okay -// // because the toString documentation indicates it works fine with any type including -// // null and unknown. If it does convert then the default case will catch it as an error. -// // I want to get rid of this issue so Travis testing is not stopped by this. However, -// // we should look into this typing issue more to see what might be a better fix. -// const fixTypeIssue: any = infoObj as any; -// const info: string = fixTypeIssue.toString(); -// // ESLint does not want const params in the one case it is used so put here. -// let params; -// //TODO validation could be implemented across all cases similar to compare period and sorting order -// switch (key) { -// case 'meterIDs': -// options.meterIDs = info.split(',').map(s => parseInt(s)); -// break; -// case 'groupIDs': -// options.groupIDs = info.split(',').map(s => parseInt(s)); -// break; -// case 'chartType': -// options.chartType = info as ChartTypes; -// break; -// case 'unitID': -// options.unitID = parseInt(info); -// break; -// case 'rate': -// params = info.split(','); -// options.rate = { label: params[0], rate: parseFloat(params[1]) } as LineGraphRate; -// break; -// case 'barDuration': -// options.barDuration = moment.duration(parseInt(info), 'days'); -// break; -// case 'barStacking': -// if (barStacking.toString() !== info) { -// options.toggleBarStacking = true; -// } -// break; -// case 'areaNormalization': -// if (areaNormalization.toString() !== info) { -// options.toggleAreaNormalization = true; -// } -// break; -// case 'areaUnit': -// options.areaUnit = info; -// break; -// case 'minMax': -// if (minMax.toString() !== info) { -// options.toggleMinMax = true; -// } -// break; -// case 'comparePeriod': -// options.comparePeriod = validateComparePeriod(info); -// break; -// case 'compareSortingOrder': -// options.compareSortingOrder = validateSortingOrder(info); -// break; -// case 'optionsVisibility': -// options.optionsVisibility = (info === 'true'); -// break; -// case 'mapID': -// options.mapID = (parseInt(info)); -// break; -// case 'serverRange': -// options.serverRange = TimeInterval.fromString(info); -// /** -// * commented out since days from present feature is not currently used -// */ -// // const index = info.indexOf('dfp'); -// // if (index === -1) { -// // options.serverRange = TimeInterval.fromString(info); -// // } else { -// // const message = info.substring(0, index); -// // const stringField = this.getNewIntervalFromMessage(message); -// // options.serverRange = TimeInterval.fromString(stringField); -// // } -// break; -// case 'sliderRange': -// options.sliderRange = TimeInterval.fromString(info); -// break; -// case 'meterOrGroupID': -// options.meterOrGroupID = parseInt(info); -// break; -// case 'meterOrGroup': -// options.meterOrGroup = info as MeterOrGroup; -// break; -// case 'readingInterval': -// options.readingInterval = parseInt(info); -// break; -// default: -// throw new Error('Unknown query parameter'); -// } -// } -// dispatch(changeOptionsFromLink(options)) -// } catch (err) { -// showErrorNotification(translate('failed.to.link.graph')); -// } -// } -// // All appropriate state updates should've been executed -// // redirect to clear the link -// return -// } +// TODO fix this route +const GraphLink = () => { + const dispatch = useAppDispatch(); + const [URLSearchParams] = useSearchParams(); + const { initComplete } = useWaitForInit(); + const dispatchQueue: PayloadAction[] = []; + try { + URLSearchParams.forEach((value, key) => { + //TODO validation could be implemented across all cases similar to compare period and sorting order + switch (key) { + case 'meterIDs': + dispatchQueue.push(graphSlice.actions.updateSelectedMeters(value.split(',').map(s => parseInt(s)))) + break; + case 'groupIDs': + dispatchQueue.push(graphSlice.actions.updateSelectedGroups(value.split(',').map(s => parseInt(s)))) + break; + case 'chartType': + dispatchQueue.push(graphSlice.actions.changeChartToRender(value as ChartTypes)) + break; + case 'unitID': + dispatchQueue.push(graphSlice.actions.updateSelectedUnit(parseInt(value))) + break; + case 'rate': + { + const params = value.split(','); + const rate = { label: params[0], rate: parseFloat(params[1]) } as LineGraphRate; + dispatchQueue.push(graphSlice.actions.updateLineGraphRate(rate)) + } + break; + case 'barDuration': + dispatchQueue.push(graphSlice.actions.updateBarDuration(moment.duration(parseInt(value), 'days'))) + break; + case 'barStacking': + dispatchQueue.push(graphSlice.actions.setBarStacking(Boolean(value))) + break; + case 'areaNormalization': + dispatchQueue.push(graphSlice.actions.setAreaNormalization(value === 'true' ? true : false)) + break; + case 'areaUnit': + dispatchQueue.push(graphSlice.actions.updateSelectedAreaUnit(value as AreaUnitType)) + break; + case 'minMax': + dispatchQueue.push(graphSlice.actions.setShowMinMax(value === 'true' ? true : false)) + break; + case 'comparePeriod': + dispatchQueue.push(graphSlice.actions.updateComparePeriod({ comparePeriod: validateComparePeriod(value), currentTime: moment() })) + break; + case 'compareSortingOrder': + dispatchQueue.push(graphSlice.actions.changeCompareSortingOrder(validateSortingOrder(value))) + break; + case 'optionsVisibility': + dispatchQueue.push(graphSlice.actions.setOptionsVisibility(value === 'true' ? true : false)) + break; + case 'mapID': + // dispatchQueue.push(graphSlice.actions.map) + console.log('Todo, FIXME! Maplink not working') + break; + case 'serverRange': + dispatchQueue.push(graphSlice.actions.changeGraphZoom(TimeInterval.fromString(value))); + /** + * commented out since days from present feature is not currently used + */ + // const index = info.indexOf('dfp'); + // if (index === -1) { + // options.serverRange = TimeInterval.fromString(info); + // } else { + // const message = info.substring(0, index); + // const stringField = this.getNewIntervalFromMessage(message); + // options.serverRange = TimeInterval.fromString(stringField); + // } + break; + case 'sliderRange': + dispatchQueue.push(graphSlice.actions.changeSliderRange(TimeInterval.fromString(value))); + break; + case 'meterOrGroupID': + dispatchQueue.push(graphSlice.actions.updateThreeDMeterOrGroupID(parseInt(value))); + break; + case 'meterOrGroup': + dispatchQueue.push(graphSlice.actions.updateThreeDMeterOrGroup(value as MeterOrGroup)); + break; + case 'readingInterval': + dispatchQueue.push(graphSlice.actions.updateThreeDReadingInterval(parseInt(value))); + break; + default: + throw new Error('Unknown query parameter'); + } + + }) + + } catch (err) { + showErrorNotification(translate('failed.to.link.graph')); + } + // All appropriate state updates should've been executed + // redirect to clear the link + if (!initComplete) { + return + } else { + dispatchQueue.forEach(action => { + dispatch(action) + }) + return + } +} \ No newline at end of file diff --git a/src/client/app/reducers/graph.ts b/src/client/app/reducers/graph.ts index bae73d6cc..d84d042ec 100644 --- a/src/client/app/reducers/graph.ts +++ b/src/client/app/reducers/graph.ts @@ -94,6 +94,9 @@ export const graphSlice = createSlice({ toggleShowMinMax: state => { state.showMinMax = !state.showMinMax }, + setShowMinMax: (state, action: PayloadAction) => { + state.showMinMax = action.payload + }, changeBarStacking: state => { state.barStacking = !state.barStacking }, @@ -109,6 +112,9 @@ export const graphSlice = createSlice({ toggleOptionsVisibility: state => { state.optionsVisibility = !state.optionsVisibility }, + setOptionsVisibility: (state, action: PayloadAction) => { + state.optionsVisibility = action.payload + }, updateLineGraphRate: (state, action: PayloadAction) => { state.lineGraphRate = action.payload }, @@ -119,6 +125,12 @@ export const graphSlice = createSlice({ state.threeD.meterOrGroupID = action.payload.meterOrGroupID state.threeD.meterOrGroup = action.payload.meterOrGroup }, + updateThreeDMeterOrGroupID: (state, action: PayloadAction) => { + state.threeD.meterOrGroupID = action.payload + }, + updateThreeDMeterOrGroup: (state, action: PayloadAction) => { + state.threeD.meterOrGroup = action.payload + }, updateSelectedMetersOrGroups: (state, action: PayloadAction<{ newMetersOrGroups: number[], meta: ActionMeta }>) => { // Destructure payload const { newMetersOrGroups, meta } = action.payload; From e9cf6e3340ce2abd0f367d8365f06eaea2fd609d Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Mon, 2 Oct 2023 22:56:10 +0000 Subject: [PATCH 020/131] Ui Options Component Refactor --- .../app/components/BarControlsComponent.tsx | 97 +++++ .../components/ChartDataSelectComponent.tsx | 14 - .../ChartDataSelectComponentWIP.tsx | 23 + .../app/components/ChartLinkComponent.tsx | 10 +- .../components/CompareControlsComponent.tsx | 85 ++++ .../app/components/DateRangeComponent.tsx | 20 +- .../app/components/LineChartComponent.tsx | 395 +++++++++++++++++ .../app/components/MapControlsComponent.tsx | 63 +++ .../MeterAndGroupSelectComponent.tsx | 38 +- .../app/components/UIOptionsComponent.tsx | 396 ++++-------------- .../app/components/UnitSelectComponent.tsx | 48 +-- 11 files changed, 790 insertions(+), 399 deletions(-) create mode 100644 src/client/app/components/BarControlsComponent.tsx create mode 100644 src/client/app/components/ChartDataSelectComponentWIP.tsx create mode 100644 src/client/app/components/CompareControlsComponent.tsx create mode 100644 src/client/app/components/LineChartComponent.tsx create mode 100644 src/client/app/components/MapControlsComponent.tsx diff --git a/src/client/app/components/BarControlsComponent.tsx b/src/client/app/components/BarControlsComponent.tsx new file mode 100644 index 000000000..396bc8b18 --- /dev/null +++ b/src/client/app/components/BarControlsComponent.tsx @@ -0,0 +1,97 @@ +import * as moment from 'moment'; +import sliderWithoutTooltips, { createSliderWithTooltip } from 'rc-slider'; +import 'rc-slider/assets/index.css'; +import * as React from 'react'; +import { Button, ButtonGroup } from 'reactstrap'; +import { graphSlice } from '../reducers/graph'; +import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import translate from '../utils/translate'; +import TooltipMarkerComponent from './TooltipMarkerComponent'; + +/** + * @returns controls for the Options Ui page. + */ +export default function BarControlsComponent() { + const dispatch = useAppDispatch(); + const barDuration = useAppSelector(state => state.graph.barDuration); + const barStacking = useAppSelector(state => state.graph.barStacking); + const [showSlider, setShowSlider] = React.useState(false); + const [sliderVal, setSliderVal] = React.useState(barDuration.asDays()); + + const toggleSlider = () => { + setShowSlider(showSlider => !showSlider) + } + + const handleChangeBarStacking = () => { + dispatch(graphSlice.actions.changeBarStacking()) + } + + const handleSliderChange = (value: number) => { + setSliderVal(value) + } + const updateBarDurationChange = (value: number) => { + dispatch(graphSlice.actions.updateBarDuration(moment.duration(value, 'days'))) + } + const barDurationDays = barDuration.asDays(); + + return ( +
+
+ + + +
+
+

{translate('bar.interval')}:

+ + + + + + +
+
+ + +
+ {showSlider && +
+ +
+ } +
+ ) +} + +const Slider = createSliderWithTooltip(sliderWithoutTooltips); +const formatSliderTip = (value: number) => `${value} ${translate(value <= 1 ? 'day' : 'days')}` + + +const divTopPadding: React.CSSProperties = { + paddingTop: '15px' +}; + +const labelStyle: React.CSSProperties = { + fontWeight: 'bold', + margin: 0 +}; + +const zIndexFix: React.CSSProperties = { + zIndex: 0 +}; \ No newline at end of file diff --git a/src/client/app/components/ChartDataSelectComponent.tsx b/src/client/app/components/ChartDataSelectComponent.tsx index 3f8ff9515..498b852d4 100644 --- a/src/client/app/components/ChartDataSelectComponent.tsx +++ b/src/client/app/components/ChartDataSelectComponent.tsx @@ -25,10 +25,8 @@ import { } from '../utils/calibration'; import { metersInGroup, unitsCompatibleWithMeters } from '../utils/determineCompatibleUnits'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; -import MeterAndGroupSelectComponent from './MeterAndGroupSelectComponent'; import MultiSelectComponent from './MultiSelectComponent'; import TooltipMarkerComponent from './TooltipMarkerComponent'; -import UnitSelectComponent from './UnitSelectComponent'; /** * A component which allows the user to select which data should be displayed on the chart. @@ -275,7 +273,6 @@ export default function ChartDataSelectComponent() { return (

- Ref: :

@@ -304,11 +301,7 @@ export default function ChartDataSelectComponent() { }} />
-
- -

- Ref: :

@@ -346,11 +339,7 @@ export default function ChartDataSelectComponent() { }} />
-
- -

- Ref: :

@@ -380,9 +369,6 @@ export default function ChartDataSelectComponent() { }} />
-
- -
); } diff --git a/src/client/app/components/ChartDataSelectComponentWIP.tsx b/src/client/app/components/ChartDataSelectComponentWIP.tsx new file mode 100644 index 000000000..4ebcbd3a6 --- /dev/null +++ b/src/client/app/components/ChartDataSelectComponentWIP.tsx @@ -0,0 +1,23 @@ +/* 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 { MeterOrGroup } from '../types/redux/graph'; +import MeterAndGroupSelectComponent from './MeterAndGroupSelectComponent'; +import UnitSelectComponent from './UnitSelectComponent'; + +/** + * A component which allows the user to select which data should be displayed on the chart. + * @returns Chart data select element + */ +export default function ChartDataSelectComponentWIP() { + + return ( +
+ + + +
+ ); +} \ No newline at end of file diff --git a/src/client/app/components/ChartLinkComponent.tsx b/src/client/app/components/ChartLinkComponent.tsx index 6ce255621..0f90427e2 100644 --- a/src/client/app/components/ChartLinkComponent.tsx +++ b/src/client/app/components/ChartLinkComponent.tsx @@ -5,8 +5,8 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { Button } from 'reactstrap'; -import {ChartTypes} from '../types/redux/graph'; -import {getRangeSliderInterval} from './DashboardComponent'; +import { ChartTypes } from '../types/redux/graph'; +import { getRangeSliderInterval } from './LineChartComponent'; import TooltipMarkerComponent from './TooltipMarkerComponent'; interface ChartLinkProps { @@ -50,14 +50,14 @@ export default class ChartLinkComponent extends React.Component - - + {this.state.showLink && <>
-
diff --git a/src/client/app/components/CompareControlsComponent.tsx b/src/client/app/components/CompareControlsComponent.tsx new file mode 100644 index 000000000..ef18bfb8b --- /dev/null +++ b/src/client/app/components/CompareControlsComponent.tsx @@ -0,0 +1,85 @@ +import * as moment from 'moment'; +import * as React from 'react'; +import { Button, ButtonGroup, Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; +import { graphSlice } from '../reducers/graph'; +import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { ComparePeriod, SortingOrder } from '../utils/calculateCompare'; +import translate from '../utils/translate'; +import TooltipMarkerComponent from './TooltipMarkerComponent'; + +/** + * @returns controls for the compare page + */ +export default function CompareControlsComponent() { + const dispatch = useAppDispatch(); + const comparePeriod = useAppSelector(state => state.graph.comparePeriod); + const compareSortingOrder = useAppSelector(state => state.graph.compareSortingOrder); + const [compareSortingDropdownOpen, setCompareSortingDropdownOpen] = React.useState(false); + const handleCompareButton = (comparePeriod: ComparePeriod) => { + dispatch(graphSlice.actions.updateComparePeriod({ comparePeriod, currentTime: moment() })) + } + const handleSortingButton = (sortingOrder: SortingOrder) => { + dispatch(graphSlice.actions.changeCompareSortingOrder(sortingOrder)) + } + + return ( +
+ + + + + + + setCompareSortingDropdownOpen(current => !current)}> + + {translate('sort')} + + + + handleSortingButton(SortingOrder.Alphabetical)} + > + {translate('alphabetically')} + + handleSortingButton(SortingOrder.Ascending)} + > + {translate('ascending')} + + handleSortingButton(SortingOrder.Descending)} + > + {translate('descending')} + + + +
+ ) +} + +const zIndexFix: React.CSSProperties = { + zIndex: 0 +}; \ No newline at end of file diff --git a/src/client/app/components/DateRangeComponent.tsx b/src/client/app/components/DateRangeComponent.tsx index ae69b2eec..10735b58a 100644 --- a/src/client/app/components/DateRangeComponent.tsx +++ b/src/client/app/components/DateRangeComponent.tsx @@ -2,20 +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 * as React from 'react'; -import { useEffect, useState } from 'react'; -import { useSelector, useDispatch } from 'react-redux' import DateRangePicker from '@wojtekmaj/react-daterange-picker'; +import '@wojtekmaj/react-daterange-picker/dist/DateRangePicker.css'; import { CloseReason, Value } from '@wojtekmaj/react-daterange-picker/dist/cjs/shared/types'; +import * as React from 'react'; +import { useEffect, useState } from 'react'; import 'react-calendar/dist/Calendar.css'; -import '@wojtekmaj/react-daterange-picker/dist/DateRangePicker.css'; +import { useDispatch, useSelector } from 'react-redux'; +import { graphSlice } from '../reducers/graph'; +import { Dispatch } from '../types/redux/actions'; +import { ChartTypes } from '../types/redux/graph'; +import { State } from '../types/redux/state'; import { dateRangeToTimeInterval, timeIntervalToDateRange } from '../utils/dateRangeCompatibility'; -import TooltipMarkerComponent from './TooltipMarkerComponent'; import translate from '../utils/translate'; -import { State } from '../types/redux/state'; -import { ChartTypes } from '../types/redux/graph'; -import { Dispatch } from '../types/redux/actions'; -import { changeGraphZoomIfNeeded } from '../actions/graph'; +import TooltipMarkerComponent from './TooltipMarkerComponent'; /** * A component which allows users to select date ranges in lieu of a slider (line graphic) @@ -34,7 +34,7 @@ export default function DateRangeComponent() { // Don't Close Calendar when selecting dates. // This allows the value to update before calling the onCalClose() method to fetch data if needed. const shouldCloseCalendar = (props: { reason: CloseReason }) => { return props.reason === 'select' ? false : true; }; - const onCalClose = () => { dispatch(changeGraphZoomIfNeeded(dateRangeToTimeInterval(dateRange))) }; + const onCalClose = () => { dispatch(graphSlice.actions.changeGraphZoom(dateRangeToTimeInterval(dateRange))) }; // Only Render if a 3D Graphic Type Selected. if (chartToRender === ChartTypes.threeD) diff --git a/src/client/app/components/LineChartComponent.tsx b/src/client/app/components/LineChartComponent.tsx new file mode 100644 index 000000000..83bcd7384 --- /dev/null +++ b/src/client/app/components/LineChartComponent.tsx @@ -0,0 +1,395 @@ +/* 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 _ from 'lodash'; +import * as moment from 'moment'; +import * as React from 'react'; +import Plot from 'react-plotly.js'; +import { Button } from 'reactstrap'; +import { TimeInterval } from '../../../common/TimeInterval'; +import { graphSlice } from '../reducers/graph'; +import { readingsApi } from '../redux/api/readingsApi'; +import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { + ChartQueryProps, LineReadingApiArgs, + selectGroupDataByID, selectMeterDataByID, + selectMeterState, selectUnitDataById +} from '../redux/selectors/dataSelectors'; +import { selectSelectedGroups, selectSelectedMeters } from '../redux/selectors/uiSelectors'; +import { DataType } from '../types/Datasources'; +import Locales from '../types/locales'; +import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; +import getGraphColor from '../utils/getGraphColor'; +import { lineUnitLabel } from '../utils/graphics'; +import translate from '../utils/translate'; +import SpinnerComponent from './SpinnerComponent'; +import TooltipMarkerComponent from './TooltipMarkerComponent'; + + +/** + * @param props qpi query + * @returns plotlyLine graphic + */ +export default function LineChartComponent(props: ChartQueryProps) { + const { meterArgs, groupsArgs } = props.queryProps; + const { + data: meterReadings, + isFetching: meterIsFetching + } = readingsApi.useLineQuery(meterArgs, { skip: !meterArgs.selectedMeters.length }); + + const { + data: groupData, + isFetching: groupIsFetching + } = readingsApi.useLineQuery(groupsArgs, { skip: !groupsArgs.selectedGroups.length }); + + const selectedUnit = useAppSelector(state => state.graph.selectedUnit); + const datasets: any[] = []; + // The unit label depends on the unit which is in selectUnit state. + const graphingUnit = useAppSelector(state => state.graph.selectedUnit); + // The current selected rate + const currentSelectedRate = useAppSelector(state => state.graph.lineGraphRate); + const unitDataByID = useAppSelector(state => selectUnitDataById(state)); + const selectedAreaNormalization = useAppSelector(state => state.graph.areaNormalization); + const selectedAreaUnit = useAppSelector(state => state.graph.selectedAreaUnit); + const selectedMeters = useAppSelector(state => selectSelectedMeters(state)); + const selectedGroups = useAppSelector(state => selectSelectedGroups(state)); + const metersState = useAppSelector(state => selectMeterState(state)); + const meterDataByID = useAppSelector(state => selectMeterDataByID(state)); + const groupDataByID = useAppSelector(state => selectGroupDataByID(state)); + + if (meterIsFetching || groupIsFetching) { + return + } + // The unit label depends on the unit which is in selectUnit state. + // The current selected rate + let unitLabel = ''; + let needsRateScaling = false; + // variables to determine the slider min and max + let minTimestamp: number | undefined; + let maxTimestamp: number | undefined; + // 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 = unitDataByID[selectedUnit]; + if (selectUnitState !== undefined) { + // Determine the y-axis label and if the rate needs to be scaled. + const returned = lineUnitLabel(selectUnitState, currentSelectedRate, selectedAreaNormalization, selectedAreaUnit); + unitLabel = returned.unitLabel + needsRateScaling = returned.needsRateScaling; + } + } + // The rate will be 1 if it is per hour (since state readings are per hour) or no rate scaling so no change. + const rateScaling = needsRateScaling ? currentSelectedRate.rate : 1; + // Add all valid data from existing meters to the line plot + for (const meterID of selectedMeters) { + const byMeterID = meterReadings + // Make sure have the meter data. If you already have the meter, unselect, change + // the timeInterval via another meter and then reselect then this new timeInterval + // may not yet be in state so verify with the second condition on the if. + // Note the second part may not be used based on next checks but do here since simple. + if (byMeterID) { + const meterArea = metersState.byMeterID[meterID].area; + // We either don't care about area, or we do in which case there needs to be a nonzero area. + if (!selectedAreaNormalization || (meterArea > 0 && meterDataByID[meterID].areaUnit != AreaUnitType.none)) { + // Convert the meter area into the proper unit if normalizing by area or use 1 if not so won't change reading values. + const areaScaling = selectedAreaNormalization ? meterArea * getAreaUnitConversion(meterDataByID[meterID].areaUnit, selectedAreaUnit) : 1; + // Divide areaScaling into the rate so have complete scaling factor for readings. + const scaling = rateScaling / areaScaling; + const readingsData = meterReadings[meterID] + if (readingsData !== undefined && !meterIsFetching) { + const label = meterDataByID[meterID].identifier; + const colorID = meterID; + if (readingsData === undefined) { + throw new Error('Unacceptable condition: readingsData.readings is undefined.'); + } + + // Create two arrays for the x and y values. Fill the array with the data from the line readings + const xData: string[] = []; + const yData: number[] = []; + const hoverText: string[] = []; + const readings = _.values(readingsData); + readings.forEach(reading => { + // As usual, we want to interpret the readings in UTC. We lose the timezone as this as the start/endTimestamp + // are equivalent to Unix timestamp in milliseconds. + const st = moment.utc(reading.startTimestamp); + // Time reading is in the middle of the start and end timestamp + const timeReading = st.add(moment.utc(reading.endTimestamp).diff(st) / 2); + xData.push(timeReading.format('YYYY-MM-DD HH:mm:ss')); + const readingValue = reading.reading * scaling; + yData.push(readingValue); + hoverText.push(` ${timeReading.format('ddd, ll LTS')}
${label}: ${readingValue.toPrecision(6)} ${unitLabel}`); + }); + + /* + get the min and max timestamp of the meter, and compare it to the global values + TODO: If we know the interval and frequency of meter data, these calculations should be able to be simplified + */ + if (readings.length > 0) { + if (minTimestamp == undefined || readings[0]['startTimestamp'] < minTimestamp) { + minTimestamp = readings[0]['startTimestamp']; + } + if (maxTimestamp == undefined || readings[readings.length - 1]['endTimestamp'] >= maxTimestamp) { + // Need to add one extra reading interval to avoid range truncation. The max bound seems to be treated as non-inclusive + maxTimestamp = readings[readings.length - 1]['endTimestamp'] + (readings[0]['endTimestamp'] - readings[0]['startTimestamp']); + } + } + + // This variable contains all the elements (x and y values, line type, etc.) assigned to the data parameter of the Plotly object + datasets.push({ + name: label, + x: xData, + y: yData, + text: hoverText, + hoverinfo: 'text', + type: 'scatter', + mode: 'lines', + line: { + shape: 'spline', + width: 2, + color: getGraphColor(colorID, DataType.Meter) + } + }); + } + } + } + } + + // TODO The meters and groups code is very similar and maybe it should be refactored out to create a function to do + // both. This would mean future changes would automatically happen to both. + // Add all valid data from existing groups to the line plot + for (const groupID of selectedGroups) { + const byGroupID = groupData + // Make sure have the group data. If you already have the group, unselect, change + // the timeInterval via another meter and then reselect then this new timeInterval + // may not yet be in state so verify with the second condition on the if. + // Note the second part may not be used based on next checks but do here since simple. + if (byGroupID) { + const groupArea = groupDataByID[groupID].area; + // We either don't care about area, or we do in which case there needs to be a nonzero area. + if (!selectedAreaNormalization || (groupArea > 0 && groupDataByID[groupID].areaUnit != AreaUnitType.none)) { + // Convert the group area into the proper unit if normalizing by area or use 1 if not so won't change reading values. + const areaScaling = selectedAreaNormalization ? + groupArea * getAreaUnitConversion(groupDataByID[groupID].areaUnit, selectedAreaUnit) : 1; + // Divide areaScaling into the rate so have complete scaling factor for readings. + const scaling = rateScaling / areaScaling; + const readingsData = byGroupID[groupID]; + if (readingsData !== undefined && !groupIsFetching) { + const label = groupDataByID[groupID].name; + const colorID = groupID; + if (readingsData === undefined) { + throw new Error('Unacceptable condition: readingsData.readings is undefined.'); + } + + // Create two arrays for the x and y values. Fill the array with the data from the line readings + const xData: string[] = []; + const yData: number[] = []; + const hoverText: string[] = []; + const readings = _.values(readingsData); + readings.forEach(reading => { + // As usual, we want to interpret the readings in UTC. We lose the timezone as this as the start/endTimestamp + // are equivalent to Unix timestamp in milliseconds. + const st = moment.utc(reading.startTimestamp); + // Time reading is in the middle of the start and end timestamp + const timeReading = st.add(moment.utc(reading.endTimestamp).diff(st) / 2); + xData.push(timeReading.utc().format('YYYY-MM-DD HH:mm:ss')); + const readingValue = reading.reading * scaling; + yData.push(readingValue); + hoverText.push(` ${timeReading.format('ddd, ll LTS')}
${label}: ${readingValue.toPrecision(6)} ${unitLabel}`); + }); + + // get the min and max timestamp of the group, and compare it to the global values + if (readings.length > 0) { + if (minTimestamp == undefined || readings[0]['startTimestamp'] < minTimestamp) { + minTimestamp = readings[0]['startTimestamp']; + } + if (maxTimestamp == undefined || readings[readings.length - 1]['endTimestamp'] >= maxTimestamp) { + // Need to add one extra reading interval to avoid range truncation. The max bound seems to be treated as non-inclusive + maxTimestamp = readings[readings.length - 1]['endTimestamp'] + (readings[0]['endTimestamp'] - readings[0]['startTimestamp']); + } + } + + // This variable contains all the elements (x and y values, line type, etc.) assigned to the data parameter of the Plotly object + datasets.push({ + name: label, + x: xData, + y: yData, + text: hoverText, + hoverinfo: 'text', + type: 'scatter', + mode: 'lines', + line: { + shape: 'spline', + width: 2, + color: getGraphColor(colorID, DataType.Group) + } + }); + } + } + } + } + + // set the bounds for the slider + if (minTimestamp == undefined) { + minTimestamp = 0; + maxTimestamp = 0; + } + const root: any = document.getElementById('root'); + root.setAttribute('min-timestamp', minTimestamp); + root.setAttribute('max-timestamp', maxTimestamp); + + // Use the min/max time found for the readings (and shifted as desired) as the + // x-axis range for the graph. + // Avoid pesky shifting timezones with utc. + const start = moment.utc(minTimestamp).toISOString(); + const end = moment.utc(maxTimestamp).toISOString(); + + let layout: any; + // Customize the layout of the plot + // See https://community.plotly.com/t/replacing-an-empty-graph-with-a-message/31497 for showing text not plot. + if (datasets.length === 0) { + // There is not data so tell user. + layout = { + 'xaxis': { + 'visible': false + }, + 'yaxis': { + 'visible': false + }, + 'annotations': [ + { + 'text': `${translate('select.meter.group')}`, + 'xref': 'paper', + 'yref': 'paper', + 'showarrow': false, + 'font': { + 'size': 28 + } + } + ] + } + + } else { + // This normal so plot. + layout = { + autosize: true, + showlegend: true, + height: 700, + legend: { + x: 0, + y: 1.1, + orientation: 'h' + }, + yaxis: { + title: unitLabel, + gridcolor: '#ddd' + }, + + xaxis: { + range: [start, end], // Specifies the start and end points of visible part of graph + rangeslider: { + thickness: 0.1 + }, + showgrid: true, + gridcolor: '#ddd' + }, + margin: { + t: 10, + b: 10 + } + }; + } + const config = { + displayModeBar: true, + responsive: true, + locales: Locales // makes locales available for use + } + return ( + <> + + {/* Only Show if there's data */ + (datasets.length !== 0) && } + + ) +} + +const SliderControls = () => { + const dispatch = useAppDispatch(); + const timeInterval = useAppSelector(state => state.graph.timeInterval); + return ( + <> + + + + + ) +} + +/** + * Determines the line graph's slider interval based after the slider is moved + * @returns The slider interval, either 'all' or a TimeInterval + */ +export function getRangeSliderInterval(): string { + const sliderContainer: any = document.querySelector('.rangeslider-bg'); + const sliderBox: any = document.querySelector('.rangeslider-slidebox'); + const root: any = document.getElementById('root'); + + if (sliderContainer && sliderBox && root) { + // Attributes of the slider: full width and the min & max values of the box + const fullWidth: number = parseInt(sliderContainer.getAttribute('width')); + const sliderMinX: number = parseInt(sliderBox.getAttribute('x')); + const sliderMaxX: number = sliderMinX + parseInt(sliderBox.getAttribute('width')); + if (sliderMaxX - sliderMinX === fullWidth) { + return 'all'; + } + + // From the Plotly line graph, get current min and max times in seconds + const minTimeStamp: number = parseInt(root.getAttribute('min-timestamp')); + const maxTimeStamp: number = parseInt(root.getAttribute('max-timestamp')); + + // Seconds displayed on graph + const deltaSeconds: number = maxTimeStamp - minTimeStamp; + const secondsPerPixel: number = deltaSeconds / fullWidth; + + // Get the new min and max times, in seconds, from the slider box + const newMinXTimestamp = Math.floor(minTimeStamp + (secondsPerPixel * sliderMinX)); + const newMaxXTimestamp = Math.floor(minTimeStamp + (secondsPerPixel * sliderMaxX)); + // The newMin/MaxTimestamp is equivalent to a Unix time in milliseconds. Thus, it will + // shift with timezone. It isn't clear if we want it in local or UTC. It depends on what + // plotly does. Here it is assumed that local is what is desired. This seems to work + // and not shift the graphs x-axis so using. + return new TimeInterval(moment(newMinXTimestamp), moment(newMaxXTimestamp)).toString(); + } else { + throw new Error('unable to get range slider params'); + } +} + + +const buttonMargin: React.CSSProperties = { + marginRight: '10px' +}; \ No newline at end of file diff --git a/src/client/app/components/MapControlsComponent.tsx b/src/client/app/components/MapControlsComponent.tsx new file mode 100644 index 000000000..9de41f393 --- /dev/null +++ b/src/client/app/components/MapControlsComponent.tsx @@ -0,0 +1,63 @@ +import * as React from 'react'; +import translate from '../utils/translate'; +import { Button, ButtonGroup } from 'reactstrap'; +import TooltipMarkerComponent from './TooltipMarkerComponent'; +import MapChartSelectComponent from './MapChartSelectComponent'; +import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { graphSlice } from '../reducers/graph'; +import * as moment from 'moment'; +/** + * @returns Map page controls + */ +export default function MapControlsComponent() { + const dispatch = useAppDispatch(); + const barDuration = useAppSelector(state => state.graph.barDuration); + + const handleDurationChange = (value: number) => { + dispatch(graphSlice.actions.updateBarDuration(moment.duration(value, 'days'))) + } + + const barDurationDays = barDuration.asDays(); + + return ( +
+
+

+ {translate('map.interval')}: +

+ + + + + + +
+ +
+ ) +} + + +const labelStyle: React.CSSProperties = { + fontWeight: 'bold', + margin: 0 +}; + +const zIndexFix: React.CSSProperties = { + zIndex: 0 +}; \ No newline at end of file diff --git a/src/client/app/components/MeterAndGroupSelectComponent.tsx b/src/client/app/components/MeterAndGroupSelectComponent.tsx index 9787bdc1b..a58a13b22 100644 --- a/src/client/app/components/MeterAndGroupSelectComponent.tsx +++ b/src/client/app/components/MeterAndGroupSelectComponent.tsx @@ -12,6 +12,7 @@ import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { selectMeterGroupSelectData } from '../redux/selectors/uiSelectors'; import { MeterOrGroup } from '../types/redux/graph'; import translate from '../utils/translate'; +import TooltipMarkerComponent from './TooltipMarkerComponent'; const animatedComponents = makeAnimated(); @@ -44,18 +45,24 @@ export default function MeterAndGroupSelectComponent(props: MeterAndGroupSelectP } return ( - - isMulti - placeholder={meterOrGroup === MeterOrGroup.meters ? translate('select.meters') : translate('select.groups')} - options={options} - value={value} - onChange={onChange} - closeMenuOnSelect={false} - // Customize Labeling for Grouped Labels - formatGroupLabel={formatGroupLabel} - // Included React-Select Animations - components={animatedComponents} - /> +
+

+ {translate(`${meterOrGroup}`)}: + +

+ + isMulti + placeholder={translate(`select.${meterOrGroup}`)} + options={options} + value={value} + onChange={onChange} + closeMenuOnSelect={false} + // Customize Labeling for Grouped Labels + formatGroupLabel={formatGroupLabel} + // Included React-Select Animations + components={animatedComponents} + /> +
) } @@ -78,3 +85,10 @@ const formatGroupLabel = (data: GroupedOption) => { interface MeterAndGroupSelectProps { meterOrGroup: MeterOrGroup; } +const divBottomPadding: React.CSSProperties = { + paddingBottom: '15px' +}; +const labelStyle: React.CSSProperties = { + fontWeight: 'bold', + margin: 0 +}; \ No newline at end of file diff --git a/src/client/app/components/UIOptionsComponent.tsx b/src/client/app/components/UIOptionsComponent.tsx index 0a1e7dd8c..8bac379fe 100644 --- a/src/client/app/components/UIOptionsComponent.tsx +++ b/src/client/app/components/UIOptionsComponent.tsx @@ -3,337 +3,83 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { FormattedMessage, defineMessages, injectIntl, WrappedComponentProps } from 'react-intl'; -import sliderWithoutTooltips, { createSliderWithTooltip } from 'rc-slider'; -import * as moment from 'moment'; -import { Button, ButtonGroup, Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap'; +import { FormattedMessage } from 'react-intl'; +import { Button } from 'reactstrap'; import ExportComponent from '../components/ExportComponent'; -import ChartSelectComponent from './ChartSelectComponent'; -import ChartDataSelectComponent from './ChartDataSelectComponent'; import ChartLinkContainer from '../containers/ChartLinkContainer'; +import { graphSlice } from '../reducers/graph'; +import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { selectChartToRender } from '../redux/selectors/uiSelectors'; import { ChartTypes } from '../types/redux/graph'; -import { ComparePeriod, SortingOrder } from '../utils/calculateCompare'; -import TooltipMarkerComponent from './TooltipMarkerComponent'; -import 'rc-slider/assets/index.css'; -import MapChartSelectComponent from './MapChartSelectComponent'; -import ReactTooltip from 'react-tooltip'; -import GraphicRateMenuComponent from './GraphicRateMenuComponent'; import AreaUnitSelectComponent from './AreaUnitSelectComponent'; -import ErrorBarComponent from './ErrorBarComponent'; +import BarControlsComponent from './BarControlsComponent'; +import ChartDataSelectComponentWIP from './ChartDataSelectComponentWIP'; +import ChartSelectComponent from './ChartSelectComponent'; +import CompareControlsComponent from './CompareControlsComponent'; import DateRangeComponent from './DateRangeComponent'; +import ErrorBarComponent from './ErrorBarComponent'; +import GraphicRateMenuComponent from './GraphicRateMenuComponent'; +import MapControlsComponent from './MapControlsComponent'; import ThreeDSelectComponent from './ReadingsPerDaySelectComponent'; -import { graphSlice } from '../reducers/graph'; - -const Slider = createSliderWithTooltip(sliderWithoutTooltips); - -export interface UIOptionsProps { - chartToRender: ChartTypes; - barStacking: boolean; - barDuration: moment.Duration; - comparePeriod: ComparePeriod; - compareSortingOrder: SortingOrder; - optionsVisibility: boolean; - changeDuration(duration: moment.Duration): Promise; - changeBarStacking(): ReturnType; - toggleOptionsVisibility(): ReturnType; - changeCompareGraph(comparePeriod: ComparePeriod): Promise; - changeCompareSortingOrder(compareSortingOrder: SortingOrder): ReturnType; -} - -type UIOptionsPropsWithIntl = UIOptionsProps & WrappedComponentProps; - -interface UIOptionsState { - barDurationDays: number; - showSlider: boolean; - compareSortingDropdownOpen: boolean; -} - -class UIOptionsComponent extends React.Component { - constructor(props: UIOptionsPropsWithIntl) { - super(props); - this.handleBarDurationChange = this.handleBarDurationChange.bind(this); - this.handleBarDurationChangeComplete = this.handleBarDurationChangeComplete.bind(this); - this.handleChangeBarStacking = this.handleChangeBarStacking.bind(this); - this.formatSliderTip = this.formatSliderTip.bind(this); - this.handleBarButton = this.handleBarButton.bind(this); - this.handleCompareButton = this.handleCompareButton.bind(this); - this.handleSortingButton = this.handleSortingButton.bind(this); - this.handleToggleOptionsVisibility = this.handleToggleOptionsVisibility.bind(this); - this.toggleSlider = this.toggleSlider.bind(this); - this.toggleDropdown = this.toggleDropdown.bind(this); - this.state = { - barDurationDays: this.props.barDuration.asDays(), - showSlider: false, - compareSortingDropdownOpen: false - }; - } - - public componentDidUpdate(prev: UIOptionsProps) { - if (prev.chartToRender !== this.props.chartToRender) { - ReactTooltip.rebuild(); // This rebuilds the tooltip so that it detects the marker that disappear because the chart type changes. - } - } - - public render() { - const labelStyle: React.CSSProperties = { - fontWeight: 'bold', - margin: 0 - }; - const divTopPadding: React.CSSProperties = { - paddingTop: '15px' - }; - const zIndexFix: React.CSSProperties = { - zIndex: 0 - }; - - return ( -
- - - - - - - - {/* Controls error bar, specifically for the line chart. */} - {this.props.chartToRender === ChartTypes.line && - - } - {/* Controls specific to the bar chart. */} - {this.props.chartToRender === ChartTypes.bar && -
-
- - - -
-

- : -

- - - - - - - - - {this.state.showSlider && -
- -
- } -
- - } - {/* Controls specific to the compare chart */} - {this.props.chartToRender === ChartTypes.compare && -
- - - - - - - - - - - - - this.handleSortingButton(SortingOrder.Alphabetical)} - > - - - this.handleSortingButton(SortingOrder.Ascending)} - > - - - this.handleSortingButton(SortingOrder.Descending)} - > - - - - -
- } - - {this.props.chartToRender === ChartTypes.map && -
-
-

- : -

- - - - - - -
- -
- } - - {/* We can't export compare data or map data */} - {this.props.chartToRender !== ChartTypes.compare && this.props.chartToRender !== ChartTypes.map && this.props.chartToRender !== ChartTypes.threeD && - < div style={divTopPadding}> - -
- } -
- -
+import TooltipMarkerComponent from './TooltipMarkerComponent'; +import ReactTooltip from 'react-tooltip'; -
- - +/** + * @returns the Ui Control panel + */ +export default function UIOptionsComponent() { + const dispatch = useAppDispatch() + const chartToRender = useAppSelector(state => selectChartToRender(state)); + const optionsVisibility = useAppSelector(state => state.graph.optionsVisibility); + ReactTooltip.rebuild(); + return ( +
+ + + + + + + + { /* Controls error bar, specifically for the line chart. */ + chartToRender === ChartTypes.line && } + + { /* Controls specific to the bar chart. */ + chartToRender === ChartTypes.bar && } + + { /* Controls specific to the compare chart */ + chartToRender === ChartTypes.compare && } + + { /* Controls specific to the compare chart */ + chartToRender === ChartTypes.map && } + + + {/* We can't export compare data or map data */ + chartToRender !== ChartTypes.compare && chartToRender !== ChartTypes.map && chartToRender !== ChartTypes.threeD && + < div style={divTopPadding}> +
-
- ); - } - - private handleBarDurationChangeComplete() { - this.props.changeDuration(moment.duration(this.state.barDurationDays, 'days')); - } - - private handleBarButton(value: number) { - this.props.changeDuration(moment.duration(value, 'days')); - this.handleBarDurationChange(value); - } - - /** - * Stores temporary barDuration until slider is released, used to update the UI of the slider - * @param value Bar duration to be stored - */ - private handleBarDurationChange(value: number) { - this.setState({ barDurationDays: value }); - } - - private handleChangeBarStacking() { - this.props.changeBarStacking(); - } - - private handleCompareButton(comparePeriod: ComparePeriod) { - this.props.changeCompareGraph(comparePeriod); - } - - private handleSortingButton(sortingOrder: SortingOrder) { - this.props.changeCompareSortingOrder(sortingOrder); - } - - private handleToggleOptionsVisibility() { - this.props.toggleOptionsVisibility(); - } - - private toggleSlider() { - this.setState({ showSlider: !this.state.showSlider }); - } - - private formatSliderTip(value: number) { - const messages = defineMessages({ - day: { id: 'day' }, - days: { id: 'days' } - }); - if (value <= 1) { - return `${value} ${this.props.intl.formatMessage(messages.day)}`; - } - return `${value} ${this.props.intl.formatMessage(messages.days)}`; - } - - private toggleDropdown() { - this.setState({ compareSortingDropdownOpen: !this.state.compareSortingDropdownOpen }); - } + } +
+ +
+ +
+ + +
+ + ); } - -export default injectIntl(UIOptionsComponent); +const divTopPadding: React.CSSProperties = { + paddingTop: '15px' +}; diff --git a/src/client/app/components/UnitSelectComponent.tsx b/src/client/app/components/UnitSelectComponent.tsx index 223780a12..2eb1ec808 100644 --- a/src/client/app/components/UnitSelectComponent.tsx +++ b/src/client/app/components/UnitSelectComponent.tsx @@ -11,6 +11,8 @@ import { GroupedOption, SelectOption } from '../types/items'; // import { FormattedMessage } from 'react-intl'; import { Badge } from 'reactstrap'; import { graphSlice } from '../reducers/graph'; +import translate from '../utils/translate'; +import TooltipMarkerComponent from './TooltipMarkerComponent'; /** * @returns A React-Select component for UI Options Panel @@ -34,19 +36,20 @@ export default function UnitSelectComponent() { const onChange = (newValue: SelectOption) => dispatch(graphSlice.actions.updateSelectedUnit(newValue?.value)) return ( - <> - {/*

- : +

+

+ {translate('units')} -

*/} +

value={selectedUnitOption} options={unitSelectOptions} + placeholder={translate('select.unit')} onChange={onChange} formatGroupLabel={formatGroupLabel} isClearable /> - +
) } const groupStyles: React.CSSProperties = { @@ -65,31 +68,10 @@ const formatGroupLabel = (data: GroupedOption) => { ) } -{/* { - // TODO I don't quite understand why the component results in an array of size 2 when updating state - // For now I have hardcoded a fix that allows units to be selected over other units without clicking the x button - if (newSelectedUnitOptions.length === 0) { - // Update the selected meters and groups to empty to avoid graphing errors - // The update selected meters/groups functions are essentially the same as the change functions - // However, they do not attempt to graph. - dispatch(graphSlice.actions.updateSelectedGroups([])); - dispatch(graphSlice.actions.updateSelectedMeters([])); - dispatch(graphSlice.actions.updateSelectedUnit(-99)); - // Sync threeD state. - dispatch(changeMeterOrGroupInfo(null)); - } - else if (newSelectedUnitOptions.length === 1) { dispatch(changeSelectedUnit(newSelectedUnitOptions[0].value)); } - else if (newSelectedUnitOptions.length > 1) { dispatch(changeSelectedUnit(newSelectedUnitOptions[1].value)); } - // This should not happen - else { dispatch(changeSelectedUnit(-99)); } -}} -/> */} - -// const labelStyle: React.CSSProperties = { -// fontWeight: 'bold', -// margin: 0 -// }; \ No newline at end of file +const divBottomPadding: React.CSSProperties = { + paddingBottom: '15px' +}; +const labelStyle: React.CSSProperties = { + fontWeight: 'bold', + margin: 0 +}; \ No newline at end of file From 2357e7dfd45a904a79309a7af4b920bc1b806e11 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Mon, 2 Oct 2023 22:59:02 +0000 Subject: [PATCH 021/131] Line & Bar Component refactor - Containers converted to components and utilize RTK Query --- .../app/components/BarChartComponent.tsx | 292 ++++++++++++++++++ .../app/components/DashboardComponent.tsx | 102 +----- src/client/app/components/ThreeDComponent.tsx | 41 +-- .../app/containers/LineChartContainer.ts | 84 ++--- src/client/app/index.tsx | 2 +- src/client/app/reducers/graph.ts | 51 ++- src/client/app/redux/api/baseApi.ts | 6 +- src/client/app/redux/api/readingsApi.ts | 25 +- .../app/redux/selectors/dataSelectors.ts | 68 ++++ src/client/app/store.ts | 2 +- 10 files changed, 480 insertions(+), 193 deletions(-) create mode 100644 src/client/app/components/BarChartComponent.tsx diff --git a/src/client/app/components/BarChartComponent.tsx b/src/client/app/components/BarChartComponent.tsx new file mode 100644 index 000000000..f88d2fd0f --- /dev/null +++ b/src/client/app/components/BarChartComponent.tsx @@ -0,0 +1,292 @@ +/* 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 _ from 'lodash'; +import * as moment from 'moment'; +import * as React from 'react'; +import Plot from 'react-plotly.js'; +import { readingsApi } from '../redux/api/readingsApi'; +import { useAppSelector } from '../redux/hooks'; +import { + BarReadingApiArgs, ChartQueryProps, + selectGroupDataByID, + selectMeterDataByID, selectUnitDataById +} from '../redux/selectors/dataSelectors'; +import { selectSelectedGroups, selectSelectedMeters } from '../redux/selectors/uiSelectors'; +import { DataType } from '../types/Datasources'; +import Locales from '../types/locales'; +import { UnitRepresentType } from '../types/redux/units'; +import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; +import getGraphColor from '../utils/getGraphColor'; +import { barUnitLabel } from '../utils/graphics'; +import translate from '../utils/translate'; +import SpinnerComponent from './SpinnerComponent'; + +/** + * Passes the current redux state of the barchart, and turns it into props for the React + * component, which is what will be visible on the page. Makes it possible to access + * your reducer state objects from within your React components. + * @param props query arguments to be used in the dataFetching Hooks. + * @returns Plotly BarChart + */ +export default function BarChartComponent(props: ChartQueryProps) { + const { meterArgs, groupsArgs } = props.queryProps; + const { + data: meterReadings, + isFetching: meterIsFetching + } = readingsApi.useBarQuery(meterArgs, { skip: !meterArgs.selectedMeters.length }); + + const { + data: groupData, + isFetching: groupIsFetching + } = readingsApi.useBarQuery(groupsArgs, { skip: !groupsArgs.selectedGroups.length }); + + const barDuration = useAppSelector(state => state.graph.barDuration); + const barStacking = useAppSelector(state => state.graph.barStacking); + const unitID = useAppSelector(state => state.graph.selectedUnit); + const datasets: any[] = []; + // The unit label depends on the unit which is in selectUnit state. + const graphingUnit = useAppSelector(state => state.graph.selectedUnit); + const unitDataByID = useAppSelector(state => selectUnitDataById(state)); + const selectedAreaNormalization = useAppSelector(state => state.graph.areaNormalization); + const selectedAreaUnit = useAppSelector(state => state.graph.selectedAreaUnit); + const selectedMeters = useAppSelector(state => selectSelectedMeters(state)) + const selectedGroups = useAppSelector(state => selectSelectedGroups(state)) + const meterDataByID = useAppSelector(state => selectMeterDataByID(state)) + const groupDataByID = useAppSelector(state => selectGroupDataByID(state)) + + if (meterIsFetching || groupIsFetching) { + return + } + let unitLabel: string = ''; + let raw = false; + // 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 = unitDataByID[unitID]; + if (selectUnitState !== undefined) { + // Determine the y-axis label. + unitLabel = barUnitLabel(selectUnitState, selectedAreaNormalization, selectedAreaUnit); + if (selectUnitState.unitRepresent === UnitRepresentType.raw) { + // Cannot graph raw units as bar so put title to indicate that and empty otherwise. + raw = true; + } + } + } + + // Add all valid data from existing meters to the bar chart + for (const meterID of selectedMeters) { + if (meterReadings) { + 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 (!selectedAreaNormalization || (meterArea > 0 && meterDataByID[meterID].areaUnit != AreaUnitType.none)) { + if (selectedAreaNormalization) { + // convert the meter area into the proper unit, if needed + meterArea *= getAreaUnitConversion(meterDataByID[meterID].areaUnit, selectedAreaUnit); + } + const readingsData = meterReadings[meterID]; + if (readingsData && !meterIsFetching) { + const label = meterDataByID[meterID].identifier; + const colorID = meterID; + if (!readingsData) { + throw new Error('Unacceptable condition: readingsData.readings is undefined.'); + } + + // Create two arrays for the x and y values. Fill the array with the data. + const xData: string[] = []; + const yData: number[] = []; + const hoverText: string[] = []; + const readings = _.values(readingsData); + readings.forEach(barReading => { + const st = moment.utc(barReading.startTimestamp); + // Time reading is in the middle of the start and end timestamp (may change this depending on how it looks on the bar graph)\ + const timeReading = st.add(moment.utc(barReading.endTimestamp).diff(st) / 2); + xData.push(timeReading.utc().format('YYYY-MM-DD HH:mm:ss')); + let readingValue = barReading.reading; + if (selectedAreaNormalization) { + readingValue /= meterArea; + } + yData.push(readingValue); + // only display a range of dates for the hover text if there is more than one day in the range + let timeRange: string = `${moment.utc(barReading.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. + timeRange += ` - ${moment.utc(barReading.endTimestamp).subtract(1, 'days').format('ll')}`; + } + hoverText.push(` ${timeRange}
${label}: ${readingValue.toPrecision(6)} ${unitLabel}`); + }); + // This variable contains all the elements (x and y values, bar type, etc.) assigned to the data parameter of the Plotly object + datasets.push({ + name: label, + x: xData, + y: yData, + text: hoverText, + hoverinfo: 'text', + type: 'bar', + marker: { color: getGraphColor(colorID, DataType.Meter) } + }); + } + } + } + } + + for (const groupID of selectedGroups) { + if (groupData) { + let groupArea = groupDataByID[groupID].area; + if (!selectedAreaNormalization || (groupArea > 0 && groupDataByID[groupID].areaUnit != AreaUnitType.none)) { + if (selectedAreaNormalization) { + // convert the meter area into the proper unit, if needed + groupArea *= getAreaUnitConversion(groupDataByID[groupID].areaUnit, selectedAreaUnit); + } + const readingsData = groupData[groupID]; + if (readingsData && !groupIsFetching) { + const label = groupDataByID[groupID].name; + const colorID = groupID; + if (!readingsData) { + throw new Error('Unacceptable condition: readingsData.readings is undefined.'); + } + + // Create two arrays for the x and y values. Fill the array with the data. + const xData: string[] = []; + const yData: number[] = []; + const hoverText: string[] = []; + const readings = _.values(readingsData); + readings.forEach(barReading => { + const st = moment.utc(barReading.startTimestamp); + // Time reading is in the middle of the start and end timestamp (may change this depending on how it looks on the bar graph)\ + const timeReading = st.add(moment.utc(barReading.endTimestamp).diff(st) / 2); + xData.push(timeReading.utc().format('YYYY-MM-DD HH:mm:ss')); + let readingValue = barReading.reading; + if (selectedAreaNormalization) { + readingValue /= groupArea; + } + yData.push(readingValue); + // only display a range of dates for the hover text if there is more than one day in the range + let timeRange: string = `${moment.utc(barReading.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. + timeRange += ` - ${moment.utc(barReading.endTimestamp).subtract(1, 'days').format('ll')}`; + } + hoverText.push(` ${timeRange}
${label}: ${readingValue.toPrecision(6)} ${unitLabel}`); + }); + + // This variable contains all the elements (x and y values, bar chart, etc.) assigned to the data parameter of the Plotly object + datasets.push({ + name: label, + x: xData, + y: yData, + text: hoverText, + hoverinfo: 'text', + type: 'bar', + marker: { color: getGraphColor(colorID, DataType.Group) } + }); + } + } + } + } + + let layout: any; + // Customize the layout of the plot + // See https://community.plotly.com/t/replacing-an-empty-graph-with-a-message/31497 for showing text not plot. + if (raw) { + // This is a raw type graphing unit so cannot plot + layout = { + 'xaxis': { + 'visible': false + }, + 'yaxis': { + 'visible': false + }, + 'annotations': [ + { + 'text': `${translate('bar.raw')}`, + 'xref': 'paper', + 'yref': 'paper', + 'showarrow': false, + 'font': { + 'size': 28 + } + } + ] + } + } else if (datasets.length === 0) { + // There is not data so tell user. + layout = { + 'xaxis': { + 'visible': false + }, + 'yaxis': { + 'visible': false + }, + 'annotations': [ + { + 'text': `${translate('select.meter.group')}`, + 'xref': 'paper', + 'yref': 'paper', + 'showarrow': false, + 'font': { + 'size': 28 + } + } + ] + } + + } else { + // This normal so plot. + layout = { + barmode: (barStacking ? 'stack' : 'group'), + bargap: 0.2, // Gap between different times of readings + bargroupgap: 0.1, // Gap between different meter's readings under the same timestamp + autosize: true, + height: 700, // Height is set to 700 for now, but we do need to scale in the future (issue #466) + showlegend: true, + legend: { + x: 0, + y: 1.1, + orientation: 'h' + }, + yaxis: { + title: unitLabel, + showgrid: true, + gridcolor: '#ddd' + }, + xaxis: { + showgrid: true, + gridcolor: '#ddd', + tickfont: { + size: 10 + }, + tickangle: -45, + autotick: true, + nticks: 10, + automargin: true + }, + margin: { + t: 0, + b: 120, + l: 120 + } + }; + } + + // Assign all the parameters required to create the Plotly object (data, layout, config) to the variable props, returned by mapStateToProps + // The Plotly toolbar is displayed if displayModeBar is set to true (not for bar charts) + const config = { + displayModeBar: false, + responsive: true, + locales: Locales // makes locales available for use + } + return ( + + ); +} + diff --git a/src/client/app/components/DashboardComponent.tsx b/src/client/app/components/DashboardComponent.tsx index daa231fee..6a72c46f9 100644 --- a/src/client/app/components/DashboardComponent.tsx +++ b/src/client/app/components/DashboardComponent.tsx @@ -3,37 +3,29 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import UIOptionsContainer from '../containers/UIOptionsContainer'; -import LineChartContainer from '../containers/LineChartContainer'; -import BarChartContainer from '../containers/BarChartContainer'; -import MultiCompareChartContainer from '../containers/MultiCompareChartContainer'; import MapChartContainer from '../containers/MapChartContainer'; -import ThreeDComponent from './ThreeDComponent'; +import MultiCompareChartContainer from '../containers/MultiCompareChartContainer'; +import UIOptionsContainer from '../containers/UIOptionsContainer'; +import { useAppSelector } from '../redux/hooks'; +import { selectChartQueryArgs } from '../redux/selectors/dataSelectors'; import { ChartTypes } from '../types/redux/graph'; -import * as moment from 'moment'; -import { TimeInterval } from '../../../common/TimeInterval'; -import { Button } from 'reactstrap'; -import { FormattedMessage } from 'react-intl'; -import TooltipMarkerComponent from './TooltipMarkerComponent'; -import { useAppDispatch, useAppSelector } from '../redux/hooks'; -import { changeGraphZoomIfNeeded } from '../actions/graph'; -import { Dispatch } from '../types/redux/actions'; +import BarChartComponent from './BarChartComponent'; +import LineChartComponent from './LineChartComponent'; +import ThreeDComponent from './ThreeDComponent'; /** * React component that controls the dashboard * @returns the Primary Dashboard Component comprising of Ui Controls, and */ export default function DashboardComponent() { - const dispatch: Dispatch = useAppDispatch(); const chartToRender = useAppSelector(state => state.graph.chartToRender); const optionsVisibility = useAppSelector(state => state.graph.optionsVisibility); + const queryArgs = useAppSelector(state => selectChartQueryArgs(state)) const optionsClassName = optionsVisibility ? 'col-2 d-none d-lg-block' : 'd-none'; const chartClassName = optionsVisibility ? 'col-12 col-lg-10' : 'col-12'; - const buttonMargin: React.CSSProperties = { - marginRight: '10px' - }; + return (
@@ -42,16 +34,8 @@ export default function DashboardComponent() {
- { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - chartToRender === ChartTypes.line && - } - { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - chartToRender === ChartTypes.bar && - } + {chartToRender === ChartTypes.line && } + {chartToRender === ChartTypes.bar && } { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore @@ -63,73 +47,13 @@ export default function DashboardComponent() { chartToRender === ChartTypes.map && } { - chartToRender === ChartTypes.threeD && + chartToRender === ChartTypes.threeD && } - {(chartToRender === ChartTypes.line) ? ( - [, - , - - ] - ) : ( - null - )}
- + ); } -/** - * Determines the line graph's slider interval based after the slider is moved - * @returns The slider interval, either 'all' or a TimeInterval - */ -export function getRangeSliderInterval(): string { - const sliderContainer: any = document.querySelector('.rangeslider-bg'); - const sliderBox: any = document.querySelector('.rangeslider-slidebox'); - const root: any = document.getElementById('root'); - - if (sliderContainer && sliderBox && root) { - // Attributes of the slider: full width and the min & max values of the box - const fullWidth: number = parseInt(sliderContainer.getAttribute('width')); - const sliderMinX: number = parseInt(sliderBox.getAttribute('x')); - const sliderMaxX: number = sliderMinX + parseInt(sliderBox.getAttribute('width')); - if (sliderMaxX - sliderMinX === fullWidth) { - return 'all'; - } - - // From the Plotly line graph, get current min and max times in seconds - const minTimeStamp: number = parseInt(root.getAttribute('min-timestamp')); - const maxTimeStamp: number = parseInt(root.getAttribute('max-timestamp')); - // Seconds displayed on graph - const deltaSeconds: number = maxTimeStamp - minTimeStamp; - const secondsPerPixel: number = deltaSeconds / fullWidth; - - // Get the new min and max times, in seconds, from the slider box - const newMinXTimestamp = Math.floor(minTimeStamp + (secondsPerPixel * sliderMinX)); - const newMaxXTimestamp = Math.floor(minTimeStamp + (secondsPerPixel * sliderMaxX)); - // The newMin/MaxTimestamp is equivalent to a Unix time in milliseconds. Thus, it will - // shift with timezone. It isn't clear if we want it in local or UTC. It depends on what - // plotly does. Here it is assumed that local is what is desired. This seems to work - // and not shift the graphs x-axis so using. - return new TimeInterval(moment(newMinXTimestamp), moment(newMaxXTimestamp)).toString(); - } else { - throw new Error('unable to get range slider params'); - } -} diff --git a/src/client/app/components/ThreeDComponent.tsx b/src/client/app/components/ThreeDComponent.tsx index 53f415cdd..6df1708a0 100644 --- a/src/client/app/components/ThreeDComponent.tsx +++ b/src/client/app/components/ThreeDComponent.tsx @@ -2,40 +2,43 @@ * 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 * as moment from 'moment'; +import * as React from 'react'; import Plot from 'react-plotly.js'; -import ThreeDPillComponent from './ThreeDPillComponent'; -import SpinnerComponent from './SpinnerComponent'; -import { State } from '../types/redux/state'; import { useSelector } from 'react-redux'; -import { ThreeDReading } from '../types/readings' -import { roundTimeIntervalForFetch } from '../utils/dateRangeCompatibility'; -import { lineUnitLabel } from '../utils/graphics'; -import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; -import translate from '../utils/translate'; -import { isValidThreeDInterval } from '../utils/dateRangeCompatibility'; +import { ThreeDReadingApiParams, readingsApi } from '../redux/api/readingsApi'; +import { useAppSelector } from '../redux/hooks'; +import { selectThreeDComponentInfo } from '../redux/selectors/threeDSelectors'; +import { ThreeDReading } from '../types/readings'; import { GraphState, MeterOrGroup } from '../types/redux/graph'; -import { UnitsState } from '../types/redux/units'; -import { MetersState } from '../types/redux/meters'; import { GroupsState } from '../types/redux/groups'; -import { readingsApi } from '../redux/api/readingsApi' -import { useAppSelector } from '../redux/hooks'; -import { selectThreeDComponentInfo, selectThreeDQueryArgs, selectThreeDSkip } from '../redux/selectors/threeDSelectors' +import { MetersState } from '../types/redux/meters'; +import { State } from '../types/redux/state'; +import { UnitsState } from '../types/redux/units'; +import { isValidThreeDInterval, roundTimeIntervalForFetch } from '../utils/dateRangeCompatibility'; +import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; +import { lineUnitLabel } from '../utils/graphics'; +import translate from '../utils/translate'; +import SpinnerComponent from './SpinnerComponent'; +import ThreeDPillComponent from './ThreeDPillComponent'; +interface ThreeDChartProps { + queryArgs: ThreeDReadingApiParams, + skip: boolean +} /** * Component used to render 3D graphics + * @param props query args for the useQueryDataFetching hooks * @returns 3D Plotly 3D Surface Graph */ -export default function ThreeDComponent() { +export default function ThreeDComponent(props: ThreeDChartProps) { + const { queryArgs, skip } = props; + const { data, isFetching } = readingsApi.endpoints.threeD.useQuery(queryArgs, { skip: skip }); const metersState = useSelector((state: State) => state.meters); const groupsState = useSelector((state: State) => state.groups); const graphState = useSelector((state: State) => state.graph); const unitState = useSelector((state: State) => state.units); const { meterOrGroupID, meterOrGroupName, isAreaCompatible } = useAppSelector(selectThreeDComponentInfo); - const queryArgs = useAppSelector(selectThreeDQueryArgs); - const shouldSkip = useAppSelector(selectThreeDSkip); - const { data, isFetching } = readingsApi.endpoints.threeD.useQuery(queryArgs, { skip: shouldSkip }); // Initialize Default values diff --git a/src/client/app/containers/LineChartContainer.ts b/src/client/app/containers/LineChartContainer.ts index 39576e1b4..17e0f5e20 100644 --- a/src/client/app/containers/LineChartContainer.ts +++ b/src/client/app/containers/LineChartContainer.ts @@ -68,12 +68,8 @@ function mapStateToProps(state: State) { // Create two arrays for the x and y values. Fill the array with the data from the line readings const xData: string[] = []; const yData: number[] = []; - // Create two arrays to store the min and max values of y-axis data points - const yMinData: number[] = []; - const yMaxData: number[] = []; const hoverText: string[] = []; const readings = _.values(readingsData.readings); - // The scaling is the factor to change the reading by. It divides by the area while will be 1 if no scaling by area. readings.forEach(reading => { // As usual, we want to interpret the readings in UTC. We lose the timezone as this as the start/endTimestamp // are equivalent to Unix timestamp in milliseconds. @@ -83,20 +79,7 @@ function mapStateToProps(state: State) { xData.push(timeReading.format('YYYY-MM-DD HH:mm:ss')); const readingValue = reading.reading * scaling; yData.push(readingValue); - // All hover have the date, meter name and value. - const hoverStart = ` ${timeReading.format('ddd, ll LTS')}
${label}: ${readingValue.toPrecision(6)} ${unitLabel}`; - if (state.graph.showMinMax && reading.max != null) { - // We want to show min/max. Note if the data is raw for this meter then all the min/max values are null. - // In this case we still push the min/max but plotly will not show them. This is a little extra work - // but makes the code cleaner. - const minValue = reading.min * scaling; - yMinData.push(minValue); - const maxValue = reading.max * scaling; - yMaxData.push(maxValue); - hoverText.push(`${hoverStart}
${translate('min')}: ${minValue.toPrecision(6)}
${translate('max')}: ${maxValue.toPrecision(6)}`); - } else { - hoverText.push(hoverStart); - } + hoverText.push(` ${timeReading.format('ddd, ll LTS')}
${label}: ${readingValue.toPrecision(6)} ${unitLabel}`); }); /* @@ -118,13 +101,6 @@ function mapStateToProps(state: State) { name: label, x: xData, y: yData, - // only show error bars if enabled and there is data - error_y: state.graph.showMinMax && yMaxData.length > 0 ? { - type: 'data', - symmetric: false, - array: yMaxData.map((maxValue, index) => (maxValue - yData[index])), - arrayminus: yData.map((value, index) => (value - yMinData[index])) - } : undefined, text: hoverText, hoverinfo: 'text', type: 'scatter', @@ -150,12 +126,14 @@ function mapStateToProps(state: State) { // may not yet be in state so verify with the second condition on the if. // Note the second part may not be used based on next checks but do here since simple. if (byGroupID !== undefined && byGroupID[timeInterval.toString()] !== undefined) { - let groupArea = state.groups.byGroupID[groupID].area; + const groupArea = state.groups.byGroupID[groupID].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 || (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 group area into the proper unit if normalizing by area or use 1 if not so won't change reading values. + const areaScaling = state.graph.areaNormalization ? + groupArea * getAreaUnitConversion(state.groups.byGroupID[groupID].areaUnit, state.graph.selectedAreaUnit) : 1; + // Divide areaScaling into the rate so have complete scaling factor for readings. + const scaling = rateScaling / areaScaling; const readingsData = byGroupID[timeInterval.toString()][unitID]; if (readingsData !== undefined && !readingsData.isFetching) { const label = state.groups.byGroupID[groupID].name; @@ -169,39 +147,19 @@ function mapStateToProps(state: State) { const yData: number[] = []; const hoverText: string[] = []; const readings = _.values(readingsData.readings); - // Check if reading needs scaling outside of the loop so only one check is needed - // Results in more code but SLIGHTLY better efficiency :D - if (needsRateScaling) { - const rate = currentSelectedRate.rate; - readings.forEach(reading => { - // As usual, we want to interpret the readings in UTC. We lose the timezone as this as the start/endTimestamp - // are equivalent to Unix timestamp in milliseconds. - const st = moment.utc(reading.startTimestamp); - // Time reading is in the middle of the start and end timestamp - const timeReading = st.add(moment.utc(reading.endTimestamp).diff(st) / 2); - xData.push(timeReading.utc().format('YYYY-MM-DD HH:mm:ss')); - yData.push(reading.reading * rate); - hoverText.push(` ${timeReading.format('ddd, ll LTS')}
${label}: ${(reading.reading * rate).toPrecision(6)} ${unitLabel}`); - }); - } - else { - readings.forEach(reading => { - // As usual, we want to interpret the readings in UTC. We lose the timezone as this as the start/endTimestamp - // are equivalent to Unix timestamp in milliseconds. - const st = moment.utc(reading.startTimestamp); - // Time reading is in the middle of the start and end timestamp - const timeReading = st.add(moment.utc(reading.endTimestamp).diff(st) / 2); - xData.push(timeReading.utc().format('YYYY-MM-DD HH:mm:ss')); - let readingValue = reading.reading; - if (state.graph.areaNormalization) { - readingValue /= groupArea; - } - yData.push(readingValue); - hoverText.push(` ${timeReading.format('ddd, ll LTS')}
${label}: ${readingValue.toPrecision(6)} ${unitLabel}`); - }); - } + readings.forEach(reading => { + // As usual, we want to interpret the readings in UTC. We lose the timezone as this as the start/endTimestamp + // are equivalent to Unix timestamp in milliseconds. + const st = moment.utc(reading.startTimestamp); + // Time reading is in the middle of the start and end timestamp + const timeReading = st.add(moment.utc(reading.endTimestamp).diff(st) / 2); + xData.push(timeReading.utc().format('YYYY-MM-DD HH:mm:ss')); + const readingValue = reading.reading * scaling; + yData.push(readingValue); + hoverText.push(` ${timeReading.format('ddd, ll LTS')}
${label}: ${readingValue.toPrecision(6)} ${unitLabel}`); + }); - // get the min and max timestamp of the meter, and compare it to the global values + // get the min and max timestamp of the group, and compare it to the global values if (readings.length > 0) { if (minTimestamp == undefined || readings[0]['startTimestamp'] < minTimestamp) { minTimestamp = readings[0]['startTimestamp']; @@ -318,4 +276,4 @@ function mapStateToProps(state: State) { return props; } -export default connect(mapStateToProps)(Plot); +export default connect(mapStateToProps)(Plot); \ No newline at end of file diff --git a/src/client/app/index.tsx b/src/client/app/index.tsx index de348b18c..e6c871378 100644 --- a/src/client/app/index.tsx +++ b/src/client/app/index.tsx @@ -19,7 +19,7 @@ const container = document.getElementById('root'); const root = createRoot(container!); root.render( // Provides the Redux store to all child components - + {/* Route container is a test of react-router-dom v6 This update introduces many useful routing hooks which can potentially be useful when migrating the codebase to hooks from Class components. diff --git a/src/client/app/reducers/graph.ts b/src/client/app/reducers/graph.ts index d84d042ec..c8a01cf8e 100644 --- a/src/client/app/reducers/graph.ts +++ b/src/client/app/reducers/graph.ts @@ -132,36 +132,63 @@ export const graphSlice = createSlice({ state.threeD.meterOrGroup = action.payload }, updateSelectedMetersOrGroups: (state, action: PayloadAction<{ newMetersOrGroups: number[], meta: ActionMeta }>) => { + // This reducer handles the addition and subtraction values for both the meter and group select components. + // The 'MeterOrGroup' type is heavily utilized in the reducer and other parts of the code. + // Note that this option is binary, if it's not a meter, then it's a group. + // Destructure payload const { newMetersOrGroups, meta } = action.payload; // Used to check if value has been added or removed + // If 'meta.option' is defined, it indicates that a single value has been added or selected. const addedMeterOrGroupID = meta.option?.value; const addedMeterOrGroup = meta.option?.meterOrGroup; + const addedMeterOrGroupUnit = meta.option?.defaultGraphicUnit; + // If 'meta.removedValue' is defined, it indicates that a single value has been removed or deselected. const removedMeterOrGroupID = meta.removedValue?.value; const removedMeterOrGroup = meta.removedValue?.meterOrGroup; - const clearedMeterOrGroups = meta.removedValues; - // If no meters selected, and no area unit, we should update unit to default graphic unit - // const shouldUpdateUnit = !state.selectedGroups.length && !state.selectedMeters.length && state.selectedUnit === -99 - // If meterMeter added then and should update unit, update unit. - // TODO graphic unit is currently snuck into the select option, find an alternative pattern - // state.selectedUnit = addedMeterOrGroupID && !shouldUpdateUnit ? state.selectedUnit : meta. + // If meta.removedValues is defined, it indicates that all values have been cleared. + const clearedMeterOrGroups = meta.removedValues; - // Determine If meter or group was modified then update appropriately - const meterOrGroup = addedMeterOrGroup ? addedMeterOrGroup : removedMeterOrGroup; + // Generic if else block pertaining to all graph types + // Check for the three possible scenarios of a change in the meters if (clearedMeterOrGroups) { + // A Select has been cleared(all values removed with clear) // use the first index of cleared items to check for meter or group const isAMeter = clearedMeterOrGroups[0].meterOrGroup === MeterOrGroup.meters // if a meter clear meters, else clear groups isAMeter ? state.selectedMeters = [] : state.selectedGroups = [] - } else if (meterOrGroup && meterOrGroup === MeterOrGroup.meters) { - state.selectedMeters = newMetersOrGroups - } else { - state.selectedGroups = newMetersOrGroups + + } else if (removedMeterOrGroup) { + // An entry was deleted. + // Update either selected meters or groups + + removedMeterOrGroup === MeterOrGroup.meters ? + state.selectedMeters = newMetersOrGroups + : + state.selectedGroups = newMetersOrGroups + + } else if (addedMeterOrGroup) { + // An entry was added, + // Update either selected meters or groups + addedMeterOrGroup === MeterOrGroup.meters ? + state.selectedMeters = newMetersOrGroups + : + state.selectedGroups = newMetersOrGroups + + // If the current unit is -99, there is not yet a graphic unit + // Set the newly added meterOrGroup's default graphic unit as the current selected unit. + if (state.selectedUnit === -99 && addedMeterOrGroupUnit) { + state.selectedUnit = addedMeterOrGroupUnit; + } } + + // Blocks Pertaining to behaviors of specific pages + + // Additional 3d logic // When a meter or group is selected/added, make it the currently active in 3D state. if (addedMeterOrGroupID && addedMeterOrGroup && state.chartToRender === ChartTypes.threeD) { // TODO Currently only tracks when on 3d, Verify that this is the desired behavior diff --git a/src/client/app/redux/api/baseApi.ts b/src/client/app/redux/api/baseApi.ts index 54eda82f9..78d57a239 100644 --- a/src/client/app/redux/api/baseApi.ts +++ b/src/client/app/redux/api/baseApi.ts @@ -19,7 +19,7 @@ export const baseApi = createApi({ // The types of tags that any injected endpoint may, provide, or invalidate. tagTypes: ['MeterData', 'GroupData', 'GroupChildrenData', 'Preferences'], // Initially no defined endpoints, Use rtk query's injectEndpoints - endpoints: () => ({}), - // Keep Data in Cache for 10 Minutes (600 seconds) - keepUnusedDataFor: 600 + endpoints: () => ({}) + // Defaults to 60 seconds or 1 minute + // keepUnusedDataFor: 60 }) \ No newline at end of file diff --git a/src/client/app/redux/api/readingsApi.ts b/src/client/app/redux/api/readingsApi.ts index 2650067c5..ddcd36bc7 100644 --- a/src/client/app/redux/api/readingsApi.ts +++ b/src/client/app/redux/api/readingsApi.ts @@ -1,6 +1,7 @@ -import { baseApi } from './baseApi' -import { ThreeDReading } from '../../types/readings' +import { BarReadingApiArgs, LineReadingApiArgs } from '../../redux/selectors/dataSelectors'; +import { BarReadings, LineReadings, ThreeDReading } from '../../types/readings'; import { MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; +import { baseApi } from './baseApi'; export type ThreeDReadingApiParams = { @@ -11,15 +12,29 @@ export type ThreeDReadingApiParams = { meterOrGroup: MeterOrGroup; }; + export const readingsApi = baseApi.injectEndpoints({ endpoints: builder => ({ threeD: builder.query({ query: ({ meterOrGroupID, timeInterval, unitID, readingInterval, meterOrGroup }) => { - const endpoint = `/api/unitReadings/threeD/${meterOrGroup}/` + const endpoint = `api/unitReadings/threeD/${meterOrGroup}/` const args = `${meterOrGroupID}?timeInterval=${timeInterval.toString()}&graphicUnitId=${unitID}&readingInterval=${readingInterval}` return `${endpoint}${args}` } + }), + line: builder.query({ + query: ({ selectedMeters, selectedGroups, timeInterval, graphicUnitID, meterOrGroup }) => { + const stringifiedIDs = meterOrGroup === MeterOrGroup.meters ? selectedMeters.join(',') : selectedGroups.join(',') + return `api/unitReadings/line/${meterOrGroup}/${stringifiedIDs}?timeInterval=${timeInterval}&graphicUnitId=${graphicUnitID}` + } + }), + bar: builder.query({ + query: ({ selectedMeters, selectedGroups, timeInterval, graphicUnitID, meterOrGroup, barWidthDays }) => { + const stringifiedIDs = meterOrGroup === MeterOrGroup.meters ? selectedMeters.join(',') : selectedGroups.join(',') + const endpoint = `api/unitReadings/bar/${meterOrGroup}/${stringifiedIDs}` + const args = `?timeInterval=${timeInterval}&barWidthDays=${barWidthDays}&graphicUnitId=${graphicUnitID}` + return `${endpoint}${args}` + } }) }) -}) -export const selectThreeDReadingData = readingsApi.endpoints.threeD.select \ No newline at end of file +}) \ No newline at end of file diff --git a/src/client/app/redux/selectors/dataSelectors.ts b/src/client/app/redux/selectors/dataSelectors.ts index 8ad0ca9fc..565ada747 100644 --- a/src/client/app/redux/selectors/dataSelectors.ts +++ b/src/client/app/redux/selectors/dataSelectors.ts @@ -1,8 +1,11 @@ import { createSelector } from '@reduxjs/toolkit'; import * as _ from 'lodash'; +import { ThreeDReadingApiParams } from '../../redux/api/readingsApi'; import { RootState } from '../../store'; +import { MeterOrGroup } from '../../types/redux/graph'; import { GroupDefinition } from '../../types/redux/groups'; import { MeterData } from '../../types/redux/meters'; +import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; import { selectIsLoggedInAsAdmin } from './authSelectors'; @@ -14,6 +17,9 @@ export const selectMeterState = (state: RootState) => state.meters; export const selectGroupState = (state: RootState) => state.groups; export const selectUnitState = (state: RootState) => state.units; export const selectMapState = (state: RootState) => state.maps; +export const selectThreeDState = (state: RootState) => state.graph.threeD; +export const selectBarWidthDays = (state: RootState) => state.graph.barDuration; +export const selectGraphState = (state: RootState) => state.graph; export const selectVisibleMetersGroupsDataByID = createSelector( selectMeterDataByID, @@ -36,4 +42,66 @@ export const selectVisibleMetersGroupsDataByID = createSelector( return { visibleMeters, visibleGroups } } +) +// line/meters/10,11,12?timeInterval=2020-05-02T14:04:36Z_2020-09-08T15:00:00Z&graphicUnitId=1 +// bar/meters/21,22,10,18?timeInterval=2020-05-02T14:04:36Z_2020-09-08T15:00:00Z&barWidthDays=28&graphicUnitId=1 + +export interface ChartQueryArgs { + meterArgs: T + groupsArgs: T +} +export interface ChartQueryProps { + queryProps: ChartQueryArgs +} +export interface commonArgs { + selectedMeters: number[]; + selectedGroups: number[]; + timeInterval: string; + graphicUnitID: number; + meterOrGroup: MeterOrGroup; +} +export interface LineReadingApiArgs extends commonArgs { } +export interface BarReadingApiArgs extends commonArgs { barWidthDays: number } + +export const selectChartQueryArgs = createSelector( + selectGraphState, + graphState => { + const baseMeterArgs: commonArgs = { + selectedMeters: graphState.selectedMeters, + selectedGroups: graphState.selectedGroups, + timeInterval: graphState.timeInterval.toString(), + graphicUnitID: graphState.selectedUnit, + meterOrGroup: MeterOrGroup.meters + } + + + const baseGroupArgs: commonArgs = { + ...baseMeterArgs, + meterOrGroup: MeterOrGroup.groups + } + + const line: ChartQueryArgs = { + meterArgs: baseMeterArgs, + groupsArgs: baseGroupArgs + } + + const bar: ChartQueryArgs = { + meterArgs: { ...baseMeterArgs, barWidthDays: Math.round(graphState.barDuration.asDays()) }, + groupsArgs: { ...baseGroupArgs, barWidthDays: Math.round(graphState.barDuration.asDays()) } + } + + + const threeD = { + args: { + meterOrGroupID: graphState.threeD.meterOrGroupID, + timeInterval: roundTimeIntervalForFetch(graphState.timeInterval).toString(), + unitID: graphState.selectedUnit, + readingInterval: graphState.threeD.readingInterval, + meterOrGroup: graphState.threeD.meterOrGroup + } as ThreeDReadingApiParams, + skip: !graphState.threeD.meterOrGroupID || !graphState.timeInterval.getIsBounded() + } + + return { line, bar, threeD } + } ) \ No newline at end of file diff --git a/src/client/app/store.ts b/src/client/app/store.ts index 1a9179c6b..61b9c3af5 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -10,7 +10,7 @@ import { baseApi } from './redux/api/baseApi'; export const store = configureStore({ reducer: rootReducer, middleware: getDefaultMiddleware => getDefaultMiddleware({ - immutableCheck: false, + // immutableCheck: false, serializableCheck: false }).concat(baseApi.middleware) }); From 535bafbce4353e4b1d6446b33b446be3bffccb3a Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Tue, 3 Oct 2023 23:16:15 +0000 Subject: [PATCH 022/131] UserChanges. --- .../app/components/RouteComponentWIP.tsx | 4 +- .../admin/CreateUserComponentWIP.tsx | 89 ++++++++++++ .../admin/UsersDetailComponentWIP.tsx | 136 ++++++++++++++++++ .../containers/admin/UsersDetailContainer.tsx | 10 +- src/client/app/redux/api/baseApi.ts | 2 +- src/client/app/redux/api/userApi.ts | 43 +++++- src/client/app/types/items.ts | 3 + 7 files changed, 275 insertions(+), 12 deletions(-) create mode 100644 src/client/app/components/admin/CreateUserComponentWIP.tsx create mode 100644 src/client/app/components/admin/UsersDetailComponentWIP.tsx diff --git a/src/client/app/components/RouteComponentWIP.tsx b/src/client/app/components/RouteComponentWIP.tsx index 21133b014..b50ca0834 100644 --- a/src/client/app/components/RouteComponentWIP.tsx +++ b/src/client/app/components/RouteComponentWIP.tsx @@ -10,7 +10,6 @@ import { BrowserRouter } from 'react-router-dom'; import { CompatRouter, Navigate, Outlet, Route, Routes, useSearchParams } from 'react-router-dom-v5-compat'; import { TimeInterval } from '../../../common/TimeInterval'; import CreateUserContainer from '../containers/admin/CreateUserContainer'; -import UsersDetailContainer from '../containers/admin/UsersDetailContainer'; import UploadCSVContainer from '../containers/csv/UploadCSVContainer'; import MapCalibrationContainer from '../containers/maps/MapCalibrationContainer'; import MapsDetailContainer from '../containers/maps/MapsDetailContainer'; @@ -29,6 +28,7 @@ import HomeComponent from './HomeComponent'; import LoginComponent from './LoginComponent'; import SpinnerComponent from './SpinnerComponent'; import AdminComponent from './admin/AdminComponent'; +import UsersDetailComponentWIP from './admin/UsersDetailComponentWIP'; import ConversionsDetailComponent from './conversion/ConversionsDetailComponent'; import GroupsDetailComponent from './groups/GroupsDetailComponent'; import MetersDetailComponent from './meters/MetersDetailComponent'; @@ -67,7 +67,7 @@ export default function RouteComponentWIP() { } /> } /> } /> - } /> + } /> }> } /> diff --git a/src/client/app/components/admin/CreateUserComponentWIP.tsx b/src/client/app/components/admin/CreateUserComponentWIP.tsx new file mode 100644 index 000000000..321ad1ebb --- /dev/null +++ b/src/client/app/components/admin/CreateUserComponentWIP.tsx @@ -0,0 +1,89 @@ +/* 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 { FormattedMessage } from 'react-intl'; +import { Button, Input } from 'reactstrap'; +import HeaderComponent from '../../components/HeaderComponent'; +import FooterContainer from '../../containers/FooterContainer'; +import { userApi } from '../../redux/api/userApi'; +import { NewUser, UserRole } from '../../types/items'; +import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; +import translate from '../../utils/translate'; + + +/** + * Component that defines the form to create a new user + * @returns Create User Page + */ +export default function CreateUserComponentWIP() { + const [email, setEmail] = React.useState(''); + const [password, setPassword] = React.useState(''); + const [confirmPassword, setConfirmPassword] = React.useState(''); + const [role, setRole] = React.useState(UserRole.ADMIN); + const [createUser] = userApi.useCreateUserMutation(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const newUser: NewUser = { email, role, password } + createUser(newUser) + .unwrap() + .then(() => { + showSuccessNotification(translate('users.successfully.create.user')) + + }).catch(() => { + showErrorNotification(translate('users.failed.to.create.user')); + }) + + } + return ( +
+ +
+

+
+
+
+
+ setEmail(target.value)} required value={email} /> +
+
+
+ setPassword(target.value)} required value={password} /> +
+
+
+ setConfirmPassword(target.value)} required value={confirmPassword} /> +
+
+
+ setRole(target.value as UserRole)} value={role}> + {Object.entries(UserRole).map(([role, val]) => ( + + ))} + +
+
+ +
+
+
+
+ +
+ + ) +} +const formInputStyle: React.CSSProperties = { + paddingBottom: '5px' +}; +const titleStyle: React.CSSProperties = { + textAlign: 'center' +}; + +const tableStyle: React.CSSProperties = { + marginLeft: '25%', + marginRight: '25%', + width: '50%' +}; \ No newline at end of file diff --git a/src/client/app/components/admin/UsersDetailComponentWIP.tsx b/src/client/app/components/admin/UsersDetailComponentWIP.tsx new file mode 100644 index 000000000..a26dcd7a5 --- /dev/null +++ b/src/client/app/components/admin/UsersDetailComponentWIP.tsx @@ -0,0 +1,136 @@ +/* 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 _ from 'lodash'; +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Button, Input, Table } from 'reactstrap'; +import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import UnsavedWarningContainer from '../../containers/UnsavedWarningContainer'; +import { userApi } from '../../redux/api/userApi'; +import { User, UserRole } from '../../types/items'; +import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; +import translate from '../../utils/translate'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; +import CreateUserLinkButtonComponent from './users/CreateUserLinkButtonComponent'; +import FooterContainer from '../../containers/FooterContainer'; +import HeaderComponent from '../HeaderComponent'; + +/** + * Component which shows user details + * @returns User Detail element + */ +export default function UserDetailComponentWIP() { + const { data: users = [] } = userApi.useGetUsersQuery(undefined); + const [submitUserEdits] = userApi.useEditUsersMutation(); + const [localUsersChanges, setLocalUsersChanges] = React.useState([]); + + // keep history in sync whenever query data changes + React.useEffect(() => { setLocalUsersChanges(users) }, [users]) + + const editUser = (e: React.ChangeEvent, targetUser: User) => { + // copy user, and update role + const updatedUser: User = { ...targetUser, role: e.target.value as UserRole } + // make new list from existing local user state + const updatedList = localUsersChanges.map(user => (user.email === targetUser.email) ? updatedUser : user) + setLocalUsersChanges(updatedList) + // editUser(user.email, target.value as UserRole); + } + const submitChanges = async () => { + submitUserEdits(localUsersChanges) + .unwrap() + .then(() => { + showSuccessNotification(translate('users.successfully.edit.users')); + }) + .catch((e) => { + console.log(e) + showErrorNotification(translate('users.failed.to.edit.users')) + }) + } + + const deleteUser = (email: string) => { + setLocalUsersChanges(localUsersChanges.filter(user => user.email !== email)) + } + + + return ( +
+ + + +
+

+ +
+ +
+

+
+ + + + + + + + + + {localUsersChanges.map(user => ( + + + + + + ))} + +
{user.email} + editUser(e, user)} + > + {Object.entries(UserRole).map(([role, val]) => ( + + ))} + + + +
+
+ + +
+
+
+ +
+ ) +} + +const titleStyle: React.CSSProperties = { + textAlign: 'center' +}; + +const tableStyle: React.CSSProperties = { + marginLeft: '10%', + marginRight: '10%' +}; + +const buttonsStyle: React.CSSProperties = { + display: 'flex', + justifyContent: 'space-between' +} + +const tooltipStyle = { + display: 'inline-block', + fontSize: '50%' +}; \ No newline at end of file diff --git a/src/client/app/containers/admin/UsersDetailContainer.tsx b/src/client/app/containers/admin/UsersDetailContainer.tsx index 802bd0a85..c7d211cbb 100644 --- a/src/client/app/containers/admin/UsersDetailContainer.tsx +++ b/src/client/app/containers/admin/UsersDetailContainer.tsx @@ -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 * as React from 'react'; import * as _ from 'lodash'; -import { User, UserRole } from '../../types/items'; +import * as React from 'react'; +import HeaderComponent from '../../components/HeaderComponent'; import UserDetailComponent from '../../components/admin/UsersDetailComponent'; -import FooterContainer from '../FooterContainer'; +import { User, UserRole } from '../../types/items'; import { usersApi } from '../../utils/api'; -import { showSuccessNotification, showErrorNotification } from '../../utils/notifications'; +import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; import translate from '../../utils/translate'; -import HeaderComponent from '../../components/HeaderComponent'; +import FooterContainer from '../FooterContainer'; interface UsersDisplayContainerProps { fetchUsers: () => User[]; diff --git a/src/client/app/redux/api/baseApi.ts b/src/client/app/redux/api/baseApi.ts index 78d57a239..bee24fa6a 100644 --- a/src/client/app/redux/api/baseApi.ts +++ b/src/client/app/redux/api/baseApi.ts @@ -17,7 +17,7 @@ export const baseApi = createApi({ } }), // The types of tags that any injected endpoint may, provide, or invalidate. - tagTypes: ['MeterData', 'GroupData', 'GroupChildrenData', 'Preferences'], + tagTypes: ['MeterData', 'GroupData', 'GroupChildrenData', 'Preferences','Users'], // Initially no defined endpoints, Use rtk query's injectEndpoints endpoints: () => ({}) // Defaults to 60 seconds or 1 minute diff --git a/src/client/app/redux/api/userApi.ts b/src/client/app/redux/api/userApi.ts index 2a8f509ab..4a6dab595 100644 --- a/src/client/app/redux/api/userApi.ts +++ b/src/client/app/redux/api/userApi.ts @@ -1,4 +1,4 @@ -import { User } from '../../types/items'; +import { NewUser, User } from '../../types/items'; // import { authApi } from './authApi'; import { baseApi } from './baseApi'; @@ -8,7 +8,42 @@ export const userApi = baseApi.injectEndpoints({ query: () => 'api/users/token', // Do not retain response when no subscribers keepUnusedDataFor: 0 - } - ) + }), + getUsers: builder.query({ + query: () => 'api/users', + providesTags: ['Users'] + }), + createUser: builder.mutation({ + query: user => ({ + url: 'api/users/create', + method: 'POST', + body: { user } + }), + invalidatesTags: ['Users'] + }), + editUsers: builder.mutation({ + query: users => ({ + url: 'api/users/edit', + method: 'POST', + body: { users } + }), + invalidatesTags: ['Users'] + }), + deleteUsers: builder.mutation({ + query: email => ({ + url: 'api/users/delete', + method: 'POST', + body: { email } + }), + invalidatesTags: ['Users'] + }) }) -}) \ No newline at end of file +}) + +// public async editUsers(users: User[]) { +// return await this.backend.doPostRequest('/api/users/edit', { users }); +// } + +// public async deleteUser(email: string) { +// return await this.backend.doPostRequest('/api/users/delete', { email }); +// } \ No newline at end of file diff --git a/src/client/app/types/items.ts b/src/client/app/types/items.ts index e7cbabc05..832a01b74 100644 --- a/src/client/app/types/items.ts +++ b/src/client/app/types/items.ts @@ -80,6 +80,9 @@ export interface User { email: string; role: UserRole; } +export interface NewUser extends User { + password: string; +} /** * The values of this enum that needs to match the keys of User.role in src/server/models/User From 14dfcc110bb008316866bdf68a2f07da170ba2f5 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Sun, 8 Oct 2023 21:33:21 +0000 Subject: [PATCH 023/131] Query updates --- .../app/components/BarChartComponent.tsx | 4 ++-- .../app/components/LineChartComponent.tsx | 6 ++++-- src/client/app/redux/api/readingsApi.ts | 10 ++++------ .../app/redux/selectors/dataSelectors.ts | 19 ++++++++++++------- src/client/app/redux/selectors/uiSelectors.ts | 7 +++++++ 5 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/client/app/components/BarChartComponent.tsx b/src/client/app/components/BarChartComponent.tsx index f88d2fd0f..98bd5aa3f 100644 --- a/src/client/app/components/BarChartComponent.tsx +++ b/src/client/app/components/BarChartComponent.tsx @@ -35,12 +35,12 @@ export default function BarChartComponent(props: ChartQueryProps state.graph.barDuration); const barStacking = useAppSelector(state => state.graph.barStacking); diff --git a/src/client/app/components/LineChartComponent.tsx b/src/client/app/components/LineChartComponent.tsx index c1f1eddf8..0e6ff0419 100644 --- a/src/client/app/components/LineChartComponent.tsx +++ b/src/client/app/components/LineChartComponent.tsx @@ -38,12 +38,12 @@ export default function LineChartComponent(props: ChartQueryProps state.graph.selectedUnit); const datasets: any[] = []; @@ -339,6 +339,8 @@ export default function LineChartComponent(props: ChartQueryProps console.log(e.layout.xaxis?.range, e.layout.xaxis?.rangeslider?.range, e.layout.xaxis?.rangeselector)} + onUpdate={e => console.log(e.layout.xaxis?.range, e.layout.xaxis?.rangeslider?.range, e.layout.xaxis?.rangeselector)} onRelayout={handleRelayout} config={config} style={{ width: '100%', height: '80%' }} diff --git a/src/client/app/redux/api/readingsApi.ts b/src/client/app/redux/api/readingsApi.ts index ddcd36bc7..47a0e9046 100644 --- a/src/client/app/redux/api/readingsApi.ts +++ b/src/client/app/redux/api/readingsApi.ts @@ -23,15 +23,13 @@ export const readingsApi = baseApi.injectEndpoints({ } }), line: builder.query({ - query: ({ selectedMeters, selectedGroups, timeInterval, graphicUnitID, meterOrGroup }) => { - const stringifiedIDs = meterOrGroup === MeterOrGroup.meters ? selectedMeters.join(',') : selectedGroups.join(',') - return `api/unitReadings/line/${meterOrGroup}/${stringifiedIDs}?timeInterval=${timeInterval}&graphicUnitId=${graphicUnitID}` + query: ({ ids, timeInterval, graphicUnitID, meterOrGroup }) => { + return `api/unitReadings/line/${meterOrGroup}/${ids.join(',')}?timeInterval=${timeInterval}&graphicUnitId=${graphicUnitID}` } }), bar: builder.query({ - query: ({ selectedMeters, selectedGroups, timeInterval, graphicUnitID, meterOrGroup, barWidthDays }) => { - const stringifiedIDs = meterOrGroup === MeterOrGroup.meters ? selectedMeters.join(',') : selectedGroups.join(',') - const endpoint = `api/unitReadings/bar/${meterOrGroup}/${stringifiedIDs}` + query: ({ ids, timeInterval, graphicUnitID, meterOrGroup, barWidthDays }) => { + const endpoint = `api/unitReadings/bar/${meterOrGroup}/${ids.join(',')}` const args = `?timeInterval=${timeInterval}&barWidthDays=${barWidthDays}&graphicUnitId=${graphicUnitID}` return `${endpoint}${args}` } diff --git a/src/client/app/redux/selectors/dataSelectors.ts b/src/client/app/redux/selectors/dataSelectors.ts index 565ada747..1539f8d52 100644 --- a/src/client/app/redux/selectors/dataSelectors.ts +++ b/src/client/app/redux/selectors/dataSelectors.ts @@ -50,16 +50,18 @@ export interface ChartQueryArgs { meterArgs: T groupsArgs: T } + export interface ChartQueryProps { queryProps: ChartQueryArgs } + export interface commonArgs { - selectedMeters: number[]; - selectedGroups: number[]; + ids: number[]; timeInterval: string; graphicUnitID: number; meterOrGroup: MeterOrGroup; } + export interface LineReadingApiArgs extends commonArgs { } export interface BarReadingApiArgs extends commonArgs { barWidthDays: number } @@ -67,16 +69,18 @@ export const selectChartQueryArgs = createSelector( selectGraphState, graphState => { const baseMeterArgs: commonArgs = { - selectedMeters: graphState.selectedMeters, - selectedGroups: graphState.selectedGroups, + // Sort the arrays immutably. Sorting the arrays helps with cache hits. + ids: [...graphState.selectedMeters].sort(), timeInterval: graphState.timeInterval.toString(), graphicUnitID: graphState.selectedUnit, meterOrGroup: MeterOrGroup.meters } - const baseGroupArgs: commonArgs = { - ...baseMeterArgs, + // Sort the arrays immutably. Sorting the arrays helps with cache hits. + ids: [...graphState.selectedGroups].sort(), + timeInterval: graphState.timeInterval.toString(), + graphicUnitID: graphState.selectedUnit, meterOrGroup: MeterOrGroup.groups } @@ -104,4 +108,5 @@ export const selectChartQueryArgs = createSelector( return { line, bar, threeD } } -) \ 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 73d23cdc6..483365817 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -518,3 +518,10 @@ export function getSelectOptionsByItem(compatibleItems: Set, incompatibl return { compatible: sortedCompatibleOptions, incompatible: sortedIncompatibleOptions } } + +export const selectDateRangeInterval = createSelector( + selectGraphTimeInterval, + timeInterval => { + return timeInterval + } +) \ No newline at end of file From a09835173cb22074a18d2f0ccf3c4db3b9973cb2 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Sun, 8 Oct 2023 23:41:32 +0000 Subject: [PATCH 024/131] RTK 5.0.0-beta.2 Selector Refactor - Selectors live in CreateSlice's selector's property --- package-lock.json | 47 ++++++---- package.json | 2 +- .../app/components/BarChartComponent.tsx | 21 +++-- .../app/components/LineChartComponent.tsx | 22 +++-- .../app/components/UIOptionsComponent.tsx | 4 +- .../groups/GroupsDetailComponent.tsx | 5 +- .../meters/MetersDetailComponent.tsx | 5 +- .../components/unit/UnitsDetailComponent.tsx | 4 +- src/client/app/reducers/graph.ts | 17 +++- src/client/app/reducers/groups.ts | 5 ++ src/client/app/reducers/maps.ts | 16 ++-- src/client/app/reducers/meters.ts | 5 ++ src/client/app/reducers/units.ts | 5 ++ .../app/redux/selectors/dataSelectors.ts | 33 +++---- .../app/redux/selectors/threeDSelectors.ts | 29 +++--- src/client/app/redux/selectors/uiSelectors.ts | 88 +++++++++++-------- 16 files changed, 182 insertions(+), 126 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7d522dd1e..1a3c17c53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "open-energy-dashboard", - "version": "0.8.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-energy-dashboard", - "version": "0.8.0", + "version": "1.0.0", "license": "MPL-2.0", "dependencies": { - "@reduxjs/toolkit": "~1.9.5", + "@reduxjs/toolkit": "~2.0.0-beta.2", "@wojtekmaj/react-daterange-picker": "~5.2.0", "axios": "~0.24.0", "bcryptjs": "~2.4.3", @@ -2538,18 +2538,18 @@ } }, "node_modules/@reduxjs/toolkit": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.5.tgz", - "integrity": "sha512-Rt97jHmfTeaxL4swLRNPD/zV4OxTes4la07Xc4hetpUW/vc75t5m1ANyxG6ymnEQ2FsLQsoMlYB2vV1sO3m8tQ==", + "version": "2.0.0-beta.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.0.0-beta.2.tgz", + "integrity": "sha512-LVfySlJ5UUFWM6jBD869CAK568f0iF8sI0opzcFn1KFNhn7sYQ9s+MgIc2vUCCGsrDr3fqp9QYFnmtDofeDN1A==", "dependencies": { - "immer": "^9.0.21", - "redux": "^4.2.1", - "redux-thunk": "^2.4.2", - "reselect": "^4.1.8" + "immer": "^10.0.2", + "redux": "^5.0.0-beta.0", + "redux-thunk": "^3.0.0-beta.0", + "reselect": "^5.0.0-alpha.2" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18", - "react-redux": "^7.2.1 || ^8.0.2" + "react-redux": "^7.2.1 || ^8.0.2 || ^9.0.0-alpha.1" }, "peerDependenciesMeta": { "react": { @@ -2560,6 +2560,19 @@ } } }, + "node_modules/@reduxjs/toolkit/node_modules/redux": { + "version": "5.0.0-beta.0", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.0-beta.0.tgz", + "integrity": "sha512-RHSGHIiJ+1nkuve0daeveubiEdloy+DkYkP63uHk2FHpP18kb5umytsPU8TY8Lw8sLjL1eFg0DD5yf99ry/JhA==" + }, + "node_modules/@reduxjs/toolkit/node_modules/redux-thunk": { + "version": "3.0.0-beta.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.0.0-beta.0.tgz", + "integrity": "sha512-BLed4FtBhPv52AgqeR7DiOhrDA8z6owqXOkObOqgl1kwq4QQ1T74dy32qxyWsdyAlvq9wAHHW6t4tlxz8XnFhA==", + "peerDependencies": { + "redux": "^4 || ^5.0.0-beta.0" + } + }, "node_modules/@remix-run/router": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.9.0.tgz", @@ -7354,9 +7367,9 @@ "dev": true }, "node_modules/immer": { - "version": "9.0.21", - "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", - "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz", + "integrity": "sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==", "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -10817,9 +10830,9 @@ "peer": true }, "node_modules/reselect": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", - "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + "version": "5.0.0-alpha.2", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.0.0-alpha.2.tgz", + "integrity": "sha512-wachIH1FWB/ceIgBP418PXtjJyhvgjtjqi0Go5nCqe/2xrwwAyCn1/4krfBurNfxxo7dWpiLGb1yYjCrWi40PA==" }, "node_modules/resolve": { "version": "1.22.4", diff --git a/package.json b/package.json index 9ed64124b..1fc19ae73 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "babel-plugin-lodash": "~3.3.4" }, "dependencies": { - "@reduxjs/toolkit": "~1.9.5", + "@reduxjs/toolkit": "~2.0.0-beta.2", "@wojtekmaj/react-daterange-picker": "~5.2.0", "axios": "~0.24.0", "bcryptjs": "~2.4.3", diff --git a/src/client/app/components/BarChartComponent.tsx b/src/client/app/components/BarChartComponent.tsx index 98bd5aa3f..684e880d2 100644 --- a/src/client/app/components/BarChartComponent.tsx +++ b/src/client/app/components/BarChartComponent.tsx @@ -8,12 +8,7 @@ import * as React from 'react'; import Plot from 'react-plotly.js'; import { readingsApi } from '../redux/api/readingsApi'; import { useAppSelector } from '../redux/hooks'; -import { - BarReadingApiArgs, ChartQueryProps, - selectGroupDataByID, - selectMeterDataByID, selectUnitDataById -} from '../redux/selectors/dataSelectors'; -import { selectSelectedGroups, selectSelectedMeters } from '../redux/selectors/uiSelectors'; +import { BarReadingApiArgs, ChartQueryProps } from '../redux/selectors/dataSelectors'; import { DataType } from '../types/Datasources'; import Locales from '../types/locales'; import { UnitRepresentType } from '../types/redux/units'; @@ -22,6 +17,10 @@ import getGraphColor from '../utils/getGraphColor'; import { barUnitLabel } from '../utils/graphics'; import translate from '../utils/translate'; import SpinnerComponent from './SpinnerComponent'; +import { graphSlice } from '../reducers/graph'; +import { groupsSlice } from '../reducers/groups'; +import { metersSlice } from '../reducers/meters'; +import { unitsSlice } from '../reducers/units'; /** * Passes the current redux state of the barchart, and turns it into props for the React @@ -48,13 +47,13 @@ export default function BarChartComponent(props: ChartQueryProps state.graph.selectedUnit); - const unitDataByID = useAppSelector(state => selectUnitDataById(state)); + const unitDataByID = useAppSelector(state => unitsSlice.selectors.unitDataById(state)); const selectedAreaNormalization = useAppSelector(state => state.graph.areaNormalization); const selectedAreaUnit = useAppSelector(state => state.graph.selectedAreaUnit); - const selectedMeters = useAppSelector(state => selectSelectedMeters(state)) - const selectedGroups = useAppSelector(state => selectSelectedGroups(state)) - const meterDataByID = useAppSelector(state => selectMeterDataByID(state)) - const groupDataByID = useAppSelector(state => selectGroupDataByID(state)) + const selectedMeters = useAppSelector(state => graphSlice.selectors.selectedMeters(state)); + const selectedGroups = useAppSelector(state => graphSlice.selectors.selectedGroups(state)); + const meterDataByID = useAppSelector(state => metersSlice.selectors.meterDataByID(state)); + const groupDataByID = useAppSelector(state => groupsSlice.selectors.groupDataByID(state)); if (meterIsFetching || groupIsFetching) { return diff --git a/src/client/app/components/LineChartComponent.tsx b/src/client/app/components/LineChartComponent.tsx index 0e6ff0419..c12e4e88a 100644 --- a/src/client/app/components/LineChartComponent.tsx +++ b/src/client/app/components/LineChartComponent.tsx @@ -10,14 +10,12 @@ import Plot from 'react-plotly.js'; import { Button } from 'reactstrap'; import { TimeInterval } from '../../../common/TimeInterval'; import { graphSlice } from '../reducers/graph'; +import { groupsSlice } from '../reducers/groups'; +import { metersSlice } from '../reducers/meters'; +import { unitsSlice } from '../reducers/units'; import { readingsApi } from '../redux/api/readingsApi'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; -import { - ChartQueryProps, LineReadingApiArgs, - selectGroupDataByID, selectMeterDataByID, - selectMeterState, selectUnitDataById -} from '../redux/selectors/dataSelectors'; -import { selectSelectedGroups, selectSelectedMeters } from '../redux/selectors/uiSelectors'; +import { ChartQueryProps, LineReadingApiArgs } from '../redux/selectors/dataSelectors'; import { DataType } from '../types/Datasources'; import Locales from '../types/locales'; import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; @@ -52,14 +50,14 @@ export default function LineChartComponent(props: ChartQueryProps state.graph.lineGraphRate); const timeInterval = useAppSelector(state => state.graph.timeInterval); - const unitDataByID = useAppSelector(state => selectUnitDataById(state)); + const unitDataByID = useAppSelector(state => unitsSlice.selectors.unitDataById(state)); const selectedAreaNormalization = useAppSelector(state => state.graph.areaNormalization); const selectedAreaUnit = useAppSelector(state => state.graph.selectedAreaUnit); - const selectedMeters = useAppSelector(state => selectSelectedMeters(state)); - const selectedGroups = useAppSelector(state => selectSelectedGroups(state)); - const metersState = useAppSelector(state => selectMeterState(state)); - const meterDataByID = useAppSelector(state => selectMeterDataByID(state)); - const groupDataByID = useAppSelector(state => selectGroupDataByID(state)); + const selectedMeters = useAppSelector(state => graphSlice.selectors.selectedMeters(state)); + const selectedGroups = useAppSelector(state => graphSlice.selectors.selectedGroups(state)); + const metersState = useAppSelector(state => metersSlice.selectors.meterState(state)); + const meterDataByID = useAppSelector(state => metersSlice.selectors.meterDataByID(state)); + const groupDataByID = useAppSelector(state => groupsSlice.selectors.groupDataByID(state)); // Keeps Track of the Slider Values const startTsRef = React.useRef(null); const endTsRef = React.useRef(null); diff --git a/src/client/app/components/UIOptionsComponent.tsx b/src/client/app/components/UIOptionsComponent.tsx index 604f3a7a9..ffa5a2d78 100644 --- a/src/client/app/components/UIOptionsComponent.tsx +++ b/src/client/app/components/UIOptionsComponent.tsx @@ -7,7 +7,6 @@ import ReactTooltip from 'react-tooltip'; import ExportComponent from '../components/ExportComponent'; import ChartLinkContainer from '../containers/ChartLinkContainer'; import { useAppSelector } from '../redux/hooks'; -import { selectChartToRender } from '../redux/selectors/uiSelectors'; import { ChartTypes } from '../types/redux/graph'; import AreaUnitSelectComponent from './AreaUnitSelectComponent'; import BarControlsComponent from './BarControlsComponent'; @@ -19,12 +18,13 @@ import ErrorBarComponent from './ErrorBarComponent'; import GraphicRateMenuComponent from './GraphicRateMenuComponent'; import MapControlsComponent from './MapControlsComponent'; import ThreeDSelectComponent from './ReadingsPerDaySelectComponent'; +import { graphSlice } from '../reducers/graph'; /** * @returns the Ui Control panel */ export default function UIOptionsComponent() { - const chartToRender = useAppSelector(state => selectChartToRender(state)); + const chartToRender = useAppSelector(state => graphSlice.selectors.chartToRender(state)); ReactTooltip.rebuild(); return (
diff --git a/src/client/app/components/groups/GroupsDetailComponent.tsx b/src/client/app/components/groups/GroupsDetailComponent.tsx index b9b9d9e95..258560bc8 100644 --- a/src/client/app/components/groups/GroupsDetailComponent.tsx +++ b/src/client/app/components/groups/GroupsDetailComponent.tsx @@ -9,12 +9,13 @@ import FooterContainer from '../../containers/FooterContainer'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; import { useAppSelector } from '../../redux/hooks'; import { selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; -import { selectUnitDataById, selectVisibleMetersGroupsDataByID } from '../../redux/selectors/dataSelectors'; +import { selectVisibleMetersGroupsDataByID } from '../../redux/selectors/dataSelectors'; import { GroupDefinition } from '../../types/redux/groups'; import { potentialGraphicUnits } from '../../utils/input'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateGroupModalComponent from './CreateGroupModalComponent'; import GroupViewComponent from './GroupViewComponent'; +import { unitsSlice } from '../../reducers/units'; /** * Defines the groups page card view @@ -29,7 +30,7 @@ export default function GroupsDetailComponent() { const { visibleGroups } = useAppSelector(state => selectVisibleMetersGroupsDataByID(state)); // Units state - const unitDataById = useAppSelector(state => selectUnitDataById(state)); + const unitDataById = useAppSelector(state => unitsSlice.selectors.unitDataById(state)); // Possible graphic units to use const possibleGraphicUnits = potentialGraphicUnits(unitDataById); diff --git a/src/client/app/components/meters/MetersDetailComponent.tsx b/src/client/app/components/meters/MetersDetailComponent.tsx index 8cf043595..774c80098 100644 --- a/src/client/app/components/meters/MetersDetailComponent.tsx +++ b/src/client/app/components/meters/MetersDetailComponent.tsx @@ -10,7 +10,7 @@ import FooterContainer from '../../containers/FooterContainer'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; import { useAppSelector } from '../../redux/hooks'; import { selectCurrentUser, selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; -import { selectUnitDataById, selectVisibleMetersGroupsDataByID } from '../../redux/selectors/dataSelectors'; +import { selectVisibleMetersGroupsDataByID } from '../../redux/selectors/dataSelectors'; import '../../styles/card-page.css'; import { MeterData } from '../../types/redux/meters'; import { UnitData, UnitType } from '../../types/redux/units'; @@ -18,6 +18,7 @@ import { noUnitTranslated, potentialGraphicUnits } from '../../utils/input'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateMeterModalComponent from './CreateMeterModalComponent'; import MeterViewComponent from './MeterViewComponent'; +import { unitsSlice } from '../../reducers/units'; /** * Defines the meters page card view @@ -35,7 +36,7 @@ export default function MetersDetailComponent() { const { visibleMeters } = useAppSelector(state => selectVisibleMetersGroupsDataByID(state)); // Units state - const unitDataById = useAppSelector(state => selectUnitDataById(state)); + const unitDataById = useAppSelector(state => unitsSlice.selectors.unitDataById(state)); // TODO? Convert into Selector? // Possible Meter Units to use diff --git a/src/client/app/components/unit/UnitsDetailComponent.tsx b/src/client/app/components/unit/UnitsDetailComponent.tsx index 22bdcdf01..724565f2c 100644 --- a/src/client/app/components/unit/UnitsDetailComponent.tsx +++ b/src/client/app/components/unit/UnitsDetailComponent.tsx @@ -10,11 +10,11 @@ import SpinnerComponent from '../../components/SpinnerComponent'; import FooterContainer from '../../containers/FooterContainer'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; import { useAppSelector } from '../../redux/hooks'; -import { selectUnitDataById } from '../../redux/selectors/dataSelectors'; import { State } from '../../types/redux/state'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateUnitModalComponent from './CreateUnitModalComponent'; import UnitViewComponent from './UnitViewComponent'; +import { unitsSlice } from '../../reducers/units'; /** * Defines the units page card view @@ -25,7 +25,7 @@ export default function UnitsDetailComponent() { const isUpdatingCikAndDBViews = useSelector((state: State) => state.admin.isUpdatingCikAndDBViews); //Units state - const unitDataById = useAppSelector(state => selectUnitDataById(state)); + const unitDataById = useAppSelector(state => unitsSlice.selectors.unitDataById(state)); const titleStyle: React.CSSProperties = { diff --git a/src/client/app/reducers/graph.ts b/src/client/app/reducers/graph.ts index c8a01cf8e..4a82d32f8 100644 --- a/src/client/app/reducers/graph.ts +++ b/src/client/app/reducers/graph.ts @@ -6,11 +6,11 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit'; import * as moment from 'moment'; import { ActionMeta } from 'react-select'; import { TimeInterval } from '../../../common/TimeInterval'; +import { preferencesApi } from '../redux/api/preferencesApi'; import { SelectOption } from '../types/items'; import { ChartTypes, GraphState, LineGraphRate, MeterOrGroup, ReadingInterval } from '../types/redux/graph'; import { ComparePeriod, SortingOrder, calculateCompareTimeInterval } from '../utils/calculateCompare'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; -import { preferencesApi } from '../redux/api/preferencesApi'; const defaultState: GraphState = { selectedMeters: [], @@ -220,5 +220,20 @@ export const graphSlice = createSlice({ state.selectedAreaUnit = action.payload.defaultAreaUnit } }) + }, + // New Feature as of 2.0.0 Beta. + selectors: { + threeDState: state => state.threeD, + barWidthDays: state => state.barDuration, + graphState: state => state, + selectedMeters: state => state.selectedMeters, + selectedGroups: state => state.selectedGroups, + graphTimeInterval: state => state.timeInterval, + graphUnitID: state => state.selectedUnit, + graphAreaNormalization: state => state.areaNormalization, + chartToRender: state => state.chartToRender, + threeDMeterOrGroup: state => state.threeD.meterOrGroup, + threeDMeterOrGroupID: state => state.threeD.meterOrGroupID, + threeDReadingInterval: state => state.threeD.readingInterval } }) diff --git a/src/client/app/reducers/groups.ts b/src/client/app/reducers/groups.ts index 7774c067e..08d30700c 100644 --- a/src/client/app/reducers/groups.ts +++ b/src/client/app/reducers/groups.ts @@ -135,5 +135,10 @@ export const groupsSlice = createSlice({ } }) + }, + selectors: { + groupState: state => state, + groupDataByID: state => state.byGroupID + } }); \ No newline at end of file diff --git a/src/client/app/reducers/maps.ts b/src/client/app/reducers/maps.ts index 9248537df..5c9c64548 100644 --- a/src/client/app/reducers/maps.ts +++ b/src/client/app/reducers/maps.ts @@ -2,10 +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 {MapMetadata, MapsAction, MapState} from '../types/redux/map'; -import {ActionType} from '../types/redux/actions'; +import { MapMetadata, MapsAction, MapState } from '../types/redux/map'; +import { ActionType } from '../types/redux/actions'; import * as _ from 'lodash'; -import {CalibratedPoint} from '../utils/calibration'; +import { CalibratedPoint } from '../utils/calibration'; +import { RootState } from '../store'; const defaultState: MapState = { isLoading: false, @@ -15,7 +16,7 @@ const defaultState: MapState = { editedMaps: {}, submitting: [], newMapCounter: 0, - calibrationSettings: {showGrid: false} + calibrationSettings: { showGrid: false } }; export default function maps(state = defaultState, action: MapsAction) { @@ -117,7 +118,7 @@ export default function maps(state = defaultState, action: MapsAction) { }; case ActionType.ResetCalibration: { editedMaps = state.editedMaps; - const mapToReset = {...editedMaps[action.mapID]}; + const mapToReset = { ...editedMaps[action.mapID] }; delete mapToReset.currentPoint; delete mapToReset.calibrationResult; delete mapToReset.calibrationSet; @@ -159,7 +160,7 @@ export default function maps(state = defaultState, action: MapsAction) { editedMaps = state.editedMaps; if (action.mapID > 0) { submitting.splice(submitting.indexOf(action.mapID)); - byMapID[action.mapID] = {...editedMaps[action.mapID]}; + byMapID[action.mapID] = { ...editedMaps[action.mapID] }; } delete editedMaps[action.mapID]; return { @@ -182,7 +183,7 @@ export default function maps(state = defaultState, action: MapsAction) { case ActionType.UpdateCurrentCartesian: { const newDataPoint: CalibratedPoint = { cartesian: action.currentCartesian, - gps: {longitude: -1, latitude: -1} + gps: { longitude: -1, latitude: -1 } }; return { ...state, @@ -241,3 +242,4 @@ export default function maps(state = defaultState, action: MapsAction) { return state; } } +export const selectMapState = (state: RootState) => state.maps; \ No newline at end of file diff --git a/src/client/app/reducers/meters.ts b/src/client/app/reducers/meters.ts index 983e56549..5a17fa366 100644 --- a/src/client/app/reducers/meters.ts +++ b/src/client/app/reducers/meters.ts @@ -56,5 +56,10 @@ export const metersSlice = createSlice({ state.byMeterID = payload } ) + }, + selectors: { + meterState: state => state, + meterDataByID: state => state.byMeterID + } }); \ No newline at end of file diff --git a/src/client/app/reducers/units.ts b/src/client/app/reducers/units.ts index cfd4dadae..b1004ad85 100644 --- a/src/client/app/reducers/units.ts +++ b/src/client/app/reducers/units.ts @@ -46,5 +46,10 @@ export const unitsSlice = createSlice({ builder.addMatcher(unitsApi.endpoints.getUnitsDetails.matchFulfilled, (state, action) => { state.units = _.keyBy(action.payload, unit => unit.id) } ) + }, + selectors: { + unitsState: state => state, + unitDataById: state => state.units + } }); \ No newline at end of file diff --git a/src/client/app/redux/selectors/dataSelectors.ts b/src/client/app/redux/selectors/dataSelectors.ts index 1539f8d52..b03740155 100644 --- a/src/client/app/redux/selectors/dataSelectors.ts +++ b/src/client/app/redux/selectors/dataSelectors.ts @@ -1,29 +1,18 @@ import { createSelector } from '@reduxjs/toolkit'; import * as _ from 'lodash'; +import { graphSlice } from '../../reducers/graph'; +import { groupsSlice } from '../../reducers/groups'; +import { metersSlice } from '../../reducers/meters'; import { ThreeDReadingApiParams } from '../../redux/api/readingsApi'; -import { RootState } from '../../store'; import { MeterOrGroup } from '../../types/redux/graph'; import { GroupDefinition } from '../../types/redux/groups'; import { MeterData } from '../../types/redux/meters'; import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; import { selectIsLoggedInAsAdmin } from './authSelectors'; - -export const selectMeterDataByID = (state: RootState) => state.meters.byMeterID; -export const selectGroupDataByID = (state: RootState) => state.groups.byGroupID; -export const selectUnitDataById = (state: RootState) => state.units.units; - -export const selectMeterState = (state: RootState) => state.meters; -export const selectGroupState = (state: RootState) => state.groups; -export const selectUnitState = (state: RootState) => state.units; -export const selectMapState = (state: RootState) => state.maps; -export const selectThreeDState = (state: RootState) => state.graph.threeD; -export const selectBarWidthDays = (state: RootState) => state.graph.barDuration; -export const selectGraphState = (state: RootState) => state.graph; - export const selectVisibleMetersGroupsDataByID = createSelector( - selectMeterDataByID, - selectGroupDataByID, + metersSlice.selectors.meterDataByID, + groupsSlice.selectors.groupDataByID, selectIsLoggedInAsAdmin, (meterDataByID, groupDataByID, isAdmin) => { let visibleMeters; @@ -66,7 +55,7 @@ export interface LineReadingApiArgs extends commonArgs { } export interface BarReadingApiArgs extends commonArgs { barWidthDays: number } export const selectChartQueryArgs = createSelector( - selectGraphState, + graphSlice.selectors.graphState, graphState => { const baseMeterArgs: commonArgs = { // Sort the arrays immutably. Sorting the arrays helps with cache hits. @@ -90,8 +79,14 @@ export const selectChartQueryArgs = createSelector( } const bar: ChartQueryArgs = { - meterArgs: { ...baseMeterArgs, barWidthDays: Math.round(graphState.barDuration.asDays()) }, - groupsArgs: { ...baseGroupArgs, barWidthDays: Math.round(graphState.barDuration.asDays()) } + meterArgs: { + ...baseMeterArgs, + barWidthDays: Math.round(graphState.barDuration.asDays()) + }, + groupsArgs: { + ...baseGroupArgs, + barWidthDays: Math.round(graphState.barDuration.asDays()) + } } diff --git a/src/client/app/redux/selectors/threeDSelectors.ts b/src/client/app/redux/selectors/threeDSelectors.ts index 83a569c0a..6e77ccdc6 100644 --- a/src/client/app/redux/selectors/threeDSelectors.ts +++ b/src/client/app/redux/selectors/threeDSelectors.ts @@ -1,20 +1,21 @@ import { createSelector } from '@reduxjs/toolkit'; -import { RootState } from '../../store'; +import { graphSlice } from '../../reducers/graph'; +import { groupsSlice } from '../../reducers/groups'; +import { metersSlice } from '../../reducers/meters'; import { MeterOrGroup } from '../../types/redux/graph'; import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; import { ThreeDReadingApiParams } from '../api/readingsApi'; -import { selectGraphTimeInterval, selectGraphUnitID } from '../selectors/uiSelectors'; -import { selectGroupState, selectMeterState } from './dataSelectors'; // Common Fine Grained selectors -const selectThreeDMeterOrGroupID = (state: RootState) => state.graph.threeD.meterOrGroupID; -const selectThreeDMeterOrGroup = (state: RootState) => state.graph.threeD.meterOrGroup; -export const selectThreeDReadingInterval = (state: RootState) => state.graph.threeD.readingInterval; +const { threeDMeterOrGroup, threeDMeterOrGroupID, threeDReadingInterval } = graphSlice.selectors; +const { graphTimeInterval, graphUnitID } = graphSlice.selectors; +const { meterState } = metersSlice.selectors; +const { groupState } = groupsSlice.selectors; // Memoized Selectors export const selectThreeDComponentInfo = createSelector( - [selectThreeDMeterOrGroupID, selectThreeDMeterOrGroup, selectMeterState, selectGroupState], + [threeDMeterOrGroupID, threeDMeterOrGroup, meterState, groupState], (id, meterOrGroup, meterData, groupData) => { //Default Values let meterOrGroupName = 'Unselected Meter or Group' @@ -44,11 +45,11 @@ export const selectThreeDComponentInfo = createSelector( ) export const selectThreeDQueryArgs = createSelector( - selectThreeDMeterOrGroupID, - selectGraphTimeInterval, - selectGraphUnitID, - selectThreeDReadingInterval, - selectThreeDMeterOrGroup, + threeDMeterOrGroupID, + graphTimeInterval, + graphUnitID, + threeDReadingInterval, + threeDMeterOrGroup, (id, timeInterval, unitID, readingInterval, meterOrGroup) => { return { meterOrGroupID: id, @@ -61,7 +62,7 @@ export const selectThreeDQueryArgs = createSelector( ) export const selectThreeDSkip = createSelector( - selectThreeDMeterOrGroupID, - selectGraphTimeInterval, + threeDMeterOrGroupID, + graphTimeInterval, (id, interval) => !id || !interval.getIsBounded() ) \ 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 483365817..2a72baa65 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -5,13 +5,17 @@ import { createSelector } from '@reduxjs/toolkit'; import * as _ from 'lodash'; import { instanceOfGroupsState, instanceOfMetersState, instanceOfUnitsState } from '../../components/ChartDataSelectComponent'; -import { RootState } from '../../store'; +import { graphSlice } from '../../reducers/graph'; +import { groupsSlice } from '../../reducers/groups'; +import { selectMapState } from '../../reducers/maps'; +import { metersSlice } from '../../reducers/meters'; +import { unitsSlice } from '../../reducers/units'; import { DataType } from '../../types/Datasources'; import { GroupedOption, SelectOption } from '../../types/items'; import { ChartTypes, MeterOrGroup } from '../../types/redux/graph'; import { GroupsState } from '../../types/redux/groups'; import { MetersState } from '../../types/redux/meters'; -import { DisplayableType, UnitData, UnitRepresentType, UnitType, UnitsState } from '../../types/redux/units'; +import { DisplayableType, UnitRepresentType, UnitType, UnitsState } from '../../types/redux/units'; import { CartesianPoint, Dimensions, calculateScaleFromEndpoints, gpsToUserGrid, itemDisplayableOnMap, itemMapInfoOk, normalizeImageDimensions @@ -19,19 +23,19 @@ import { import { metersInGroup, unitsCompatibleWithMeters } from '../../utils/determineCompatibleUnits'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; import { selectCurrentUser } from './authSelectors'; -import { selectGroupState, selectMapState, selectMeterState, selectUnitState } from './dataSelectors'; - - -export const selectSelectedMeters = (state: RootState) => state.graph.selectedMeters; -export const selectSelectedGroups = (state: RootState) => state.graph.selectedGroups; -export const selectGraphTimeInterval = (state: RootState) => state.graph.timeInterval; -export const selectGraphUnitID = (state: RootState) => state.graph.selectedUnit; -export const selectGraphAreaNormalization = (state: RootState) => state.graph.areaNormalization; -export const selectChartToRender = (state: RootState) => state.graph.chartToRender; +// Destruct selectors from Slices (rtk5.0.2Beta) +// Selectors will be used as arguments for the Create Selectors. +// Ensure these selectors always return a stable reference. +const { meterState } = metersSlice.selectors; +const { groupState } = groupsSlice.selectors; +const { graphUnitID, selectedMeters, selectedGroups, chartToRender, graphAreaNormalization } = graphSlice.selectors; +const { unitsState } = unitsSlice.selectors; export const selectVisibleMetersAndGroups = createSelector( - [selectMeterState, selectGroupState, selectCurrentUser], + meterState, + groupState, + selectCurrentUser, (meterState, groupState, currentUser) => { // Holds all meters visible to the user const visibleMeters = new Set(); @@ -65,7 +69,12 @@ export const selectVisibleMetersAndGroups = createSelector( ); export const selectCurrentUnitCompatibility = createSelector( - [selectVisibleMetersAndGroups, selectMeterState, selectGroupState, selectGraphUnitID], + [ + selectVisibleMetersAndGroups, + meterState, + groupState, + graphUnitID + ], (visible, meterState, groupState, graphUnitID) => { // meters and groups that can graph const compatibleMeters = new Set(); @@ -135,13 +144,16 @@ export const selectCurrentUnitCompatibility = createSelector( return { compatibleMeters, incompatibleMeters, compatibleGroups, incompatibleGroups } } ) + export const selectCurrentAreaCompatibility = createSelector( - selectCurrentUnitCompatibility, - selectGraphAreaNormalization, - selectGraphUnitID, - selectMeterState, - selectGroupState, - selectUnitState, + [ + selectCurrentUnitCompatibility, + graphAreaNormalization, + graphUnitID, + meterState, + groupState, + unitsState + ], (currentUnitCompatibility, areaNormalization, unitID, meterState, groupState, unitState) => { // Deep Copy previous selector's values, and update as needed based on current Area Normalization setting const compatibleMeters = new Set(currentUnitCompatibility.compatibleMeters); @@ -185,9 +197,9 @@ export const selectCurrentAreaCompatibility = createSelector( export const selectChartTypeCompatibility = createSelector( selectCurrentAreaCompatibility, - selectChartToRender, - selectMeterState, - selectGroupState, + chartToRender, + meterState, + groupState, selectMapState, (areaCompat, chartToRender, meterState, groupState, mapState) => { // Deep Copy previous selector's values, and update as needed based on current ChartType(s) @@ -272,11 +284,13 @@ export const selectChartTypeCompatibility = createSelector( ) export const selectMeterGroupSelectData = createSelector( - selectChartTypeCompatibility, - selectMeterState, - selectGroupState, - selectSelectedMeters, - selectSelectedGroups, + [ + selectChartTypeCompatibility, + meterState, + groupState, + selectedMeters, + selectedGroups + ], (chartTypeCompatibility, meterState, groupState, selectedMeters, selectedGroups) => { // Destructure Previous Selectors's values const { compatibleMeters, incompatibleMeters, compatibleGroups, incompatibleGroups } = chartTypeCompatibility; @@ -332,19 +346,19 @@ export const selectMeterGroupSelectData = createSelector( * @returns an array of UnitData */ export const selectVisibleUnitOrSuffixState = createSelector( - selectUnitState, + unitsState, selectCurrentUser, (unitState, currentUser) => { let visibleUnitsOrSuffixes; if (currentUser.profile?.role === 'admin') { // User is an admin, allow all units to be seen - visibleUnitsOrSuffixes = _.filter(unitState.units, (o: UnitData) => { + visibleUnitsOrSuffixes = _.filter(unitState.units, o => { return (o.typeOfUnit == UnitType.unit || o.typeOfUnit == UnitType.suffix) && o.displayable != DisplayableType.none; }); } else { // User is not an admin, do not allow for admin units to be seen - visibleUnitsOrSuffixes = _.filter(unitState.units, (o: UnitData) => { + visibleUnitsOrSuffixes = _.filter(unitState.units, o => { return (o.typeOfUnit == UnitType.unit || o.typeOfUnit == UnitType.suffix) && o.displayable == DisplayableType.all; }); } @@ -353,11 +367,13 @@ export const selectVisibleUnitOrSuffixState = createSelector( ) export const selectUnitSelectData = createSelector( - selectUnitState, - selectVisibleUnitOrSuffixState, - selectSelectedMeters, - selectSelectedGroups, - selectGraphAreaNormalization, + [ + unitsState, + selectVisibleUnitOrSuffixState, + selectedMeters, + selectedGroups, + graphAreaNormalization + ], (unitState, visibleUnitsOrSuffixes, selectedMeters, selectedGroups, areaNormalization) => { // Holds all units that are compatible with selected meters/groups const compatibleUnits = new Set(); @@ -520,7 +536,7 @@ export function getSelectOptionsByItem(compatibleItems: Set, incompatibl } export const selectDateRangeInterval = createSelector( - selectGraphTimeInterval, + graphSlice.selectors.graphTimeInterval, timeInterval => { return timeInterval } From 56f1aa7dab4f3fdf5a895988c0c358b6973ff984 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Mon, 16 Oct 2023 00:14:57 +0000 Subject: [PATCH 025/131] Query Updates, and Data Selectors -- Redux 5.0.3 Beta version bump -- Selectors Refactor -- Custom Cache Behavior for network efficiency --- package-lock.json | 1211 +++++++++-------- package.json | 2 +- src/client/app/actions/graph.ts | 24 +- .../app/components/BarChartComponent.tsx | 217 ++- .../app/components/DateRangeComponent.tsx | 79 +- src/client/app/components/ExportComponent.tsx | 4 +- .../app/components/LineChartComponent.tsx | 237 ++-- .../MeterAndGroupSelectComponent.tsx | 24 +- .../app/components/RouteComponentWIP.tsx | 8 +- src/client/app/components/ThreeDComponent.tsx | 2 +- .../app/components/UIOptionsComponent.tsx | 5 +- .../groups/GroupsDetailComponent.tsx | 4 +- .../meters/MetersDetailComponent.tsx | 7 +- .../components/unit/UnitsDetailComponent.tsx | 4 +- .../app/containers/BarChartContainer.ts | 2 +- .../app/containers/ChartLinkContainer.ts | 4 +- .../app/containers/DashboardContainer.ts | 2 +- .../app/containers/LineChartContainer.ts | 2 +- .../app/containers/MapChartContainer.ts | 2 +- src/client/app/reducers/currentUser.ts | 12 +- src/client/app/reducers/graph.ts | 63 +- src/client/app/reducers/groups.ts | 9 +- src/client/app/reducers/meters.ts | 8 +- src/client/app/reducers/units.ts | 9 +- src/client/app/redux/api/readingsApi.ts | 100 +- .../app/redux/selectors/authSelectors.ts | 4 +- .../app/redux/selectors/dataSelectors.ts | 94 +- .../app/redux/selectors/threeDSelectors.ts | 32 +- src/client/app/redux/selectors/uiSelectors.ts | 85 +- src/client/app/types/redux/graph.ts | 8 +- 30 files changed, 1213 insertions(+), 1051 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1a3c17c53..06cdd8293 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MPL-2.0", "dependencies": { - "@reduxjs/toolkit": "~2.0.0-beta.2", + "@reduxjs/toolkit": "~2.0.0-beta.3", "@wojtekmaj/react-daterange-picker": "~5.2.0", "axios": "~0.24.0", "bcryptjs": "~2.4.3", @@ -111,11 +111,11 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.10.tgz", - "integrity": "sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dependencies": { - "@babel/highlight": "^7.22.10", + "@babel/highlight": "^7.22.13", "chalk": "^2.4.2" }, "engines": { @@ -123,9 +123,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", - "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz", + "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==", "dev": true, "engines": { "node": ">=6.9.0" @@ -162,12 +162,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.10.tgz", - "integrity": "sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "dependencies": { - "@babel/types": "^7.22.10", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -189,25 +189,25 @@ } }, "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.10.tgz", - "integrity": "sha512-Av0qubwDQxC56DoUReVDeLfMEjYYSN1nZrTUrWkXd7hpU73ymRANkbuDm3yni9npkn+RXy9nNbEJZEzXr7xrfQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", "dev": true, "dependencies": { - "@babel/types": "^7.22.10" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz", - "integrity": "sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", + "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", "dev": true, "dependencies": { "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", "browserslist": "^4.21.9", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -217,15 +217,15 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.10.tgz", - "integrity": "sha512-5IBb77txKYQPpOEdUdIhBx8VrZyDCQ+H82H0+5dX1TmuscP5vJKEE3cKurjtIw/vFwzbVH48VweE78kVDBrqjA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz", + "integrity": "sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-replace-supers": "^7.22.9", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", @@ -240,9 +240,9 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz", - "integrity": "sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", @@ -274,22 +274,22 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -308,39 +308,39 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", - "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", + "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", "dev": true, "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", - "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", - "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz", + "integrity": "sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", "@babel/helper-simple-access": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.5" + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -371,14 +371,14 @@ } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.9.tgz", - "integrity": "sha512-8WWC4oR4Px+tr+Fp0X3RHDVfINGpF3ad1HIbrc8A77epiR6eMMc6jsgozkzT2uDiOOdoS9cLIQ+XD2XvI2WSmQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-wrap-function": "^7.22.9" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -388,13 +388,13 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz", - "integrity": "sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", + "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", "dev": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.22.15", "@babel/helper-optimise-call-expression": "^7.22.5" }, "engines": { @@ -449,56 +449,56 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", - "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", + "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.10.tgz", - "integrity": "sha512-OnMhjWjuGYtdoO3FmsEFWvBStBAe2QOgwOLsLNDjN+aaiMD8InJk1/O3HSD8lkqTjCgg5YI34Tz15KNNA3p+nQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz", + "integrity": "sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==", "dev": true, "dependencies": { "@babel/helper-function-name": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.10" + "@babel/template": "^7.22.15", + "@babel/types": "^7.22.19" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.10.tgz", - "integrity": "sha512-a41J4NW8HyZa1I1vAndrraTlPZ/eZoga2ZgS7fEr0tZJGVU4xqdE80CEm0CcNjha5EZ8fTBYLKHF0kqDUuAwQw==", + "version": "7.23.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.1.tgz", + "integrity": "sha512-chNpneuK18yW5Oxsr+t553UZzzAs3aZnFm4bxhebsNTeshrC95yA7l5yl7GBAG+JG1rF0F7zzD2EixK9mWSDoA==", "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.10", - "@babel/types": "^7.22.10" + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.23.0", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.10.tgz", - "integrity": "sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, @@ -507,9 +507,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.10.tgz", - "integrity": "sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -519,9 +519,9 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz", - "integrity": "sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz", + "integrity": "sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -534,14 +534,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz", - "integrity": "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz", + "integrity": "sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.5" + "@babel/plugin-transform-optional-chaining": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -554,6 +554,7 @@ "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead.", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.18.9", @@ -572,6 +573,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead.", "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.18.6", @@ -588,6 +590,7 @@ "version": "7.21.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz", "integrity": "sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-static-block instead.", "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.21.0", @@ -605,6 +608,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-dynamic-import instead.", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", @@ -621,6 +625,7 @@ "version": "7.18.9", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-export-namespace-from instead.", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.18.9", @@ -637,6 +642,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-json-strings instead.", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", @@ -653,6 +659,7 @@ "version": "7.20.7", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead.", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.20.2", @@ -669,6 +676,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead.", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", @@ -685,6 +693,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead.", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", @@ -701,6 +710,7 @@ "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.16.7.tgz", "integrity": "sha512-3O0Y4+dw94HA86qSg9IHfyPktgR7q3gpNVAeiKQd+8jBKFaU5NQS1Yatgo4wY+UFNuLjvxcSmzcsHqrhgTyBUA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead.", "dev": true, "dependencies": { "@babel/compat-data": "^7.16.4", @@ -720,6 +730,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead.", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", @@ -736,6 +747,7 @@ "version": "7.21.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead.", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.20.2", @@ -753,6 +765,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-methods instead.", "dev": true, "dependencies": { "@babel/helper-create-class-features-plugin": "^7.18.6", @@ -769,6 +782,7 @@ "version": "7.21.11", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.18.6", @@ -787,6 +801,7 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-unicode-property-regex instead.", "dev": true, "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", @@ -1039,9 +1054,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.10.tgz", - "integrity": "sha512-1+kVpGAOOI1Albt6Vse7c8pHzcZQdQKW+wJH+g8mCaszOdDVwRXa/slHPqIw+oJAJANTKDMuM2cBdV0Dg618Vg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.0.tgz", + "integrity": "sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1054,18 +1069,18 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.6.tgz", - "integrity": "sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz", + "integrity": "sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-compilation-targets": "^7.22.15", "@babel/helper-environment-visitor": "^7.22.5", "@babel/helper-function-name": "^7.22.5", "@babel/helper-optimise-call-expression": "^7.22.5", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.9", "@babel/helper-split-export-declaration": "^7.22.6", "globals": "^11.1.0" }, @@ -1093,9 +1108,9 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.10.tgz", - "integrity": "sha512-dPJrL0VOyxqLM9sritNbMSGx/teueHF/htMKrPT7DNxccXxRDPYqlgPFFdr8u+F+qUZOkZoXue/6rL5O5GduEw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.0.tgz", + "integrity": "sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1155,9 +1170,9 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz", - "integrity": "sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz", + "integrity": "sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1217,12 +1232,12 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", - "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.0.tgz", + "integrity": "sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5" }, "engines": { @@ -1233,12 +1248,12 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz", - "integrity": "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz", + "integrity": "sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5", "@babel/helper-simple-access": "^7.22.5" }, @@ -1250,15 +1265,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz", - "integrity": "sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.0.tgz", + "integrity": "sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg==", "dev": true, "dependencies": { "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.0", "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5" + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { "node": ">=6.9.0" @@ -1331,9 +1346,9 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.10.tgz", - "integrity": "sha512-MMkQqZAZ+MGj+jGTG3OTuhKeBpNcO+0oCEbrGNEaOmiEn+1MzRyQlYsruGiU8RTK3zV6XwrVJTmwiDOyYK6J9g==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.0.tgz", + "integrity": "sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", @@ -1348,9 +1363,9 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz", - "integrity": "sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz", + "integrity": "sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" @@ -1393,16 +1408,16 @@ } }, "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.5.tgz", - "integrity": "sha512-rog5gZaVbUip5iWDMTYbVM15XQq+RkUKhET/IHR6oizR+JEoN6CAfTTuHcK4vwUyzca30qqHqEpzBOnaRMWYMA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.15.tgz", + "integrity": "sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", "@babel/helper-plugin-utils": "^7.22.5", "@babel/plugin-syntax-jsx": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" @@ -1685,15 +1700,15 @@ } }, "node_modules/@babel/preset-react": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.22.5.tgz", - "integrity": "sha512-M+Is3WikOpEJHgR385HbuCITPTaPRaNkibTEa9oiofmJvIsrceb4yp9RL9Kb+TE8LznmeyZqpP+Lopwcx59xPQ==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.22.15.tgz", + "integrity": "sha512-Csy1IJ2uEh/PecCBXXoZGAZBeCATTuePzCSB7dLYWS0vOEj6CNpjxIhW4duWwZodBNueH7QO14WbGn8YyeuN9w==", "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", "@babel/plugin-transform-react-display-name": "^7.22.5", - "@babel/plugin-transform-react-jsx": "^7.22.5", + "@babel/plugin-transform-react-jsx": "^7.22.15", "@babel/plugin-transform-react-jsx-development": "^7.22.5", "@babel/plugin-transform-react-pure-annotations": "^7.22.5" }, @@ -1711,9 +1726,9 @@ "dev": true }, "node_modules/@babel/runtime": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz", - "integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==", + "version": "7.23.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", + "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -1722,33 +1737,33 @@ } }, "node_modules/@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.10.tgz", - "integrity": "sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.0.tgz", + "integrity": "sha512-t/QaEvyIoIkwzpiZ7aoSKK8kObQYeF7T2v+dazAYCb8SXtp58zEVkWW7zAnju8FNKNdr4ScAOEDmMItbyOmEYw==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.10", - "@babel/generator": "^7.22.10", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.10", - "@babel/types": "^7.22.10", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -1757,12 +1772,12 @@ } }, "node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dependencies": { "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -1933,9 +1948,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", - "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", + "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -1965,9 +1980,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.21.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", - "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -1989,26 +2004,26 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.4.1.tgz", - "integrity": "sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.0.tgz", + "integrity": "sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==", "dependencies": { - "@floating-ui/utils": "^0.1.1" + "@floating-ui/utils": "^0.1.3" } }, "node_modules/@floating-ui/dom": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.1.tgz", - "integrity": "sha512-KwvVcPSXg6mQygvA1TjbN/gh///36kKtllIF8SUm0qpFj8+rvYrpvlYdL1JoA71SHpDqgSSdGOSoQ0Mp3uY5aw==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.3.tgz", + "integrity": "sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==", "dependencies": { - "@floating-ui/core": "^1.4.1", - "@floating-ui/utils": "^0.1.1" + "@floating-ui/core": "^1.4.2", + "@floating-ui/utils": "^0.1.3" } }, "node_modules/@floating-ui/utils": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.1.tgz", - "integrity": "sha512-m0G6wlnhm/AX0H12IOWtK8gASEMffnX08RtKkCgTdHb9JpHKGloI7icFfLg9ZmQeavcvR0PKmzxClyuFPSjKWw==" + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", + "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" }, "node_modules/@formatjs/ecma402-abstract": { "version": "1.14.3", @@ -2050,16 +2065,16 @@ } }, "node_modules/@formatjs/intl": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.9.0.tgz", - "integrity": "sha512-Ym0trUoC/VO6wQu4YHa0H1VR2tEixFRmwZgADkDLm7nD+vv1Ob+/88mUAoT0pwvirFqYKgUKEwp1tFepqyqvVA==", + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.9.3.tgz", + "integrity": "sha512-hclPdyCF1zk2XmhgdXfl5Sd30QEdRBnIijH7Vc1AWz2K0/saVRrxuL3UYn+m3xEyfOa4yDbTWVbmXDL0XEzlsQ==", "dependencies": { - "@formatjs/ecma402-abstract": "1.17.0", + "@formatjs/ecma402-abstract": "1.17.2", "@formatjs/fast-memoize": "2.2.0", - "@formatjs/icu-messageformat-parser": "2.6.0", - "@formatjs/intl-displaynames": "6.5.0", - "@formatjs/intl-listformat": "7.4.0", - "intl-messageformat": "10.5.0", + "@formatjs/icu-messageformat-parser": "2.6.2", + "@formatjs/intl-displaynames": "6.5.2", + "@formatjs/intl-listformat": "7.4.2", + "intl-messageformat": "10.5.3", "tslib": "^2.4.0" }, "peerDependencies": { @@ -2072,55 +2087,55 @@ } }, "node_modules/@formatjs/intl-displaynames": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-6.5.0.tgz", - "integrity": "sha512-sg/nR8ILEdUl+2sWu6jc1nQ5s04yucGlH1RVfatW8TSJ5uG3Yy3vgigi8NNC/BuhcncUNPWqSpTCSI1hA+rhiw==", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-6.5.2.tgz", + "integrity": "sha512-uC2VBlz+WydGTDDpJwMTQuPH3CUpTricr91WH1QMfz5oEHg2sB7mUERcZONE/lu8MOe1jREIx4vBciZEVTqkmA==", "dependencies": { - "@formatjs/ecma402-abstract": "1.17.0", - "@formatjs/intl-localematcher": "0.4.0", + "@formatjs/ecma402-abstract": "1.17.2", + "@formatjs/intl-localematcher": "0.4.2", "tslib": "^2.4.0" } }, "node_modules/@formatjs/intl-displaynames/node_modules/@formatjs/ecma402-abstract": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.0.tgz", - "integrity": "sha512-6ueQTeJZtwKjmh23bdkq/DMqH4l4bmfvtQH98blOSbiXv/OUiyijSW6jU22IT8BNM1ujCaEvJfTtyCYVH38EMQ==", + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.2.tgz", + "integrity": "sha512-k2mTh0m+IV1HRdU0xXM617tSQTi53tVR2muvYOsBeYcUgEAyxV1FOC7Qj279th3fBVQ+Dj6muvNJZcHSPNdbKg==", "dependencies": { - "@formatjs/intl-localematcher": "0.4.0", + "@formatjs/intl-localematcher": "0.4.2", "tslib": "^2.4.0" } }, "node_modules/@formatjs/intl-displaynames/node_modules/@formatjs/intl-localematcher": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.0.tgz", - "integrity": "sha512-bRTd+rKomvfdS4QDlVJ6TA/Jx1F2h/TBVO5LjvhQ7QPPHp19oPNMIum7W2CMEReq/zPxpmCeB31F9+5gl/qtvw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.2.tgz", + "integrity": "sha512-BGdtJFmaNJy5An/Zan4OId/yR9Ih1OojFjcduX/xOvq798OgWSyDtd6Qd5jqJXwJs1ipe4Fxu9+cshic5Ox2tA==", "dependencies": { "tslib": "^2.4.0" } }, "node_modules/@formatjs/intl-listformat": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.4.0.tgz", - "integrity": "sha512-ifupb+balZUAF/Oh3QyGRqPRWGSKwWoMPR0cYZEG7r61SimD+m38oFQqVx/3Fp7LfQFF11m7IS+MlxOo2sKINA==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.4.2.tgz", + "integrity": "sha512-+6bSVudEQkf12Hh7kuKt8Xv/MyFlqdwA4V4NLnTZW8uYdF9RxlOELDD0rPaOc2++TMKIzI5o6XXwHPvpL6VrPA==", "dependencies": { - "@formatjs/ecma402-abstract": "1.17.0", - "@formatjs/intl-localematcher": "0.4.0", + "@formatjs/ecma402-abstract": "1.17.2", + "@formatjs/intl-localematcher": "0.4.2", "tslib": "^2.4.0" } }, "node_modules/@formatjs/intl-listformat/node_modules/@formatjs/ecma402-abstract": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.0.tgz", - "integrity": "sha512-6ueQTeJZtwKjmh23bdkq/DMqH4l4bmfvtQH98blOSbiXv/OUiyijSW6jU22IT8BNM1ujCaEvJfTtyCYVH38EMQ==", + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.2.tgz", + "integrity": "sha512-k2mTh0m+IV1HRdU0xXM617tSQTi53tVR2muvYOsBeYcUgEAyxV1FOC7Qj279th3fBVQ+Dj6muvNJZcHSPNdbKg==", "dependencies": { - "@formatjs/intl-localematcher": "0.4.0", + "@formatjs/intl-localematcher": "0.4.2", "tslib": "^2.4.0" } }, "node_modules/@formatjs/intl-listformat/node_modules/@formatjs/intl-localematcher": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.0.tgz", - "integrity": "sha512-bRTd+rKomvfdS4QDlVJ6TA/Jx1F2h/TBVO5LjvhQ7QPPHp19oPNMIum7W2CMEReq/zPxpmCeB31F9+5gl/qtvw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.2.tgz", + "integrity": "sha512-BGdtJFmaNJy5An/Zan4OId/yR9Ih1OojFjcduX/xOvq798OgWSyDtd6Qd5jqJXwJs1ipe4Fxu9+cshic5Ox2tA==", "dependencies": { "tslib": "^2.4.0" } @@ -2135,37 +2150,37 @@ } }, "node_modules/@formatjs/intl/node_modules/@formatjs/ecma402-abstract": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.0.tgz", - "integrity": "sha512-6ueQTeJZtwKjmh23bdkq/DMqH4l4bmfvtQH98blOSbiXv/OUiyijSW6jU22IT8BNM1ujCaEvJfTtyCYVH38EMQ==", + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.2.tgz", + "integrity": "sha512-k2mTh0m+IV1HRdU0xXM617tSQTi53tVR2muvYOsBeYcUgEAyxV1FOC7Qj279th3fBVQ+Dj6muvNJZcHSPNdbKg==", "dependencies": { - "@formatjs/intl-localematcher": "0.4.0", + "@formatjs/intl-localematcher": "0.4.2", "tslib": "^2.4.0" } }, "node_modules/@formatjs/intl/node_modules/@formatjs/icu-messageformat-parser": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.6.0.tgz", - "integrity": "sha512-yT6at0qc0DANw9qM/TU8RZaCtfDXtj4pZM/IC2WnVU80yAcliS3KVDiuUt4jSQAeFL9JS5bc2hARnFmjPdA6qw==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.6.2.tgz", + "integrity": "sha512-nF/Iww7sc5h+1MBCDRm68qpHTCG4xvGzYs/x9HFcDETSGScaJ1Fcadk5U/NXjXeCtzD+DhN4BAwKFVclHfKMdA==", "dependencies": { - "@formatjs/ecma402-abstract": "1.17.0", - "@formatjs/icu-skeleton-parser": "1.6.0", + "@formatjs/ecma402-abstract": "1.17.2", + "@formatjs/icu-skeleton-parser": "1.6.2", "tslib": "^2.4.0" } }, "node_modules/@formatjs/intl/node_modules/@formatjs/icu-skeleton-parser": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.6.0.tgz", - "integrity": "sha512-eMmxNpoX/J1IPUjPGSZwo0Wh+7CEvdEMddP2Jxg1gQJXfGfht/FdW2D5XDFj3VMbOTUQlDIdZJY7uC6O6gjPoA==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.6.2.tgz", + "integrity": "sha512-VtB9Slo4ZL6QgtDFJ8Injvscf0xiDd4bIV93SOJTBjUF4xe2nAWOoSjLEtqIG+hlIs1sNrVKAaFo3nuTI4r5ZA==", "dependencies": { - "@formatjs/ecma402-abstract": "1.17.0", + "@formatjs/ecma402-abstract": "1.17.2", "tslib": "^2.4.0" } }, "node_modules/@formatjs/intl/node_modules/@formatjs/intl-localematcher": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.0.tgz", - "integrity": "sha512-bRTd+rKomvfdS4QDlVJ6TA/Jx1F2h/TBVO5LjvhQ7QPPHp19oPNMIum7W2CMEReq/zPxpmCeB31F9+5gl/qtvw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.2.tgz", + "integrity": "sha512-BGdtJFmaNJy5An/Zan4OId/yR9Ih1OojFjcduX/xOvq798OgWSyDtd6Qd5jqJXwJs1ipe4Fxu9+cshic5Ox2tA==", "dependencies": { "tslib": "^2.4.0" } @@ -2283,9 +2298,9 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", - "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", + "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", @@ -2538,9 +2553,9 @@ } }, "node_modules/@reduxjs/toolkit": { - "version": "2.0.0-beta.2", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.0.0-beta.2.tgz", - "integrity": "sha512-LVfySlJ5UUFWM6jBD869CAK568f0iF8sI0opzcFn1KFNhn7sYQ9s+MgIc2vUCCGsrDr3fqp9QYFnmtDofeDN1A==", + "version": "2.0.0-beta.3", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.0.0-beta.3.tgz", + "integrity": "sha512-JUINQUveScGU29Z2rCdfh6REVZDJksMn7/L5bGlhjQ442/Yq9JuMc3n5rjjnwcpSYnwgCireKC0dEpS1i+5Q6w==", "dependencies": { "immer": "^10.0.2", "redux": "^5.0.0-beta.0", @@ -2690,9 +2705,9 @@ } }, "node_modules/@types/babel__core": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.1.tgz", - "integrity": "sha512-aACu/U/omhdk15O4Nfb+fHgH/z3QsfQzpnvRZhYhThms83ZnAOZz7zZAWO7mn2yyNQaA4xTO8GLK3uqFU4bYYw==", + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.2.tgz", + "integrity": "sha512-pNpr1T1xLUc2l3xJKuPtsEky3ybxN3m4fJkknfIpTCTfIZCDW57oAg+EfCgIIp2rvCe0Wn++/FfodDS4YXxBwA==", "dev": true, "dependencies": { "@babel/parser": "^7.20.7", @@ -2703,27 +2718,27 @@ } }, "node_modules/@types/babel__generator": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", - "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.5.tgz", + "integrity": "sha512-h9yIuWbJKdOPLJTbmSpPzkF67e659PbQDba7ifWm5BJ8xTv+sDmS7rFmywkWOvXedGTivCdeGSIIX8WLcRTz8w==", "dev": true, "dependencies": { "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__helper-plugin-utils": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/@types/babel__helper-plugin-utils/-/babel__helper-plugin-utils-7.10.0.tgz", - "integrity": "sha512-60YtHzhQ9HAkToHVV+TB4VLzBn9lrfgrsOjiJMtbv/c1jPdekBxaByd6DMsGBzROXWoIL6U3lEFvvbu69RkUoA==", + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@types/babel__helper-plugin-utils/-/babel__helper-plugin-utils-7.10.1.tgz", + "integrity": "sha512-6RaT7i6r2rT6ouIDZ2Cd6dPkq4wn1F8pLyDO+7wPVsL1dodvORiZORImaD6j9FBcHjPGuERE0hhtwkuPNXsO0A==", "dev": true, "dependencies": { "@types/babel__core": "*" } }, "node_modules/@types/babel__template": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", - "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.2.tgz", + "integrity": "sha512-/AVzPICMhMOMYoSx9MoKpGDKdBRsIXMNByh1PXSZoa+v6ZoLa8xxtsT/uLQ/NJm0XVAWl/BvId4MlDeXJaeIZQ==", "dev": true, "dependencies": { "@babel/parser": "^7.1.0", @@ -2731,18 +2746,18 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.1.tgz", - "integrity": "sha512-MitHFXnhtgwsGZWtT68URpOvLN4EREih1u3QtQiN4VdAxWKRVvGCSvw/Qth0M0Qq3pJpnGOu5JaM/ydK7OGbqg==", + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.2.tgz", + "integrity": "sha512-ojlGK1Hsfce93J0+kn3H5R73elidKUaZonirN33GSmgTUMpzI/MIFfSpF3haANe3G1bEBS9/9/QEqwTzwqFsKw==", "dev": true, "dependencies": { "@babel/types": "^7.20.7" } }, "node_modules/@types/chai": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", - "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-/k+vesl92vMvMygmQrFe9Aimxi6oQXFUX9mA5HanTrKUSAMoLauSi6PNFOdRw0oeqilaW600GNx2vSaT2f8aIQ==", "dev": true }, "node_modules/@types/cookiejar": { @@ -2752,9 +2767,9 @@ "dev": true }, "node_modules/@types/eslint": { - "version": "8.44.2", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.2.tgz", - "integrity": "sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==", + "version": "8.44.3", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.3.tgz", + "integrity": "sha512-iM/WfkwAhwmPff3wZuPLYiHX18HI24jU8k1ZSH7P8FHwxTjZ2P6CoX2wnF43oprR+YXJM6UUxATkNvyv/JHd+g==", "dev": true, "dependencies": { "@types/estree": "*", @@ -2762,9 +2777,9 @@ } }, "node_modules/@types/eslint-scope": { - "version": "3.7.4", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", - "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "version": "3.7.5", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.5.tgz", + "integrity": "sha512-JNvhIEyxVW6EoMIFIvj93ZOywYFatlpu9deeH6eSx6PE3WHYvHaQtmHmQeNw7aA81bYGBPPQqdtBm6b1SsQMmA==", "dev": true, "dependencies": { "@types/eslint": "*", @@ -2784,18 +2799,18 @@ "dev": true }, "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", - "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-YIQtIg4PKr7ZyqNPZObpxfHsHEmuB8dXCxd6qVcGuQVDK2bpsF7bYNnBJ4Nn7giuACZg+WewExgrtAJ3XnA4Xw==", "dependencies": { "@types/react": "*", "hoist-non-react-statics": "^3.3.0" } }, "node_modules/@types/json-schema": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", - "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", + "version": "7.0.13", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", + "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", "dev": true }, "node_modules/@types/json-stable-stringify": { @@ -2805,9 +2820,9 @@ "dev": true }, "node_modules/@types/lodash": { - "version": "4.14.197", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.197.tgz", - "integrity": "sha512-BMVOiWs0uNxHVlHBgzTIqJYmj+PgCo4euloGF+5m4okL3rEYzM2EEv78mw8zWSMM57dM7kVIgJ2QDvwHSoCI5g==" + "version": "4.14.199", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.199.tgz", + "integrity": "sha512-Vrjz5N5Ia4SEzWWgIVwnHNEnb1UE1XMkvY5DGXrAeOGE9imk0hgTHh5GyDjLDJi9OTCn9oo9dXH1uToK1VRfrg==" }, "node_modules/@types/lodash.memoize": { "version": "4.1.7", @@ -2818,9 +2833,9 @@ } }, "node_modules/@types/node": { - "version": "20.5.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.6.tgz", - "integrity": "sha512-Gi5wRGPbbyOTX+4Y2iULQ27oUPrefaB0PxGQJnfyWN3kvEDGM3mIB5M/gQLmitZf7A9FmLeaqxD3L1CXpm3VKQ==", + "version": "20.5.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.9.tgz", + "integrity": "sha512-PcGNd//40kHAS3sTlzKB9C9XL4K0sTup8nbG5lC14kzEteTNuAFh9u5nA0o5TWnSG2r/JNPRXFVcHJIIeRlmqQ==", "dev": true }, "node_modules/@types/parse-json": { @@ -2829,15 +2844,15 @@ "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" }, "node_modules/@types/plotly.js": { - "version": "2.12.26", - "resolved": "https://registry.npmjs.org/@types/plotly.js/-/plotly.js-2.12.26.tgz", - "integrity": "sha512-vP1iaVL4HHYSbugv49pwtLL6D9CSqOnQLjiRRdRYjVMEDbjIWhMgxc49BJAxSUShupiJHDp35e0WJS9SwIB2WA==", + "version": "2.12.27", + "resolved": "https://registry.npmjs.org/@types/plotly.js/-/plotly.js-2.12.27.tgz", + "integrity": "sha512-Ah7XuePFNxu2XAHG79GeKN/Ky8dZ0k6hzy49da6AeZFrTqO5wDbtJovp3co3C+iRitp8tA6rIxkltiJ3cjsQWw==", "dev": true }, "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + "version": "15.7.8", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.8.tgz", + "integrity": "sha512-kMpQpfZKSCBqltAJwskgePRaYRFukDkm1oItcAbC3gNELR20XIBcN9VRgg4+m8DKsTfkWeA4m4Imp4DDuWy7FQ==" }, "node_modules/@types/rc-slider": { "version": "8.2.5", @@ -2850,18 +2865,18 @@ } }, "node_modules/@types/rc-tooltip": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/rc-tooltip/-/rc-tooltip-3.7.7.tgz", - "integrity": "sha512-oTcXDJ9hSug+8MZgotVcfXlPmw5K0XiLwuGKkL7lsf12jGLsYAGez3KIRxuxR0icfZkmNN4JQLsScbrBQnJlkg==", + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/@types/rc-tooltip/-/rc-tooltip-3.7.8.tgz", + "integrity": "sha512-s+isieYit4ciyf/LcrO+zxFBGusmmx6FMHqwrKNo7yqf7sXxM0CNj8RuQIzFjWVcjHOrJxH46HPih+dxYbaGMQ==", "dev": true, "dependencies": { "@types/react": "*" } }, "node_modules/@types/react": { - "version": "18.2.20", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.20.tgz", - "integrity": "sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw==", + "version": "18.2.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.27.tgz", + "integrity": "sha512-Wfv7B7FZiR2r3MIqbAlXoY1+tXm4bOqfz4oRr+nyXdBqapDBZ0l/IGcSlAfvxIHEEJjkPU0MYAc/BlFPOcrgLw==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -2869,27 +2884,27 @@ } }, "node_modules/@types/react-dom": { - "version": "18.2.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz", - "integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==", + "version": "18.2.12", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.12.tgz", + "integrity": "sha512-QWZuiA/7J/hPIGocXreCRbx7wyoeet9ooxfbSA+zbIWqyQEE7GMtRn4A37BdYyksnN+/NDnWgfxZH9UVGDw1hg==", "devOptional": true, "dependencies": { "@types/react": "*" } }, "node_modules/@types/react-notification-system": { - "version": "0.2.42", - "resolved": "https://registry.npmjs.org/@types/react-notification-system/-/react-notification-system-0.2.42.tgz", - "integrity": "sha512-9zL1HJNluY9wrjQ4rXSscLRtrir3k8P/4rV0r4xElPu4Nh07HgEjzd6x1nKrN+72KVXDBTc0JaUV7QeD7yx/1g==", + "version": "0.2.43", + "resolved": "https://registry.npmjs.org/@types/react-notification-system/-/react-notification-system-0.2.43.tgz", + "integrity": "sha512-eGz/6A+VS4L7Ka+mqWZiRAsfX4ghEKeXWhzutyjml53A9UgMHGn4Sj1bSGNaUhsPkrgYZQ4nN+nNoTxqv+WJcA==", "dev": true, "dependencies": { "@types/react": "*" } }, "node_modules/@types/react-plotly.js": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@types/react-plotly.js/-/react-plotly.js-2.6.0.tgz", - "integrity": "sha512-nJJ57U0/CNDAO+F3dpnMgM8PtjLE/O1I3O6gq4+5Q13uKqrPnHGYOttfdzQJ4D7KYgF609miVzEYakUS2zds8w==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@types/react-plotly.js/-/react-plotly.js-2.6.1.tgz", + "integrity": "sha512-vFJZRCC2Pav0NdrFm0grPMm9+67ejGZZglDBWqo+J6VFbB4CAatjoNiowfardznuujaaoDNoZ4MSCFwYyVk4aA==", "dev": true, "dependencies": { "@types/plotly.js": "*", @@ -2897,9 +2912,9 @@ } }, "node_modules/@types/react-redux": { - "version": "7.1.25", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz", - "integrity": "sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg==", + "version": "7.1.27", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.27.tgz", + "integrity": "sha512-xj7d9z32p1K/eBmO+OEy+qfaWXtcPlN8f1Xk3Ne0p/ZRQ867RI5bQ/bpBtxbqU1AHNhKJSgGvld/P2myU2uYkg==", "dev": true, "dependencies": { "@types/hoist-non-react-statics": "^3.3.0", @@ -2930,22 +2945,22 @@ } }, "node_modules/@types/react-transition-group": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz", - "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==", + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.7.tgz", + "integrity": "sha512-ICCyBl5mvyqYp8Qeq9B5G/fyBSRC0zx3XM3sCC6KkcMsNeAHqXBKkmat4GqdJET5jtYUpZXrxI5flve5qhi2Eg==", "dependencies": { "@types/react": "*" } }, "node_modules/@types/scheduler": { - "version": "0.16.3", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz", - "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==" + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.4.tgz", + "integrity": "sha512-2L9ifAGl7wmXwP4v3pN4p2FLhD0O1qsJpvKmNin5VA8+UvNVb447UDaAEV6UdrkA+m/Xs58U1RFps44x6TFsVQ==" }, "node_modules/@types/semver": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", - "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==", "dev": true }, "node_modules/@types/superagent": { @@ -3448,23 +3463,23 @@ } }, "node_modules/@wojtekmaj/date-utils": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-1.4.1.tgz", - "integrity": "sha512-Fjs0KJz0//0AmlJVFx9AQmWpmxOTw4foDo4DKoswWVVjHsna4rdu+fXwid5YHNgzv/wHi9AkZCRPmHWsf890lg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@wojtekmaj/date-utils/-/date-utils-1.5.0.tgz", + "integrity": "sha512-0mq88lCND6QiffnSDWp+TbOxzJSwy2V/3XN+HwWZ7S2n19QAgR5dy5hRVhlECXvQIq2r+VcblBu+S9V+yMcxXw==", "funding": { "url": "https://github.com/wojtekmaj/date-utils?sponsor=1" } }, "node_modules/@wojtekmaj/react-daterange-picker": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@wojtekmaj/react-daterange-picker/-/react-daterange-picker-5.2.0.tgz", - "integrity": "sha512-pDr689IrW4KLU8XbSCOFq3rY0T8JJKdc3yeACN667wgCAzAjjUii10UgCl0XivD2JXogS8LfnzrlrtN07+VSug==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@wojtekmaj/react-daterange-picker/-/react-daterange-picker-5.2.1.tgz", + "integrity": "sha512-wn3528ZZzG5av+cZ3rZFUj2TuV8uPL3J8AlCyvYCuafqLKMaExjGQlzFYqjgACK80PDcmqXogV3dmB2NkEhsUA==", "dependencies": { - "clsx": "^1.2.1", + "clsx": "^2.0.0", "make-event-props": "^1.4.2", "prop-types": "^15.6.0", - "react-calendar": "^4.2.1", - "react-date-picker": "^10.2.0", + "react-calendar": "^4.4.0", + "react-date-picker": "^10.2.1", "react-fit": "^1.5.1" }, "funding": { @@ -3718,15 +3733,16 @@ "dev": true }, "node_modules/assert": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.0.0.tgz", - "integrity": "sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", "dev": true, "dependencies": { - "es6-object-assign": "^1.1.0", - "is-nan": "^1.2.1", - "object-is": "^1.0.1", - "util": "^0.12.0" + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" } }, "node_modules/assert-options": { @@ -4191,9 +4207,9 @@ } }, "node_modules/browserslist": { - "version": "4.21.10", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", - "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", + "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", "dev": true, "funding": [ { @@ -4210,10 +4226,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001517", - "electron-to-chromium": "^1.4.477", + "caniuse-lite": "^1.0.30001541", + "electron-to-chromium": "^1.4.535", "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.11" + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" @@ -4341,9 +4357,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001519", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz", - "integrity": "sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==", + "version": "1.0.30001546", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001546.tgz", + "integrity": "sha512-zvtSJwuQFpewSyRrI3AsftF6rM0X80mZkChIt1spBGEvRglCrjTniXvinc8JKRoqTwXAgvqTImaN9igfSMtUBw==", "dev": true, "funding": [ { @@ -4434,10 +4450,13 @@ } }, "node_modules/check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, + "dependencies": { + "get-func-name": "^2.0.2" + }, "engines": { "node": "*" } @@ -4536,9 +4555,9 @@ } }, "node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", "engines": { "node": ">=6" } @@ -4755,6 +4774,14 @@ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -4774,12 +4801,12 @@ "hasInstallScript": true }, "node_modules/core-js-compat": { - "version": "3.32.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.0.tgz", - "integrity": "sha512-7a9a3D1k4UCVKnLhrgALyFcP7YCsLOQIxPd0dKjf/6GuPcgyiGP70ewWdCGrSK7evyhymi0qO4EqCmSJofDeYw==", + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.33.0.tgz", + "integrity": "sha512-0w4LcLXsVEuNkIqwjjf9rjCoPhK8uqA4tMRh4Ge26vfLtUutshn+aRJU21I9LCJlh2QQHfisNToLjw1XEJLTWw==", "dev": true, "dependencies": { - "browserslist": "^4.21.9" + "browserslist": "^4.22.1" }, "funding": { "type": "opencollective", @@ -5252,12 +5279,27 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" }, + "node_modules/define-data-property": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz", + "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/define-properties": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", - "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, "dependencies": { + "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -5505,9 +5547,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.490", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.490.tgz", - "integrity": "sha512-6s7NVJz+sATdYnIwhdshx/N/9O6rvMxmhVoDSDFdj6iA45gHR8EQje70+RYsF4GeB+k0IeNSBnP7yG9ZXJFr7A==", + "version": "1.4.546", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.546.tgz", + "integrity": "sha512-cz9bBM26ZqoEmGHkdHXU3LP7OofVyEzRoMqfALQ9Au9WlB4rogAHzqj/NkNvw2JJjy4xuxS1me+pP2lbCD5Mfw==", "dev": true }, "node_modules/element-size": { @@ -5638,12 +5680,6 @@ "es6-symbol": "^3.1.1" } }, - "node_modules/es6-object-assign": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", - "integrity": "sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==", - "dev": true - }, "node_modules/es6-symbol": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", @@ -5707,6 +5743,14 @@ "source-map": "~0.6.1" } }, + "node_modules/escodegen/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/escodegen/node_modules/levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -5818,9 +5862,9 @@ } }, "node_modules/eslint-plugin-jsdoc": { - "version": "46.5.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.5.0.tgz", - "integrity": "sha512-aulXdA4I1dyWpzyS1Nh/GNoS6PavzeucxEapnMR4JUERowWvaEk2Y4A5irpHAcdXtBBHLVe8WIhdXNjoAlGQgA==", + "version": "46.5.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.5.1.tgz", + "integrity": "sha512-CPbvKprmEuJYoxMj5g8gXfPqUGgcqMM6jpH06Kp4pn5Uy5MrPkFKzoD7UFp2E4RBzfXbJz1+TeuEivwFVMkXBg==", "dev": true, "dependencies": { "@es-joy/jsdoccomment": "~0.40.1", @@ -5886,16 +5930,19 @@ "dev": true }, "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "estraverse": "^5.2.0" }, "engines": { - "node": ">=8.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { @@ -5971,35 +6018,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/eslint/node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -6073,15 +6095,6 @@ "node": ">=0.10" } }, - "node_modules/esquery/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -6094,7 +6107,7 @@ "node": ">=4.0" } }, - "node_modules/esrecurse/node_modules/estraverse": { + "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", @@ -6103,14 +6116,6 @@ "node": ">=4.0" } }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "engines": { - "node": ">=4.0" - } - }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -6210,14 +6215,6 @@ "node": ">= 0.8" } }, - "node_modules/express/node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -6525,22 +6522,23 @@ } }, "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", + "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", "dev": true, "dependencies": { - "flatted": "^3.1.0", + "flatted": "^3.2.9", + "keyv": "^4.5.3", "rimraf": "^3.0.2" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=12.0.0" } }, "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, "node_modules/flatten-vertex-data": { @@ -6552,9 +6550,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", "funding": [ { "type": "individual", @@ -6682,9 +6680,9 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "optional": true, @@ -6728,6 +6726,15 @@ "resolved": "https://registry.npmjs.org/get-canvas-context/-/get-canvas-context-1.0.2.tgz", "integrity": "sha512-LnpfLf/TNzr9zVOGiIY6aKCz8EKuXmlYNV7CM2pUjBa/B+c2I15tS7KLySep75+FuerJdmArvJLcsAXWEy2H0A==" }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", @@ -7114,12 +7121,9 @@ } }, "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dependencies": { - "function-bind": "^1.1.1" - }, + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", + "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", "engines": { "node": ">= 0.4.0" } @@ -7376,9 +7380,9 @@ } }, "node_modules/immutable": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.2.tgz", - "integrity": "sha512-oGXzbEDem9OOpDWZu88jGiYCvIsLHMvGw+8OXlpsvTFvIQplQbjg1B1cvKg8f7Hoch6+NGjpPsH1Fr+Mc2D1aA==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", + "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==", "dev": true }, "node_modules/import-fresh": { @@ -7456,48 +7460,48 @@ } }, "node_modules/intl-messageformat": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.0.tgz", - "integrity": "sha512-AvojYuOaRb6r2veOKfTVpxH9TrmjSdc5iR9R5RgBwrDZYSmAAFVT+QLbW3C4V7Qsg0OguMp67Q/EoUkxZzXRGw==", + "version": "10.5.3", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.5.3.tgz", + "integrity": "sha512-TzKn1uhJBMyuKTO4zUX47SU+d66fu1W9tVzIiZrQ6hBqQQeYscBMIzKL/qEXnFbJrH9uU5VV3+T5fWib4SIcKA==", "dependencies": { - "@formatjs/ecma402-abstract": "1.17.0", + "@formatjs/ecma402-abstract": "1.17.2", "@formatjs/fast-memoize": "2.2.0", - "@formatjs/icu-messageformat-parser": "2.6.0", + "@formatjs/icu-messageformat-parser": "2.6.2", "tslib": "^2.4.0" } }, "node_modules/intl-messageformat/node_modules/@formatjs/ecma402-abstract": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.0.tgz", - "integrity": "sha512-6ueQTeJZtwKjmh23bdkq/DMqH4l4bmfvtQH98blOSbiXv/OUiyijSW6jU22IT8BNM1ujCaEvJfTtyCYVH38EMQ==", + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.2.tgz", + "integrity": "sha512-k2mTh0m+IV1HRdU0xXM617tSQTi53tVR2muvYOsBeYcUgEAyxV1FOC7Qj279th3fBVQ+Dj6muvNJZcHSPNdbKg==", "dependencies": { - "@formatjs/intl-localematcher": "0.4.0", + "@formatjs/intl-localematcher": "0.4.2", "tslib": "^2.4.0" } }, "node_modules/intl-messageformat/node_modules/@formatjs/icu-messageformat-parser": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.6.0.tgz", - "integrity": "sha512-yT6at0qc0DANw9qM/TU8RZaCtfDXtj4pZM/IC2WnVU80yAcliS3KVDiuUt4jSQAeFL9JS5bc2hARnFmjPdA6qw==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.6.2.tgz", + "integrity": "sha512-nF/Iww7sc5h+1MBCDRm68qpHTCG4xvGzYs/x9HFcDETSGScaJ1Fcadk5U/NXjXeCtzD+DhN4BAwKFVclHfKMdA==", "dependencies": { - "@formatjs/ecma402-abstract": "1.17.0", - "@formatjs/icu-skeleton-parser": "1.6.0", + "@formatjs/ecma402-abstract": "1.17.2", + "@formatjs/icu-skeleton-parser": "1.6.2", "tslib": "^2.4.0" } }, "node_modules/intl-messageformat/node_modules/@formatjs/icu-skeleton-parser": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.6.0.tgz", - "integrity": "sha512-eMmxNpoX/J1IPUjPGSZwo0Wh+7CEvdEMddP2Jxg1gQJXfGfht/FdW2D5XDFj3VMbOTUQlDIdZJY7uC6O6gjPoA==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.6.2.tgz", + "integrity": "sha512-VtB9Slo4ZL6QgtDFJ8Injvscf0xiDd4bIV93SOJTBjUF4xe2nAWOoSjLEtqIG+hlIs1sNrVKAaFo3nuTI4r5ZA==", "dependencies": { - "@formatjs/ecma402-abstract": "1.17.0", + "@formatjs/ecma402-abstract": "1.17.2", "tslib": "^2.4.0" } }, "node_modules/intl-messageformat/node_modules/@formatjs/intl-localematcher": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.0.tgz", - "integrity": "sha512-bRTd+rKomvfdS4QDlVJ6TA/Jx1F2h/TBVO5LjvhQ7QPPHp19oPNMIum7W2CMEReq/zPxpmCeB31F9+5gl/qtvw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.2.tgz", + "integrity": "sha512-BGdtJFmaNJy5An/Zan4OId/yR9Ih1OojFjcduX/xOvq798OgWSyDtd6Qd5jqJXwJs1ipe4Fxu9+cshic5Ox2tA==", "dependencies": { "tslib": "^2.4.0" } @@ -7892,6 +7896,12 @@ "node": ">=4" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -7951,14 +7961,20 @@ } }, "node_modules/jsonwebtoken": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz", - "integrity": "sha512-K8wx7eJ5TPvEjuiVSkv167EVboBDv9PZdDoF7BgeQnBLVvZWW9clr2PsQHVJDTKaEIH5JBIwHujGcHp7GgI2eg==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", "dependencies": { "jws": "^3.2.2", - "lodash": "^4.17.21", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", "ms": "^2.1.1", - "semver": "^7.3.8" + "semver": "^7.5.4" }, "engines": { "node": ">=12", @@ -8025,6 +8041,15 @@ "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==" }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -8119,6 +8144,36 @@ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "dev": true }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -8129,6 +8184,11 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -9114,6 +9174,24 @@ "node": ">= 0.4" } }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -9430,12 +9508,12 @@ } }, "node_modules/pg-promise": { - "version": "11.5.3", - "resolved": "https://registry.npmjs.org/pg-promise/-/pg-promise-11.5.3.tgz", - "integrity": "sha512-OBrGa/fE8Sw9tGxXfI45+8sLsYXHLqc8nKWI9Kv7aFMXEWQhUNlCMBPe8m74ycjobnhYBnv4SM0/sFe35pyS8Q==", + "version": "11.5.4", + "resolved": "https://registry.npmjs.org/pg-promise/-/pg-promise-11.5.4.tgz", + "integrity": "sha512-esYSkDt2h6NQOkfotGAm1Ld5OjoITJLpB88Z1PIlcAU/RQ0XQE2PxW0bLJEOMHPGV5iaRnj1Y7ARznXbgN4FNw==", "dependencies": { "assert-options": "0.8.1", - "pg": "8.11.2", + "pg": "8.11.3", "pg-minify": "1.6.3", "spex": "3.3.0" }, @@ -9444,9 +9522,9 @@ } }, "node_modules/pg-promise/node_modules/pg": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.2.tgz", - "integrity": "sha512-l4rmVeV8qTIrrPrIR3kZQqBgSN93331s9i6wiUiLOSk0Q7PmUxZD/m1rQI622l3NfqBby9Ar5PABfS/SulfieQ==", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", "dependencies": { "buffer-writer": "2.0.0", "packet-reader": "1.0.0", @@ -9653,9 +9731,9 @@ "integrity": "sha512-mKjR5nolISvF+q2BtC1fi/llpxBPTQ3wLWN8+ldzdw2Hocpc8C72ZqnamCM4Z6z+68GVVjkeM01WJegQmZ8MEQ==" }, "node_modules/postcss": { - "version": "8.4.27", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.27.tgz", - "integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==", + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", "dev": true, "funding": [ { @@ -10136,44 +10214,56 @@ } }, "node_modules/react-calendar": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/react-calendar/-/react-calendar-4.3.0.tgz", - "integrity": "sha512-TyCv8NbXnqXADyXNtMG0szkGvJNH3NG/WMTEE2q6g3RqAsFNyHwYbQD5Kvb6jRV/CqO0WB+oMCtkxblprdeT5A==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/react-calendar/-/react-calendar-4.6.0.tgz", + "integrity": "sha512-GJ6ZipKMQmlK666t+0hgmecu6WHydEnMWJjKdEkUxW6F471hiM5DkbWXkfr8wlAg9tc9feNCBhXw3SqsPOm01A==", "dependencies": { - "@types/react": "*", "@wojtekmaj/date-utils": "^1.1.3", - "clsx": "^1.2.1", + "clsx": "^2.0.0", "get-user-locale": "^2.2.1", - "prop-types": "^15.6.0" + "prop-types": "^15.6.0", + "tiny-warning": "^1.0.0" }, "funding": { "url": "https://github.com/wojtekmaj/react-calendar?sponsor=1" }, "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/react-date-picker": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/react-date-picker/-/react-date-picker-10.2.0.tgz", - "integrity": "sha512-AapiakQ9hY2sPNyaBgLJgPDXeeZyiC3Px75jWbkB9NwJqX1gAwRQ7O8qshRqGWJX7T4/cUh6n59j+N+M4GpGow==", + "version": "10.5.1", + "resolved": "https://registry.npmjs.org/react-date-picker/-/react-date-picker-10.5.1.tgz", + "integrity": "sha512-Ipsq0NorPqXRuZQsqWhdd86RlSbZbnhpiH0aEMzJo6Rs2YA7eALZKB5lrl9stSjpDriDJH0dyh2kdWQmkBhNFg==", "dependencies": { "@wojtekmaj/date-utils": "^1.1.3", - "clsx": "^1.2.1", + "clsx": "^2.0.0", "get-user-locale": "^2.2.1", - "make-event-props": "^1.4.2", + "make-event-props": "^1.6.0", "prop-types": "^15.6.0", - "react-calendar": "^4.2.1", - "react-fit": "^1.5.1", - "update-input-width": "^1.3.1" + "react-calendar": "^4.6.0", + "react-fit": "^1.7.0", + "update-input-width": "^1.4.0" }, "funding": { "url": "https://github.com/wojtekmaj/react-date-picker?sponsor=1" }, "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, "node_modules/react-dom": { @@ -10221,19 +10311,19 @@ } }, "node_modules/react-intl": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.4.4.tgz", - "integrity": "sha512-/C9Sl/5//ohfkNG6AWlJuf4BhTXsbzyk93K62A4zRhSPANyOGpKZ+fWhN+TLfFd5YjDUHy+exU/09y0w1bO4Xw==", - "dependencies": { - "@formatjs/ecma402-abstract": "1.17.0", - "@formatjs/icu-messageformat-parser": "2.6.0", - "@formatjs/intl": "2.9.0", - "@formatjs/intl-displaynames": "6.5.0", - "@formatjs/intl-listformat": "7.4.0", + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.4.7.tgz", + "integrity": "sha512-0hnOHAZhxTFqD1hGTxrF40qNyZJPPYiGhWIIxIz0Udz+3e3c7sdN80qlxArR+AbJ+jb5ALXZkJYH20+GPFCM0Q==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.17.2", + "@formatjs/icu-messageformat-parser": "2.6.2", + "@formatjs/intl": "2.9.3", + "@formatjs/intl-displaynames": "6.5.2", + "@formatjs/intl-listformat": "7.4.2", "@types/hoist-non-react-statics": "^3.3.1", "@types/react": "16 || 17 || 18", "hoist-non-react-statics": "^3.3.2", - "intl-messageformat": "10.5.0", + "intl-messageformat": "10.5.3", "tslib": "^2.4.0" }, "peerDependencies": { @@ -10247,37 +10337,37 @@ } }, "node_modules/react-intl/node_modules/@formatjs/ecma402-abstract": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.0.tgz", - "integrity": "sha512-6ueQTeJZtwKjmh23bdkq/DMqH4l4bmfvtQH98blOSbiXv/OUiyijSW6jU22IT8BNM1ujCaEvJfTtyCYVH38EMQ==", + "version": "1.17.2", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.17.2.tgz", + "integrity": "sha512-k2mTh0m+IV1HRdU0xXM617tSQTi53tVR2muvYOsBeYcUgEAyxV1FOC7Qj279th3fBVQ+Dj6muvNJZcHSPNdbKg==", "dependencies": { - "@formatjs/intl-localematcher": "0.4.0", + "@formatjs/intl-localematcher": "0.4.2", "tslib": "^2.4.0" } }, "node_modules/react-intl/node_modules/@formatjs/icu-messageformat-parser": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.6.0.tgz", - "integrity": "sha512-yT6at0qc0DANw9qM/TU8RZaCtfDXtj4pZM/IC2WnVU80yAcliS3KVDiuUt4jSQAeFL9JS5bc2hARnFmjPdA6qw==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.6.2.tgz", + "integrity": "sha512-nF/Iww7sc5h+1MBCDRm68qpHTCG4xvGzYs/x9HFcDETSGScaJ1Fcadk5U/NXjXeCtzD+DhN4BAwKFVclHfKMdA==", "dependencies": { - "@formatjs/ecma402-abstract": "1.17.0", - "@formatjs/icu-skeleton-parser": "1.6.0", + "@formatjs/ecma402-abstract": "1.17.2", + "@formatjs/icu-skeleton-parser": "1.6.2", "tslib": "^2.4.0" } }, "node_modules/react-intl/node_modules/@formatjs/icu-skeleton-parser": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.6.0.tgz", - "integrity": "sha512-eMmxNpoX/J1IPUjPGSZwo0Wh+7CEvdEMddP2Jxg1gQJXfGfht/FdW2D5XDFj3VMbOTUQlDIdZJY7uC6O6gjPoA==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.6.2.tgz", + "integrity": "sha512-VtB9Slo4ZL6QgtDFJ8Injvscf0xiDd4bIV93SOJTBjUF4xe2nAWOoSjLEtqIG+hlIs1sNrVKAaFo3nuTI4r5ZA==", "dependencies": { - "@formatjs/ecma402-abstract": "1.17.0", + "@formatjs/ecma402-abstract": "1.17.2", "tslib": "^2.4.0" } }, "node_modules/react-intl/node_modules/@formatjs/intl-localematcher": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.0.tgz", - "integrity": "sha512-bRTd+rKomvfdS4QDlVJ6TA/Jx1F2h/TBVO5LjvhQ7QPPHp19oPNMIum7W2CMEReq/zPxpmCeB31F9+5gl/qtvw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.4.2.tgz", + "integrity": "sha512-BGdtJFmaNJy5An/Zan4OId/yR9Ih1OojFjcduX/xOvq798OgWSyDtd6Qd5jqJXwJs1ipe4Fxu9+cshic5Ox2tA==", "dependencies": { "tslib": "^2.4.0" } @@ -10327,9 +10417,9 @@ } }, "node_modules/react-redux": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.2.tgz", - "integrity": "sha512-xJKYI189VwfsFc4CJvHqHlDrzyFTY/3vZACbE+rr/zQ34Xx1wQfB4OTOSeOSNrF6BDVe8OOdxIrAnMGXA3ggfw==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz", + "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==", "dependencies": { "@babel/runtime": "^7.12.1", "@types/hoist-non-react-statics": "^3.3.1", @@ -10504,9 +10594,9 @@ "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" }, "node_modules/react-select": { - "version": "5.7.4", - "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.7.4.tgz", - "integrity": "sha512-NhuE56X+p9QDFh4BgeygHFIvJJszO1i1KSkg/JPcIJrbovyRtI+GuOEa4XzFCEpZRAEoEI8u/cAHK+jG/PgUzQ==", + "version": "5.7.7", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.7.7.tgz", + "integrity": "sha512-HhashZZJDRlfF/AKj0a0Lnfs3sRdw/46VJIRd8IbB9/Ovr74+ZIwkAdSBjSPXsFMG+u72c5xShqwLSKIJllzqw==", "dependencies": { "@babel/runtime": "^7.12.0", "@emotion/cache": "^11.4.0", @@ -10535,6 +10625,14 @@ "react-dom": ">=16" } }, + "node_modules/react-toastify/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/react-tooltip": { "version": "4.2.21", "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-4.2.21.tgz", @@ -10651,9 +10749,9 @@ "dev": true }, "node_modules/regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", "dev": true, "dependencies": { "regenerate": "^1.4.2" @@ -10835,9 +10933,9 @@ "integrity": "sha512-wachIH1FWB/ceIgBP418PXtjJyhvgjtjqi0Go5nCqe/2xrwwAyCn1/4krfBurNfxxo7dWpiLGb1yYjCrWi40PA==" }, "node_modules/resolve": { - "version": "1.22.4", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz", - "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==", + "version": "1.22.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", + "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -10985,9 +11083,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" }, "node_modules/scheduler": { "version": "0.23.0", @@ -11392,9 +11490,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", - "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", + "version": "3.0.16", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", + "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", "dev": true }, "node_modules/spex": { @@ -11774,9 +11872,9 @@ } }, "node_modules/terser": { - "version": "5.19.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.2.tgz", - "integrity": "sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==", + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.21.0.tgz", + "integrity": "sha512-WtnFKrxu9kaoXuiZFSGrcAvvBqAdmKx0SFNmVNYdJamMu9yyN3I/QF0FbH4QcqJQ+y1CJnzxGIKH0cSj+FGYRw==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -11998,9 +12096,9 @@ } }, "node_modules/ts-api-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.2.tgz", - "integrity": "sha512-Cbu4nIqnEdd+THNEsBdkolnOXhg0I8XteoHaEKgvsxpsbWda4IsUut2c187HxywQCvveojow0Dgw/amxtSKVkQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", "dev": true, "engines": { "node": ">=16.13.0" @@ -12132,9 +12230,9 @@ "dev": true }, "node_modules/tslib": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", - "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tty-browserify": { "version": "0.0.1", @@ -12279,9 +12377,9 @@ "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==" }, "node_modules/update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "dev": true, "funding": [ { @@ -12331,13 +12429,13 @@ } }, "node_modules/url": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.1.tgz", - "integrity": "sha512-rWS3H04/+mzzJkv0eZ7vEDGiQbgquI1fGfOad6zKvgYQi1SzMmhl7c/DdRGxhaWrVH6z0qWITo8rpnxK/RfEhA==", + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz", + "integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==", "dev": true, "dependencies": { "punycode": "^1.4.1", - "qs": "^6.11.0" + "qs": "^6.11.2" } }, "node_modules/url/node_modules/punycode": { @@ -12346,6 +12444,21 @@ "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", "dev": true }, + "node_modules/url/node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/use-isomorphic-layout-effect": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", @@ -12587,6 +12700,28 @@ "node": ">=10.13.0" } }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/webpack/node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", diff --git a/package.json b/package.json index 1fc19ae73..0164c5244 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "babel-plugin-lodash": "~3.3.4" }, "dependencies": { - "@reduxjs/toolkit": "~2.0.0-beta.2", + "@reduxjs/toolkit": "~2.0.0-beta.3", "@wojtekmaj/react-daterange-picker": "~5.2.0", "axios": "~0.24.0", "bcryptjs": "~2.4.3", diff --git a/src/client/app/actions/graph.ts b/src/client/app/actions/graph.ts index 3424b798b..7da9db504 100644 --- a/src/client/app/actions/graph.ts +++ b/src/client/app/actions/graph.ts @@ -32,13 +32,13 @@ export function toggleOptionsVisibility() { } function changeGraphZoom(timeInterval: TimeInterval) { - return graphSlice.actions.changeGraphZoom(timeInterval); + return graphSlice.actions.updateTimeInterval(timeInterval); } export function changeBarDuration(barDuration: moment.Duration): Thunk { return (dispatch: Dispatch, getState: GetState) => { dispatch(graphSlice.actions.updateBarDuration(barDuration)); - dispatch(fetchNeededBarReadings(getState().graph.timeInterval, getState().graph.selectedUnit)); + dispatch(fetchNeededBarReadings(getState().graph.queryTimeInterval, getState().graph.selectedUnit)); return Promise.resolve(); }; } @@ -70,10 +70,10 @@ export function changeSelectedMeters(meterIDs: number[]): Thunk { dispatch(graphSlice.actions.updateSelectedMeters(meterIDs)); // Nesting dispatches to preserve that updateSelectedMeters() is called before fetching readings dispatch((dispatch2: Dispatch) => { - dispatch2(fetchNeededLineReadings(getState().graph.timeInterval, getState().graph.selectedUnit)); - dispatch2(fetchNeededBarReadings(getState().graph.timeInterval, getState().graph.selectedUnit)); + dispatch2(fetchNeededLineReadings(getState().graph.queryTimeInterval, getState().graph.selectedUnit)); + dispatch2(fetchNeededBarReadings(getState().graph.queryTimeInterval, getState().graph.selectedUnit)); dispatch2(fetchNeededCompareReadings(getState().graph.comparePeriod, getState().graph.selectedUnit)); - dispatch2(fetchNeededMapReadings(getState().graph.timeInterval, getState().graph.selectedUnit)); + dispatch2(fetchNeededMapReadings(getState().graph.queryTimeInterval, getState().graph.selectedUnit)); }); return Promise.resolve(); }; @@ -84,10 +84,10 @@ export function changeSelectedGroups(groupIDs: number[]): Thunk { dispatch(graphSlice.actions.updateSelectedGroups(groupIDs)); // Nesting dispatches to preserve that updateSelectedGroups() is called before fetching readings dispatch((dispatch2: Dispatch) => { - dispatch2(fetchNeededLineReadings(getState().graph.timeInterval, getState().graph.selectedUnit)); - dispatch2(fetchNeededBarReadings(getState().graph.timeInterval, getState().graph.selectedUnit)); + dispatch2(fetchNeededLineReadings(getState().graph.queryTimeInterval, getState().graph.selectedUnit)); + dispatch2(fetchNeededBarReadings(getState().graph.queryTimeInterval, getState().graph.selectedUnit)); dispatch2(fetchNeededCompareReadings(getState().graph.comparePeriod, getState().graph.selectedUnit)); - dispatch2(fetchNeededMapReadings(getState().graph.timeInterval, getState().graph.selectedUnit)); + dispatch2(fetchNeededMapReadings(getState().graph.queryTimeInterval, getState().graph.selectedUnit)); }); return Promise.resolve(); }; @@ -97,10 +97,10 @@ export function changeSelectedUnit(unitID: number): Thunk { return (dispatch: Dispatch, getState: GetState) => { dispatch(graphSlice.actions.updateSelectedUnit(unitID)); dispatch((dispatch2: Dispatch) => { - dispatch(fetchNeededLineReadings(getState().graph.timeInterval, unitID)); - dispatch2(fetchNeededBarReadings(getState().graph.timeInterval, unitID)); + dispatch(fetchNeededLineReadings(getState().graph.queryTimeInterval, unitID)); + dispatch2(fetchNeededBarReadings(getState().graph.queryTimeInterval, unitID)); dispatch2(fetchNeededCompareReadings(getState().graph.comparePeriod, unitID)); - dispatch2(fetchNeededMapReadings(getState().graph.timeInterval, unitID)); + dispatch2(fetchNeededMapReadings(getState().graph.queryTimeInterval, unitID)); }); return Promise.resolve(); } @@ -116,7 +116,7 @@ function fetchNeededReadingsForGraph(timeInterval: TimeInterval, unitID: number) } function shouldChangeGraphZoom(state: State, timeInterval: TimeInterval): boolean { - return !state.graph.timeInterval.equals(timeInterval); + return !state.graph.queryTimeInterval.equals(timeInterval); } export function changeGraphZoomIfNeeded(timeInterval: TimeInterval): Thunk { diff --git a/src/client/app/components/BarChartComponent.tsx b/src/client/app/components/BarChartComponent.tsx index 684e880d2..2b1ebb5c1 100644 --- a/src/client/app/components/BarChartComponent.tsx +++ b/src/client/app/components/BarChartComponent.tsx @@ -4,23 +4,24 @@ import * as _ from 'lodash'; import * as moment from 'moment'; +import { PlotRelayoutEvent } from 'plotly.js'; import * as React from 'react'; -import Plot from 'react-plotly.js'; +import Plot, { Figure } from 'react-plotly.js'; +import { TimeInterval } from '../../../common/TimeInterval'; +import { graphSlice, selectSelectedGroups, selectSelectedMeters } from '../reducers/graph'; +import { groupsSlice } from '../reducers/groups'; +import { metersSlice } from '../reducers/meters'; +import { unitsSlice } from '../reducers/units'; import { readingsApi } from '../redux/api/readingsApi'; -import { useAppSelector } from '../redux/hooks'; +import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { BarReadingApiArgs, ChartQueryProps } from '../redux/selectors/dataSelectors'; import { DataType } from '../types/Datasources'; -import Locales from '../types/locales'; import { UnitRepresentType } from '../types/redux/units'; import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; import getGraphColor from '../utils/getGraphColor'; import { barUnitLabel } from '../utils/graphics'; import translate from '../utils/translate'; import SpinnerComponent from './SpinnerComponent'; -import { graphSlice } from '../reducers/graph'; -import { groupsSlice } from '../reducers/groups'; -import { metersSlice } from '../reducers/meters'; -import { unitsSlice } from '../reducers/units'; /** * Passes the current redux state of the barchart, and turns it into props for the React @@ -30,30 +31,25 @@ import { unitsSlice } from '../reducers/units'; * @returns Plotly BarChart */ export default function BarChartComponent(props: ChartQueryProps) { - const { meterArgs, groupsArgs } = props.queryProps; - const { - data: meterReadings, - isFetching: meterIsFetching - } = readingsApi.useBarQuery(meterArgs, { skip: !meterArgs.ids.length }); - - const { - data: groupData, - isFetching: groupIsFetching - } = readingsApi.useBarQuery(groupsArgs, { skip: !groupsArgs.ids.length }); - + const { meterArgs, groupsArgs, meterSkipQuery, groupSkipQuery } = props.queryProps; + const dispatch = useAppDispatch(); const barDuration = useAppSelector(state => state.graph.barDuration); const barStacking = useAppSelector(state => state.graph.barStacking); const unitID = useAppSelector(state => state.graph.selectedUnit); - const datasets: any[] = []; // The unit label depends on the unit which is in selectUnit state. const graphingUnit = useAppSelector(state => state.graph.selectedUnit); - const unitDataByID = useAppSelector(state => unitsSlice.selectors.unitDataById(state)); + const unitDataByID = useAppSelector(state => unitsSlice.selectors.selectUnitDataById(state)); const selectedAreaNormalization = useAppSelector(state => state.graph.areaNormalization); const selectedAreaUnit = useAppSelector(state => state.graph.selectedAreaUnit); - const selectedMeters = useAppSelector(state => graphSlice.selectors.selectedMeters(state)); - const selectedGroups = useAppSelector(state => graphSlice.selectors.selectedGroups(state)); - const meterDataByID = useAppSelector(state => metersSlice.selectors.meterDataByID(state)); - const groupDataByID = useAppSelector(state => groupsSlice.selectors.groupDataByID(state)); + const selectedMeters = useAppSelector(selectSelectedMeters); + const selectedGroups = useAppSelector(selectSelectedGroups); + const meterDataByID = useAppSelector(state => metersSlice.selectors.selectMeterDataByID(state)); + const groupDataByID = useAppSelector(state => groupsSlice.selectors.selectGroupDataByID(state)); + + // useQueryHooks for data fetching + const { data: meterReadings, isFetching: meterIsFetching } = readingsApi.useBarQuery(meterArgs, { skip: meterSkipQuery }); + const { data: groupData, isFetching: groupIsFetching } = readingsApi.useBarQuery(groupsArgs, { skip: groupSkipQuery }); + const datasets = []; if (meterIsFetching || groupIsFetching) { return @@ -187,105 +183,88 @@ export default function BarChartComponent(props: ChartQueryProps${translate('bar.raw')}`, - 'xref': 'paper', - 'yref': 'paper', - 'showarrow': false, - 'font': { - 'size': 28 - } - } - ] - } - } else if (datasets.length === 0) { - // There is not data so tell user. - layout = { - 'xaxis': { - 'visible': false - }, - 'yaxis': { - 'visible': false - }, - 'annotations': [ - { - 'text': `${translate('select.meter.group')}`, - 'xref': 'paper', - 'yref': 'paper', - 'showarrow': false, - 'font': { - 'size': 28 - } - } - ] + // Method responsible for setting the 'Working Time Interval' + const handleOnInit = (figure: Figure) => { + if (figure.layout.xaxis?.range) { + const startTS = moment.utc(figure.layout.xaxis?.range[0]) + const endTS = moment.utc(figure.layout.xaxis?.range[1]) + const workingTimeInterval = new TimeInterval(startTS, endTS); + dispatch(graphSlice.actions.updateWorkingTimeInterval(workingTimeInterval)) + + // console.log(figure.layout.xaxis?.range, figure.layout.xaxis?.rangeslider?.range, figure.layout.xaxis) } + } - } else { - // This normal so plot. - layout = { - barmode: (barStacking ? 'stack' : 'group'), - bargap: 0.2, // Gap between different times of readings - bargroupgap: 0.1, // Gap between different meter's readings under the same timestamp - autosize: true, - height: 700, // Height is set to 700 for now, but we do need to scale in the future (issue #466) - showlegend: true, - legend: { - x: 0, - y: 1.1, - orientation: 'h' - }, - yaxis: { - title: unitLabel, - showgrid: true, - gridcolor: '#ddd' - }, - xaxis: { - showgrid: true, - gridcolor: '#ddd', - tickfont: { - size: 10 - }, - tickangle: -45, - autotick: true, - nticks: 10, - automargin: true - }, - margin: { - t: 0, - b: 120, - l: 120 - } - }; + const handleRelayout = (e: PlotRelayoutEvent) => { + console.log(typeof e['xaxis.range[0]'], typeof e['xaxis.range[1]']) + // This event emits an object that contains values indicating changes in the user's graph, such as zooming. + // These values indicate when the user has zoomed or made other changes to the graph. + if (e['xaxis.range[0]'] && e['xaxis.range[0]']) { + // The event signals changes in the user's interaction with the graph. + // this will automatically trigger a refetch due to updating a query arg. + const startTS = moment.utc(e['xaxis.range[0]']) + const endTS = moment.utc(e['xaxis.range[1]']) + const workingTimeInterval = new TimeInterval(startTS, endTS); + dispatch(graphSlice.actions.updateTimeInterval(workingTimeInterval)); + } } // Assign all the parameters required to create the Plotly object (data, layout, config) to the variable props, returned by mapStateToProps // The Plotly toolbar is displayed if displayModeBar is set to true (not for bar charts) - const config = { - displayModeBar: false, - responsive: true, - locales: Locales // makes locales available for use - } - return ( - - ); -} + if (raw) { + return

${translate('bar.raw')}

+ } + let enoughData = false; + datasets.forEach(dataset => { + if (dataset.x.length > 1) { + enoughData = true + } + }) + console.log(datasets.length, datasets) + if (datasets.length === 0) { + return

+ {`${translate('select.meter.group')}`} +

+ } else if (!enoughData) { + // This normal so plot. + return

+ {`${translate('threeD.no.data')}`} +

+ } else { + return ( + + ); + } +} \ No newline at end of file diff --git a/src/client/app/components/DateRangeComponent.tsx b/src/client/app/components/DateRangeComponent.tsx index 10735b58a..9bb8bd4c2 100644 --- a/src/client/app/components/DateRangeComponent.tsx +++ b/src/client/app/components/DateRangeComponent.tsx @@ -4,62 +4,57 @@ import DateRangePicker from '@wojtekmaj/react-daterange-picker'; import '@wojtekmaj/react-daterange-picker/dist/DateRangePicker.css'; -import { CloseReason, Value } from '@wojtekmaj/react-daterange-picker/dist/cjs/shared/types'; +import { Value } from '@wojtekmaj/react-daterange-picker/dist/cjs/shared/types'; import * as React from 'react'; -import { useEffect, useState } from 'react'; import 'react-calendar/dist/Calendar.css'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { graphSlice } from '../reducers/graph'; +import { useAppSelector } from '../redux/hooks'; import { Dispatch } from '../types/redux/actions'; -import { ChartTypes } from '../types/redux/graph'; -import { State } from '../types/redux/state'; import { dateRangeToTimeInterval, timeIntervalToDateRange } from '../utils/dateRangeCompatibility'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; - /** * A component which allows users to select date ranges in lieu of a slider (line graphic) * @returns Date Range Calendar Picker - */ +*/ export default function DateRangeComponent() { - const timeInterval = useSelector((state: State) => state.graph.timeInterval); - const locale = useSelector((state: State) => state.options.selectedLanguage); - const chartToRender = useSelector((state: State) => state.graph.chartToRender); + const { selectWorkingTimeInterval: graphWorkingTimeInterval, selectQueryTimeInterval } = graphSlice.selectors const dispatch: Dispatch = useDispatch(); + const timeInterval = useAppSelector(selectQueryTimeInterval); + const workingTimeInterval = useAppSelector(graphWorkingTimeInterval); + const locale = useAppSelector(state => state.options.selectedLanguage); - const [dateRange, setDateRange] = useState([null, null]); - // Keep this component in sync with global time interval - useEffect(() => setDateRange(timeIntervalToDateRange(timeInterval)), [timeInterval]); + const handleChange = (value: Value) => { + console.log(value) - // Don't Close Calendar when selecting dates. - // This allows the value to update before calling the onCalClose() method to fetch data if needed. - const shouldCloseCalendar = (props: { reason: CloseReason }) => { return props.reason === 'select' ? false : true; }; - const onCalClose = () => { dispatch(graphSlice.actions.changeGraphZoom(dateRangeToTimeInterval(dateRange))) }; + if (!value) { + // Value has been cleared + dispatch(graphSlice.actions.resetTimeInterval()) + } else { + dispatch(graphSlice.actions.updateTimeInterval(dateRangeToTimeInterval(value))) - // Only Render if a 3D Graphic Type Selected. - if (chartToRender === ChartTypes.threeD) - return ( -
-

- {translate('date.range')}: - -

- -
); - else - return null; -} + } + } + return ( +
+

+ {translate('date.range')}: + +

+ +
+ ); +} -const labelStyle: React.CSSProperties = { fontWeight: 'bold', margin: 0 }; \ No newline at end of file +const labelStyle: React.CSSProperties = { fontWeight: 'bold', margin: 0 }; diff --git a/src/client/app/components/ExportComponent.tsx b/src/client/app/components/ExportComponent.tsx index 8f37a1fbb..c928455f2 100644 --- a/src/client/app/components/ExportComponent.tsx +++ b/src/client/app/components/ExportComponent.tsx @@ -42,7 +42,7 @@ export default function ExportComponent() { // error bar state const errorBarState = useSelector((state: State) => state.graph.showMinMax); // Time range of graphic - const timeInterval = graphState.timeInterval; + const timeInterval = graphState.queryTimeInterval; // Function to export the data in a graph. const exportGraphReading = () => { @@ -222,7 +222,7 @@ export default function ExportComponent() { // we will still get the correct count since this is not done very often and don't want to get // the wrong value. The time to do this is small compared to most raw exports (if file is large // when it matters). - const count = await metersApi.lineReadingsCount(graphState.selectedMeters, graphState.timeInterval); + const count = await metersApi.lineReadingsCount(graphState.selectedMeters, graphState.queryTimeInterval); // Estimated file size in MB. Note that changing the language effects the size about +/- 8%. // This is just a decent estimate for larger files. const fileSize = (count * 0.082 / 1000); diff --git a/src/client/app/components/LineChartComponent.tsx b/src/client/app/components/LineChartComponent.tsx index c12e4e88a..70aaf4e23 100644 --- a/src/client/app/components/LineChartComponent.tsx +++ b/src/client/app/components/LineChartComponent.tsx @@ -4,12 +4,11 @@ import * as _ from 'lodash'; import * as moment from 'moment'; -import { Datum, PlotRelayoutEvent } from 'plotly.js'; +import { PlotRelayoutEvent } from 'plotly.js'; import * as React from 'react'; -import Plot from 'react-plotly.js'; -import { Button } from 'reactstrap'; +import Plot, { Figure } from 'react-plotly.js'; import { TimeInterval } from '../../../common/TimeInterval'; -import { graphSlice } from '../reducers/graph'; +import { graphSlice, selectSelectedGroups, selectSelectedMeters } from '../reducers/graph'; import { groupsSlice } from '../reducers/groups'; import { metersSlice } from '../reducers/meters'; import { unitsSlice } from '../reducers/units'; @@ -17,13 +16,11 @@ import { readingsApi } from '../redux/api/readingsApi'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { ChartQueryProps, LineReadingApiArgs } from '../redux/selectors/dataSelectors'; import { DataType } from '../types/Datasources'; -import Locales from '../types/locales'; import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; import getGraphColor from '../utils/getGraphColor'; import { lineUnitLabel } from '../utils/graphics'; import translate from '../utils/translate'; import LogoSpinner from './LogoSpinner'; -import TooltipMarkerComponent from './TooltipMarkerComponent'; /** @@ -31,64 +28,34 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; * @returns plotlyLine graphic */ export default function LineChartComponent(props: ChartQueryProps) { - const { meterArgs, groupsArgs } = props.queryProps; + const { meterArgs, groupsArgs, meterSkipQuery, groupSkipQuery } = props.queryProps; const dispatch = useAppDispatch(); - const { - data: meterReadings, - isFetching: meterIsFetching - } = readingsApi.useLineQuery(meterArgs, { skip: !meterArgs.ids.length }); - - const { - data: groupData, - isFetching: groupIsFetching - } = readingsApi.useLineQuery(groupsArgs, { skip: !groupsArgs.ids.length }); const selectedUnit = useAppSelector(state => state.graph.selectedUnit); - const datasets: any[] = []; // The unit label depends on the unit which is in selectUnit state. const graphingUnit = useAppSelector(state => state.graph.selectedUnit); // The current selected rate const currentSelectedRate = useAppSelector(state => state.graph.lineGraphRate); - const timeInterval = useAppSelector(state => state.graph.timeInterval); - const unitDataByID = useAppSelector(state => unitsSlice.selectors.unitDataById(state)); + const unitDataByID = useAppSelector(state => unitsSlice.selectors.selectUnitDataById(state)); const selectedAreaNormalization = useAppSelector(state => state.graph.areaNormalization); const selectedAreaUnit = useAppSelector(state => state.graph.selectedAreaUnit); - const selectedMeters = useAppSelector(state => graphSlice.selectors.selectedMeters(state)); - const selectedGroups = useAppSelector(state => graphSlice.selectors.selectedGroups(state)); - const metersState = useAppSelector(state => metersSlice.selectors.meterState(state)); - const meterDataByID = useAppSelector(state => metersSlice.selectors.meterDataByID(state)); - const groupDataByID = useAppSelector(state => groupsSlice.selectors.groupDataByID(state)); - // Keeps Track of the Slider Values - const startTsRef = React.useRef(null); - const endTsRef = React.useRef(null); + const selectedMeters = useAppSelector(state => selectSelectedMeters(state)); + const selectedGroups = useAppSelector(state => selectSelectedGroups(state)); + const metersState = useAppSelector(state => metersSlice.selectors.selectMeterState(state)); + const meterDataByID = useAppSelector(state => metersSlice.selectors.selectMeterDataByID(state)); + const groupDataByID = useAppSelector(state => groupsSlice.selectors.selectGroupDataByID(state)); + + // dataFetching Query Hooks + const { data: meterReadings, isFetching: meterIsFetching } = readingsApi.useLineQuery(meterArgs, { skip: meterSkipQuery }); + const { data: groupData, isFetching: groupIsFetching } = readingsApi.useLineQuery(groupsArgs, { skip: groupSkipQuery }); + + const datasets = []; if (meterIsFetching || groupIsFetching) { return // return } - const handleRelayout = (e: PlotRelayoutEvent) => { - // Relayout emits many kinds of events listen for the two that give the slider range changes. - if (e['xaxis.range']) { - startTsRef.current = e['xaxis.range'][0]; - endTsRef.current = e['xaxis.range'][1]; - } else if (e['xaxis.range[0]'] && e['xaxis.range[1]']) { - startTsRef.current = e['xaxis.range[0]']; - endTsRef.current = e['xaxis.range[1]']; - } - } - - - const getTimeIntervalFromRefs = () => { - if (!startTsRef.current && !endTsRef.current) { - return TimeInterval.unbounded(); - } else { - return new TimeInterval( - moment.utc(startTsRef.current), - moment.utc(endTsRef.current) - ) - } - } // The unit label depends on the unit which is in selectUnit state. // The current selected rate let unitLabel = ''; @@ -257,121 +224,78 @@ export default function LineChartComponent(props: ChartQueryProps { + // dispatch(graphSlice.actions.updateWorkingTimeInterval()) + if (figure.layout.xaxis?.range) { + const startTS = moment.utc(figure.layout.xaxis?.range[0]) + const endTS = moment.utc(figure.layout.xaxis?.range[1]) + const workingTimeInterval = new TimeInterval(startTS, endTS); + dispatch(graphSlice.actions.updateWorkingTimeInterval(workingTimeInterval)) + + // console.log(figure.layout.xaxis?.range, figure.layout.xaxis?.rangeslider?.range, figure.layout.xaxis) + } } - const root: any = document.getElementById('root'); - root.setAttribute('min-timestamp', minTimestamp); - root.setAttribute('max-timestamp', maxTimestamp); - // Use the min/max time found for the readings (and shifted as desired) as the - // x-axis range for the graph. - // Avoid pesky shifting timezones with utc. - const start = moment.utc(minTimestamp).toISOString(); - const end = moment.utc(maxTimestamp).toISOString(); + const handleRelayout = (e: PlotRelayoutEvent) => { + // console.log(e, e['xaxis.range[0]'], e['xaxis.range[1]']) + // This event emits an object that contains values indicating changes in the user's graph, such as zooming. + // These values indicate when the user has zoomed or made other changes to the graph. + if (e['xaxis.range[0]'] && e['xaxis.range[0]']) { + // The event signals changes in the user's interaction with the graph. + // this will automatically trigger a refetch due to updating a query arg. + const startTS = moment.utc(e['xaxis.range[0]']) + const endTS = moment.utc(e['xaxis.range[1]']) + const workingTimeInterval = new TimeInterval(startTS, endTS); + dispatch(graphSlice.actions.updateTimeInterval(workingTimeInterval)); + } + } - let layout: any; + let enoughData = false; + datasets.forEach(dataset => { + if (dataset.x.length > 1) { + enoughData = true + return + } + }) + // console.log(datasets.length, datasets) // Customize the layout of the plot // See https://community.plotly.com/t/replacing-an-empty-graph-with-a-message/31497 for showing text not plot. if (datasets.length === 0) { - // There is not data so tell user. - layout = { - 'xaxis': { - 'visible': false - }, - 'yaxis': { - 'visible': false - }, - 'annotations': [ - { - 'text': `${translate('select.meter.group')}`, - 'xref': 'paper', - 'yref': 'paper', - 'showarrow': false, - 'font': { - 'size': 28 - } - } - ] - } - - } else { + return

+ {`${translate('select.meter.group')}`} +

+ } else if (!enoughData) { // This normal so plot. - layout = { - autosize: true, - showlegend: true, - height: 700, - legend: { - x: 0, - y: 1.1, - orientation: 'h' - }, - yaxis: { - title: unitLabel, - gridcolor: '#ddd' - }, - - xaxis: { - range: [start, end], // Specifies the start and end points of visible part of graph - rangeslider: { - thickness: 0.1 - }, - showgrid: true, - gridcolor: '#ddd' - }, - margin: { - t: 10, - b: 10 - } - }; - } - const config = { - displayModeBar: true, - responsive: true, - locales: Locales // makes locales available for use + return

+ {`${translate('threeD.no.data')}`} +

+ } else { + return ( +
+ +
+ ) } - return ( -
- console.log(e.layout.xaxis?.range, e.layout.xaxis?.rangeslider?.range, e.layout.xaxis?.rangeselector)} - onUpdate={e => console.log(e.layout.xaxis?.range, e.layout.xaxis?.rangeslider?.range, e.layout.xaxis?.rangeselector)} - onRelayout={handleRelayout} - config={config} - style={{ width: '100%', height: '80%' }} - useResizeHandler={true} - /> - {/* Only Show if there's data */ - (datasets.length !== 0) && - <> - - - - } -
- ) } /** @@ -414,6 +338,3 @@ export function getRangeSliderInterval(): string { } -const buttonMargin: React.CSSProperties = { - marginRight: '10px' -}; \ No newline at end of file diff --git a/src/client/app/components/MeterAndGroupSelectComponent.tsx b/src/client/app/components/MeterAndGroupSelectComponent.tsx index a58a13b22..48344ed6b 100644 --- a/src/client/app/components/MeterAndGroupSelectComponent.tsx +++ b/src/client/app/components/MeterAndGroupSelectComponent.tsx @@ -3,18 +3,29 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import Select, { ActionMeta, MultiValue } from 'react-select'; +import Select, { ActionMeta, MultiValue, StylesConfig } from 'react-select'; import makeAnimated from 'react-select/animated'; import { Badge } from 'reactstrap'; -import { GroupedOption, SelectOption } from 'types/items'; -import { graphSlice } from '../reducers/graph'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { selectMeterGroupSelectData } from '../redux/selectors/uiSelectors'; +import { GroupedOption, SelectOption } from '../types/items'; import { MeterOrGroup } from '../types/redux/graph'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; - +import { graphSlice } from '../reducers/graph'; const animatedComponents = makeAnimated(); +const customStyles: StylesConfig = { + valueContainer: base => ({ + ...base, + maxHeight: 150, + overflowY: 'scroll', + '&::-webkit-scrollbar': { + display: 'none' + }, + 'msOverflowStyle': 'none', + 'scrollbarWidth': 'none' + }) +}; /** * Creates a React-Select component for the UI Options Panel. @@ -39,9 +50,9 @@ export default function MeterAndGroupSelectComponent(props: MeterAndGroupSelectP : meterAndGroupSelectOptions.groupsGroupedOptions - const onChange = (newValues: MultiValue, meta: ActionMeta) => { + const onChange = async (newValues: MultiValue, meta: ActionMeta) => { const newMetersOrGroups = newValues.map((option: SelectOption) => option.value); - dispatch(graphSlice.actions.updateSelectedMetersOrGroups({ newMetersOrGroups, meta })) + dispatch(graphSlice.actions.updateSelectedMetersOrGroups({ newMetersOrGroups, meta })); } return ( @@ -61,6 +72,7 @@ export default function MeterAndGroupSelectComponent(props: MeterAndGroupSelectP formatGroupLabel={formatGroupLabel} // Included React-Select Animations components={animatedComponents} + styles={customStyles} />
) diff --git a/src/client/app/components/RouteComponentWIP.tsx b/src/client/app/components/RouteComponentWIP.tsx index b50ca0834..89821352c 100644 --- a/src/client/app/components/RouteComponentWIP.tsx +++ b/src/client/app/components/RouteComponentWIP.tsx @@ -13,10 +13,11 @@ import CreateUserContainer from '../containers/admin/CreateUserContainer'; import UploadCSVContainer from '../containers/csv/UploadCSVContainer'; import MapCalibrationContainer from '../containers/maps/MapCalibrationContainer'; import MapsDetailContainer from '../containers/maps/MapsDetailContainer'; +import { selectCurrentUser } from '../reducers/currentUser'; import { graphSlice } from '../reducers/graph'; import { baseApi } from '../redux/api/baseApi'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; -import { selectCurrentUser, selectIsLoggedInAsAdmin } from '../redux/selectors/authSelectors'; +import { selectIsLoggedInAsAdmin } from '../redux/selectors/authSelectors'; import localeData from '../translations/data'; import { UserRole } from '../types/items'; import { ChartTypes, LineGraphRate, MeterOrGroup } from '../types/redux/graph'; @@ -39,9 +40,6 @@ import UnitsDetailComponent from './unit/UnitsDetailComponent'; */ export default function RouteComponentWIP() { const lang = useAppSelector(state => state.options.selectedLanguage) - - - const messages = (localeData as any)[lang]; return ( <> @@ -200,7 +198,7 @@ const GraphLink = () => { console.log('Todo, FIXME! Maplink not working') break; case 'serverRange': - dispatchQueue.push(graphSlice.actions.changeGraphZoom(TimeInterval.fromString(value))); + dispatchQueue.push(graphSlice.actions.updateTimeInterval(TimeInterval.fromString(value))); /** * commented out since days from present feature is not currently used */ diff --git a/src/client/app/components/ThreeDComponent.tsx b/src/client/app/components/ThreeDComponent.tsx index 6df1708a0..83f135edd 100644 --- a/src/client/app/components/ThreeDComponent.tsx +++ b/src/client/app/components/ThreeDComponent.tsx @@ -52,7 +52,7 @@ export default function ThreeDComponent(props: ThreeDChartProps) { layout = setHelpLayout(translate('select.meter.group')); } else if (graphState.areaNormalization && !isAreaCompatible) { layout = setHelpLayout(`${meterOrGroupName}${translate('threeD.area.incompatible')}`); - } else if (!isValidThreeDInterval(roundTimeIntervalForFetch(graphState.timeInterval))) { + } else if (!isValidThreeDInterval(roundTimeIntervalForFetch(graphState.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/UIOptionsComponent.tsx b/src/client/app/components/UIOptionsComponent.tsx index ffa5a2d78..388fec32d 100644 --- a/src/client/app/components/UIOptionsComponent.tsx +++ b/src/client/app/components/UIOptionsComponent.tsx @@ -6,6 +6,7 @@ import * as React from 'react'; import ReactTooltip from 'react-tooltip'; import ExportComponent from '../components/ExportComponent'; import ChartLinkContainer from '../containers/ChartLinkContainer'; +import { selectChartToRender } from '../reducers/graph'; import { useAppSelector } from '../redux/hooks'; import { ChartTypes } from '../types/redux/graph'; import AreaUnitSelectComponent from './AreaUnitSelectComponent'; @@ -18,13 +19,13 @@ import ErrorBarComponent from './ErrorBarComponent'; import GraphicRateMenuComponent from './GraphicRateMenuComponent'; import MapControlsComponent from './MapControlsComponent'; import ThreeDSelectComponent from './ReadingsPerDaySelectComponent'; -import { graphSlice } from '../reducers/graph'; + /** * @returns the Ui Control panel */ export default function UIOptionsComponent() { - const chartToRender = useAppSelector(state => graphSlice.selectors.chartToRender(state)); + const chartToRender = useAppSelector(state => selectChartToRender(state)); ReactTooltip.rebuild(); return (
diff --git a/src/client/app/components/groups/GroupsDetailComponent.tsx b/src/client/app/components/groups/GroupsDetailComponent.tsx index 258560bc8..f03cbc35c 100644 --- a/src/client/app/components/groups/GroupsDetailComponent.tsx +++ b/src/client/app/components/groups/GroupsDetailComponent.tsx @@ -15,7 +15,7 @@ import { potentialGraphicUnits } from '../../utils/input'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateGroupModalComponent from './CreateGroupModalComponent'; import GroupViewComponent from './GroupViewComponent'; -import { unitsSlice } from '../../reducers/units'; +import { selectUnitDataById } from '../../reducers/units'; /** * Defines the groups page card view @@ -30,7 +30,7 @@ export default function GroupsDetailComponent() { const { visibleGroups } = useAppSelector(state => selectVisibleMetersGroupsDataByID(state)); // Units state - const unitDataById = useAppSelector(state => unitsSlice.selectors.unitDataById(state)); + const unitDataById = useAppSelector(state => selectUnitDataById(state)); // Possible graphic units to use const possibleGraphicUnits = potentialGraphicUnits(unitDataById); diff --git a/src/client/app/components/meters/MetersDetailComponent.tsx b/src/client/app/components/meters/MetersDetailComponent.tsx index 774c80098..f86cfd3c5 100644 --- a/src/client/app/components/meters/MetersDetailComponent.tsx +++ b/src/client/app/components/meters/MetersDetailComponent.tsx @@ -9,7 +9,7 @@ import HeaderComponent from '../../components/HeaderComponent'; import FooterContainer from '../../containers/FooterContainer'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; import { useAppSelector } from '../../redux/hooks'; -import { selectCurrentUser, selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; +import { selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; import { selectVisibleMetersGroupsDataByID } from '../../redux/selectors/dataSelectors'; import '../../styles/card-page.css'; import { MeterData } from '../../types/redux/meters'; @@ -18,7 +18,8 @@ import { noUnitTranslated, potentialGraphicUnits } from '../../utils/input'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateMeterModalComponent from './CreateMeterModalComponent'; import MeterViewComponent from './MeterViewComponent'; -import { unitsSlice } from '../../reducers/units'; +import { selectUnitDataById } from '../../reducers/units'; +import { selectCurrentUser } from '../../reducers/currentUser'; /** * Defines the meters page card view @@ -36,7 +37,7 @@ export default function MetersDetailComponent() { const { visibleMeters } = useAppSelector(state => selectVisibleMetersGroupsDataByID(state)); // Units state - const unitDataById = useAppSelector(state => unitsSlice.selectors.unitDataById(state)); + const unitDataById = useAppSelector(state => selectUnitDataById(state)); // TODO? Convert into Selector? // Possible Meter Units to use diff --git a/src/client/app/components/unit/UnitsDetailComponent.tsx b/src/client/app/components/unit/UnitsDetailComponent.tsx index 724565f2c..b734f22f4 100644 --- a/src/client/app/components/unit/UnitsDetailComponent.tsx +++ b/src/client/app/components/unit/UnitsDetailComponent.tsx @@ -14,7 +14,7 @@ import { State } from '../../types/redux/state'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateUnitModalComponent from './CreateUnitModalComponent'; import UnitViewComponent from './UnitViewComponent'; -import { unitsSlice } from '../../reducers/units'; +import { selectUnitDataById } from '../../reducers/units'; /** * Defines the units page card view @@ -25,7 +25,7 @@ export default function UnitsDetailComponent() { const isUpdatingCikAndDBViews = useSelector((state: State) => state.admin.isUpdatingCikAndDBViews); //Units state - const unitDataById = useAppSelector(state => unitsSlice.selectors.unitDataById(state)); + const unitDataById = useAppSelector(state => selectUnitDataById(state)); const titleStyle: React.CSSProperties = { diff --git a/src/client/app/containers/BarChartContainer.ts b/src/client/app/containers/BarChartContainer.ts index ab7395686..794bb3ba2 100644 --- a/src/client/app/containers/BarChartContainer.ts +++ b/src/client/app/containers/BarChartContainer.ts @@ -23,7 +23,7 @@ import { UnitRepresentType } from '../types/redux/units'; * Returns the props object. */ function mapStateToProps(state: State) { - const timeInterval = state.graph.timeInterval; + const timeInterval = state.graph.queryTimeInterval; const barDuration = state.graph.barDuration; const unitID = state.graph.selectedUnit; const datasets: any[] = []; diff --git a/src/client/app/containers/ChartLinkContainer.ts b/src/client/app/containers/ChartLinkContainer.ts index 4b0c8fd78..33a2f3124 100644 --- a/src/client/app/containers/ChartLinkContainer.ts +++ b/src/client/app/containers/ChartLinkContainer.ts @@ -37,7 +37,7 @@ function mapStateToProps(state: State) { } linkText += `chartType=${state.graph.chartToRender}`; // weeklyLink = linkText + '&serverRange=7dfp'; // dfp: days from present; - linkText += `&serverRange=${state.graph.timeInterval.toString()}`; + linkText += `&serverRange=${state.graph.queryTimeInterval.toString()}`; switch (chartType) { case ChartTypes.bar: linkText += `&barDuration=${state.graph.barDuration.asDays()}`; @@ -46,7 +46,7 @@ function mapStateToProps(state: State) { case ChartTypes.line: // no code for this case // under construction; - // linkText += `&displayRange=${state.graph.timeInterval.toString().split('_')}`; + // linkText += `&displayRange=${state.graph.queryTimeInterval.toString().split('_')}`; break; case ChartTypes.compare: linkText += `&comparePeriod=${state.graph.comparePeriod}`; diff --git a/src/client/app/containers/DashboardContainer.ts b/src/client/app/containers/DashboardContainer.ts index 8c146ca0e..c2d219d4b 100644 --- a/src/client/app/containers/DashboardContainer.ts +++ b/src/client/app/containers/DashboardContainer.ts @@ -24,7 +24,7 @@ function mapStateToProps(state: State) { compareLoading: state.readings.bar.isFetching, mapLoading: state.maps.isLoading, optionsVisibility: state.graph.optionsVisibility, - selectedTimeInterval: state.graph.timeInterval + selectedTimeInterval: state.graph.queryTimeInterval }; } diff --git a/src/client/app/containers/LineChartContainer.ts b/src/client/app/containers/LineChartContainer.ts index 17e0f5e20..7183fe911 100644 --- a/src/client/app/containers/LineChartContainer.ts +++ b/src/client/app/containers/LineChartContainer.ts @@ -15,7 +15,7 @@ import { lineUnitLabel } from '../utils/graphics'; import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; function mapStateToProps(state: State) { - const timeInterval = state.graph.timeInterval; + const timeInterval = state.graph.queryTimeInterval; const unitID = state.graph.selectedUnit; const datasets: any[] = []; // The unit label depends on the unit which is in selectUnit state. diff --git a/src/client/app/containers/MapChartContainer.ts b/src/client/app/containers/MapChartContainer.ts index e34013afa..ad3a832b0 100644 --- a/src/client/app/containers/MapChartContainer.ts +++ b/src/client/app/containers/MapChartContainer.ts @@ -48,7 +48,7 @@ function mapStateToProps(state: State) { const y: number[] = []; // Figure out what time interval the bar is using since user bar data for now. - const timeInterval = state.graph.timeInterval; + 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) { diff --git a/src/client/app/reducers/currentUser.ts b/src/client/app/reducers/currentUser.ts index 21d9d9eb9..9dfb9f476 100644 --- a/src/client/app/reducers/currentUser.ts +++ b/src/client/app/reducers/currentUser.ts @@ -4,10 +4,10 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; +import { authApi } from '../redux/api/authApi'; +import { userApi } from '../redux/api/userApi'; import { User } from '../types/items'; import { CurrentUserState } from '../types/redux/currentUser'; -import { userApi } from '../redux/api/userApi'; -import { authApi } from '../redux/api/authApi'; import { setToken } from '../utils/token'; /* @@ -51,5 +51,11 @@ export const currentUserSlice = createSlice({ state.token = api.payload.token setToken(state.token) }) + }, + selectors: { + selectCurrentUser: state => state + // Should resolve to a boolean, Typescript doesn't agree so type assertion 'as boolean' } -}) \ No newline at end of file +}) + +export const { selectCurrentUser } = currentUserSlice.selectors \ No newline at end of file diff --git a/src/client/app/reducers/graph.ts b/src/client/app/reducers/graph.ts index 4a82d32f8..bd3d9c9d8 100644 --- a/src/client/app/reducers/graph.ts +++ b/src/client/app/reducers/graph.ts @@ -17,7 +17,8 @@ const defaultState: GraphState = { selectedGroups: [], selectedUnit: -99, selectedAreaUnit: AreaUnitType.none, - timeInterval: TimeInterval.unbounded(), + queryTimeInterval: TimeInterval.unbounded(), + workingTimeInterval: TimeInterval.unbounded(), rangeSliderInterval: TimeInterval.unbounded(), barDuration: moment.duration(4, 'weeks'), comparePeriod: ComparePeriod.Week, @@ -69,8 +70,11 @@ export const graphSlice = createSlice({ updateBarDuration: (state, action: PayloadAction) => { state.barDuration = action.payload }, - changeGraphZoom: (state, action: PayloadAction) => { - state.timeInterval = action.payload + updateTimeInterval: (state, action: PayloadAction) => { + state.queryTimeInterval = action.payload + }, + updateWorkingTimeInterval: (state, action: PayloadAction) => { + state.workingTimeInterval = action.payload }, changeSliderRange: (state, action: PayloadAction) => { state.rangeSliderInterval = action.payload @@ -211,29 +215,46 @@ export const graphSlice = createSlice({ state.threeD.meterOrGroup = undefined } + }, + resetTimeInterval: state => { + if (!state.queryTimeInterval.equals(TimeInterval.unbounded())) { + state.queryTimeInterval = TimeInterval.unbounded() + state.workingTimeInterval = TimeInterval.unbounded() + } } }, extraReducers: builder => { - builder.addMatcher(preferencesApi.endpoints.getPreferences.matchFulfilled, - (state, action) => { - if (state.selectedAreaUnit == AreaUnitType.none) { - state.selectedAreaUnit = action.payload.defaultAreaUnit - } - }) + builder.addMatcher(preferencesApi.endpoints.getPreferences.matchFulfilled, (state, action) => { + if (state.selectedAreaUnit === AreaUnitType.none) { + state.selectedAreaUnit = action.payload.defaultAreaUnit; + } + }) }, // New Feature as of 2.0.0 Beta. selectors: { - threeDState: state => state.threeD, - barWidthDays: state => state.barDuration, - graphState: state => state, - selectedMeters: state => state.selectedMeters, - selectedGroups: state => state.selectedGroups, - graphTimeInterval: state => state.timeInterval, - graphUnitID: state => state.selectedUnit, - graphAreaNormalization: state => state.areaNormalization, - chartToRender: state => state.chartToRender, - threeDMeterOrGroup: state => state.threeD.meterOrGroup, - threeDMeterOrGroupID: state => state.threeD.meterOrGroupID, - threeDReadingInterval: state => state.threeD.readingInterval + selectThreeDState: state => state.threeD, + selectBarWidthDays: state => state.barDuration, + selectGraphState: state => state, + selectSelectedMeters: state => state.selectedMeters, + selectSelectedGroups: state => state.selectedGroups, + selectQueryTimeInterval: state => state.queryTimeInterval, + selectWorkingTimeInterval: state => state.workingTimeInterval, + selectGraphUnitID: state => state.selectedUnit, + selectGraphAreaNormalization: state => state.areaNormalization, + selectChartToRender: state => state.chartToRender, + selectThreeDMeterOrGroup: state => state.threeD.meterOrGroup, + selectThreeDMeterOrGroupID: state => state.threeD.meterOrGroupID, + selectThreeDReadingInterval: state => state.threeD.readingInterval } }) + +// Selectors that can be imported and used in 'useAppSelectors' and 'createSelectors' +export const { + selectThreeDState, selectBarWidthDays, + selectGraphState, selectSelectedMeters, + selectSelectedGroups, selectQueryTimeInterval, + selectWorkingTimeInterval, selectGraphUnitID, + selectGraphAreaNormalization, selectChartToRender, + selectThreeDMeterOrGroup, selectThreeDMeterOrGroupID, + selectThreeDReadingInterval +} = graphSlice.selectors \ No newline at end of file diff --git a/src/client/app/reducers/groups.ts b/src/client/app/reducers/groups.ts index 08d30700c..eff777277 100644 --- a/src/client/app/reducers/groups.ts +++ b/src/client/app/reducers/groups.ts @@ -137,8 +137,9 @@ export const groupsSlice = createSlice({ }, selectors: { - groupState: state => state, - groupDataByID: state => state.byGroupID - + selectGroupState: state => state, + selectGroupDataByID: state => state.byGroupID } -}); \ No newline at end of file +}) + +export const { selectGroupDataByID, selectGroupState } = groupsSlice.selectors \ No newline at end of file diff --git a/src/client/app/reducers/meters.ts b/src/client/app/reducers/meters.ts index 5a17fa366..a1548ba92 100644 --- a/src/client/app/reducers/meters.ts +++ b/src/client/app/reducers/meters.ts @@ -58,8 +58,10 @@ export const metersSlice = createSlice({ ) }, selectors: { - meterState: state => state, - meterDataByID: state => state.byMeterID + selectMeterState: state => state, + selectMeterDataByID: state => state.byMeterID } -}); \ No newline at end of file +}); + +export const { selectMeterState, selectMeterDataByID } = metersSlice.selectors \ No newline at end of file diff --git a/src/client/app/reducers/units.ts b/src/client/app/reducers/units.ts index b1004ad85..22df566e1 100644 --- a/src/client/app/reducers/units.ts +++ b/src/client/app/reducers/units.ts @@ -48,8 +48,9 @@ export const unitsSlice = createSlice({ ) }, selectors: { - unitsState: state => state, - unitDataById: state => state.units - + selectUnitsState: state => state, + selectUnitDataById: state => state.units } -}); \ No newline at end of file +}); + +export const { selectUnitsState, selectUnitDataById } = unitsSlice.selectors \ No newline at end of file diff --git a/src/client/app/redux/api/readingsApi.ts b/src/client/app/redux/api/readingsApi.ts index 47a0e9046..f2d864c56 100644 --- a/src/client/app/redux/api/readingsApi.ts +++ b/src/client/app/redux/api/readingsApi.ts @@ -1,4 +1,6 @@ +import * as _ from 'lodash'; import { BarReadingApiArgs, LineReadingApiArgs } from '../../redux/selectors/dataSelectors'; +import { RootState } from '../../store'; import { BarReadings, LineReadings, ThreeDReading } from '../../types/readings'; import { MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; import { baseApi } from './baseApi'; @@ -15,23 +17,109 @@ export type ThreeDReadingApiParams = { export const readingsApi = baseApi.injectEndpoints({ endpoints: builder => ({ + // threeD: the queryEndpoint name // builder.query threeD: builder.query({ + // ThreeD request only single meters at a time which plays well with default cache behavior + // No other properties are necessary for this endpoint + // Refer to the line endpoint for an example of an endpoint with custom cache behavior query: ({ meterOrGroupID, timeInterval, unitID, readingInterval, meterOrGroup }) => { + // destructure args that are passed into the callback, and generate the API url for the request. const endpoint = `api/unitReadings/threeD/${meterOrGroup}/` const args = `${meterOrGroupID}?timeInterval=${timeInterval.toString()}&graphicUnitId=${unitID}&readingInterval=${readingInterval}` return `${endpoint}${args}` } }), + // line: the queryEndpoint name // builder.query line: builder.query({ - query: ({ ids, timeInterval, graphicUnitID, meterOrGroup }) => { - return `api/unitReadings/line/${meterOrGroup}/${ids.join(',')}?timeInterval=${timeInterval}&graphicUnitId=${graphicUnitID}` + // To see another example of (serializeQueryArgs, merge, forceRefetch) being used in tandem to customize cache behavior refer to: + // Example for merge https://redux-toolkit.js.org/rtk-query/api/createApi#merge + + // Customize Cache Behavior by utilizing (serializeQueryArgs, merge, forceRefetch) + serializeQueryArgs: ({ queryArgs }) => { + // Modify the default serialization behavior to better suit our use case, to avoid querying already cached data. + // We omit the ids so that any query with the same timeInterval,GraphicUnitId, and meterOrGroup will hit the same cache + // if we didn't omit id's there would be separate cache entries for queries with ids [1], [1,2], [1,2,3], [1,3], etc.. + // an entry fore each means requesting the same data again for ALL meters. which results in too much duplicate data requests + + // We keep all args other than the ids. + return _.omit(queryArgs, 'ids') + }, + merge: (currentCacheData, responseData) => { + // By default subsequent queries that resolve to the same cache entry will overwrite the existing data. + // For our use case, many queries will point to the same resolved cache, therefore we must provide merge behavior to not lose data + return _.merge(currentCacheData, responseData) + }, + forceRefetch: ({ currentArg, endpointState }) => { + // Since we modified the way the we serialize the args any subsequent query would return the cache data, even if new meters were requested + // To resolve this we provide a forceRefetch where we decide if data needs to be fetched, or retrieved from the cache. + + // check if there is data in the endpointState, + const currentData = endpointState?.data ? Object.keys(endpointState.data).map(Number) : undefined + if (!currentData) { + // No data, so force fetch + return true + } + // check if the requested id's already exist in cache + const dataInCache = currentArg?.ids.every(id => currentData.includes(id)) + + // if data requested already lives in the cache, no fetch necessary, else fetch for data + return dataInCache ? false : true + }, + queryFn: async (args, queryApi, _extra, baseQuery) => { + // We opt for a query function here instead of the normal query: args => {....} + // In a queryFn, we can reference the store's state, to manipulate the provided query args + // The query can request multiple ids, but we may already have some data cached, so only request the necessary ids. + + // use the query api to get the store's state, (Type Assertion necessary for typescript otherwise, 'unknown') + const state = queryApi.getState() as RootState + // get cache data utilizing the readings Api endpoint + // Refer to: https://redux-toolkit.js.org/rtk-query/api/created-api/endpoints#select + const cachedData = readingsApi.endpoints.line.select(args)(state).data + // map cache keys to a number array, if any + const cachedIDs = cachedData ? Object.keys(cachedData).map(Number) : [] + // get the args provided in the original request + const { ids, timeInterval, graphicUnitID, meterOrGroup } = args + // subtract any already cached keys from the requested ids, and stringify the array for the url endpoint + const idsToFetch = _.difference(ids, cachedIDs).join(',') + + // api url from derived request arguments + const endpointURL = `api/unitReadings/line/${meterOrGroup}/${idsToFetch}?timeInterval=${timeInterval}&graphicUnitId=${graphicUnitID}` + + // use the baseQuery from the queryFn with our url endpoint + const { data, error } = await baseQuery(endpointURL) + + // https://redux-toolkit.js.org/rtk-query/usage/customizing-queries#implementing-a-queryfn + // queryFn requires either a data, or error object to be returned + if (error) { + return { error } + } + // since we define custom merge behavior, incoming data will merge with the existing cache + return { data: data as LineReadings } + } }), bar: builder.query({ - query: ({ ids, timeInterval, graphicUnitID, meterOrGroup, barWidthDays }) => { - const endpoint = `api/unitReadings/bar/${meterOrGroup}/${ids.join(',')}` - const args = `?timeInterval=${timeInterval}&barWidthDays=${barWidthDays}&graphicUnitId=${graphicUnitID}` - return `${endpoint}${args}` + // Refer to line endpoint for detailed explanation as the logic is identical + serializeQueryArgs: ({ queryArgs }) => _.omit(queryArgs, 'ids'), + merge: (currentCacheData, responseData) => _.merge(currentCacheData, responseData), + forceRefetch: ({ currentArg, endpointState }) => { + const currentData = endpointState?.data ? Object.keys(endpointState.data).map(Number) : undefined + if (!currentData) { return true } + const dataInCache = currentArg?.ids.every(id => currentData.includes(id)) + return !dataInCache ? true : false + }, + queryFn: async (args, queryApi, _extra, baseQuery) => { + const { ids, timeInterval, graphicUnitID, meterOrGroup, barWidthDays } = args + const state = queryApi.getState() as RootState + const cachedData = readingsApi.endpoints.bar.select(args)(state).data + const cachedIDs = cachedData ? Object.keys(cachedData).map(Number) : [] + const idsToFetch = _.difference(ids, cachedIDs).join(',') + const endpoint = `api/unitReadings/bar/${meterOrGroup}/${idsToFetch}?` + const queryArgs = `timeInterval=${timeInterval}&barWidthDays=${barWidthDays}&graphicUnitId=${graphicUnitID}` + const endpointURL = `${endpoint}${queryArgs}` + const { data, error } = await baseQuery(endpointURL) + if (error) { return { error } } + return { data: data as LineReadings } } }) }) diff --git a/src/client/app/redux/selectors/authSelectors.ts b/src/client/app/redux/selectors/authSelectors.ts index 0d411a1a4..92a34e4e0 100644 --- a/src/client/app/redux/selectors/authSelectors.ts +++ b/src/client/app/redux/selectors/authSelectors.ts @@ -1,8 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; -import { RootState } from '../../store'; import { UserRole } from '../../types/items'; - -export const selectCurrentUser = (state: RootState) => state.currentUser; +import { selectCurrentUser } from '../../reducers/currentUser' // Memoized Selectors for stable obj reference from derived Values export const selectIsLoggedInAsAdmin = createSelector( diff --git a/src/client/app/redux/selectors/dataSelectors.ts b/src/client/app/redux/selectors/dataSelectors.ts index b03740155..004c26edf 100644 --- a/src/client/app/redux/selectors/dataSelectors.ts +++ b/src/client/app/redux/selectors/dataSelectors.ts @@ -1,8 +1,8 @@ import { createSelector } from '@reduxjs/toolkit'; import * as _ from 'lodash'; -import { graphSlice } from '../../reducers/graph'; -import { groupsSlice } from '../../reducers/groups'; -import { metersSlice } from '../../reducers/meters'; +import { selectGraphState } from '../../reducers/graph'; +import { selectGroupDataByID } from '../../reducers/groups'; +import { selectMeterDataByID } from '../../reducers/meters'; import { ThreeDReadingApiParams } from '../../redux/api/readingsApi'; import { MeterOrGroup } from '../../types/redux/graph'; import { GroupDefinition } from '../../types/redux/groups'; @@ -10,74 +10,58 @@ import { MeterData } from '../../types/redux/meters'; import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; import { selectIsLoggedInAsAdmin } from './authSelectors'; -export const selectVisibleMetersGroupsDataByID = createSelector( - metersSlice.selectors.meterDataByID, - groupsSlice.selectors.groupDataByID, - selectIsLoggedInAsAdmin, - (meterDataByID, groupDataByID, isAdmin) => { - let visibleMeters; - let visibleGroups; - if (isAdmin) { - visibleMeters = meterDataByID - visibleGroups = groupDataByID; - } else { - visibleMeters = _.filter(meterDataByID, (meter: MeterData) => { - return meter.displayable === true - }); - visibleGroups = _.filter(groupDataByID, (group: GroupDefinition) => { - return group.displayable === true - }); - } - - return { visibleMeters, visibleGroups } - } -) -// line/meters/10,11,12?timeInterval=2020-05-02T14:04:36Z_2020-09-08T15:00:00Z&graphicUnitId=1 -// bar/meters/21,22,10,18?timeInterval=2020-05-02T14:04:36Z_2020-09-08T15:00:00Z&barWidthDays=28&graphicUnitId=1 +// Props that are passed to plotly components +export interface ChartQueryProps { + queryProps: ChartQueryArgs +} export interface ChartQueryArgs { meterArgs: T groupsArgs: T + meterSkipQuery: boolean + groupSkipQuery: boolean } -export interface ChartQueryProps { - queryProps: ChartQueryArgs -} - +// query args that all graphs share export interface commonArgs { ids: number[]; timeInterval: string; graphicUnitID: number; meterOrGroup: MeterOrGroup; } - +// endpoint specific args export interface LineReadingApiArgs extends commonArgs { } export interface BarReadingApiArgs extends commonArgs { barWidthDays: number } +// Selector prepares the query args for each endpoint based on the current graph slice state export const selectChartQueryArgs = createSelector( - graphSlice.selectors.graphState, + selectGraphState, graphState => { + // args that all meters queries share const baseMeterArgs: commonArgs = { - // Sort the arrays immutably. Sorting the arrays helps with cache hits. - ids: [...graphState.selectedMeters].sort(), - timeInterval: graphState.timeInterval.toString(), + ids: graphState.selectedMeters, + timeInterval: graphState.queryTimeInterval.toString(), graphicUnitID: graphState.selectedUnit, meterOrGroup: MeterOrGroup.meters } + // args that all groups queries share const baseGroupArgs: commonArgs = { - // Sort the arrays immutably. Sorting the arrays helps with cache hits. - ids: [...graphState.selectedGroups].sort(), - timeInterval: graphState.timeInterval.toString(), + ids: graphState.selectedGroups, + timeInterval: graphState.queryTimeInterval.toString(), graphicUnitID: graphState.selectedUnit, meterOrGroup: MeterOrGroup.groups } + // props to pass into the line chart component const line: ChartQueryArgs = { meterArgs: baseMeterArgs, - groupsArgs: baseGroupArgs + groupsArgs: baseGroupArgs, + meterSkipQuery: !baseMeterArgs.ids.length, + groupSkipQuery: !baseGroupArgs.ids.length } + // props to pass into the bar chart component const bar: ChartQueryArgs = { meterArgs: { ...baseMeterArgs, @@ -86,22 +70,46 @@ export const selectChartQueryArgs = createSelector( groupsArgs: { ...baseGroupArgs, barWidthDays: Math.round(graphState.barDuration.asDays()) - } + }, + meterSkipQuery: !baseMeterArgs.ids.length, + groupSkipQuery: !baseGroupArgs.ids.length } const threeD = { args: { meterOrGroupID: graphState.threeD.meterOrGroupID, - timeInterval: roundTimeIntervalForFetch(graphState.timeInterval).toString(), + timeInterval: roundTimeIntervalForFetch(graphState.queryTimeInterval).toString(), unitID: graphState.selectedUnit, readingInterval: graphState.threeD.readingInterval, meterOrGroup: graphState.threeD.meterOrGroup } as ThreeDReadingApiParams, - skip: !graphState.threeD.meterOrGroupID || !graphState.timeInterval.getIsBounded() + skip: !graphState.threeD.meterOrGroupID || !graphState.queryTimeInterval.getIsBounded() } return { line, bar, threeD } } ) +export const selectVisibleMetersGroupsDataByID = createSelector( + selectMeterDataByID, + selectGroupDataByID, + selectIsLoggedInAsAdmin, + (meterDataByID, groupDataByID, isAdmin) => { + let visibleMeters; + let visibleGroups; + if (isAdmin) { + visibleMeters = meterDataByID + visibleGroups = groupDataByID; + } else { + visibleMeters = _.filter(meterDataByID, (meter: MeterData) => { + return meter.displayable === true + }); + visibleGroups = _.filter(groupDataByID, (group: GroupDefinition) => { + return group.displayable === true + }); + } + + return { visibleMeters, visibleGroups } + } +) \ No newline at end of file diff --git a/src/client/app/redux/selectors/threeDSelectors.ts b/src/client/app/redux/selectors/threeDSelectors.ts index 6e77ccdc6..925afe10b 100644 --- a/src/client/app/redux/selectors/threeDSelectors.ts +++ b/src/client/app/redux/selectors/threeDSelectors.ts @@ -1,21 +1,21 @@ import { createSelector } from '@reduxjs/toolkit'; -import { graphSlice } from '../../reducers/graph'; -import { groupsSlice } from '../../reducers/groups'; -import { metersSlice } from '../../reducers/meters'; +import { + selectGraphUnitID, + selectQueryTimeInterval, + selectThreeDMeterOrGroup, selectThreeDMeterOrGroupID, + selectThreeDReadingInterval +} from '../../reducers/graph'; +import { selectGroupState } from '../../reducers/groups'; +import { selectMeterState } from '../../reducers/meters'; import { MeterOrGroup } from '../../types/redux/graph'; import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; import { ThreeDReadingApiParams } from '../api/readingsApi'; -// Common Fine Grained selectors -const { threeDMeterOrGroup, threeDMeterOrGroupID, threeDReadingInterval } = graphSlice.selectors; -const { graphTimeInterval, graphUnitID } = graphSlice.selectors; -const { meterState } = metersSlice.selectors; -const { groupState } = groupsSlice.selectors; // Memoized Selectors export const selectThreeDComponentInfo = createSelector( - [threeDMeterOrGroupID, threeDMeterOrGroup, meterState, groupState], + [selectThreeDMeterOrGroupID, selectThreeDMeterOrGroup, selectMeterState, selectGroupState], (id, meterOrGroup, meterData, groupData) => { //Default Values let meterOrGroupName = 'Unselected Meter or Group' @@ -45,11 +45,11 @@ export const selectThreeDComponentInfo = createSelector( ) export const selectThreeDQueryArgs = createSelector( - threeDMeterOrGroupID, - graphTimeInterval, - graphUnitID, - threeDReadingInterval, - threeDMeterOrGroup, + selectThreeDMeterOrGroupID, + selectQueryTimeInterval, + selectGraphUnitID, + selectThreeDReadingInterval, + selectThreeDMeterOrGroup, (id, timeInterval, unitID, readingInterval, meterOrGroup) => { return { meterOrGroupID: id, @@ -62,7 +62,7 @@ export const selectThreeDQueryArgs = createSelector( ) export const selectThreeDSkip = createSelector( - threeDMeterOrGroupID, - graphTimeInterval, + selectThreeDMeterOrGroupID, + selectQueryTimeInterval, (id, interval) => !id || !interval.getIsBounded() ) \ 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 2a72baa65..92f210858 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -5,11 +5,7 @@ import { createSelector } from '@reduxjs/toolkit'; import * as _ from 'lodash'; import { instanceOfGroupsState, instanceOfMetersState, instanceOfUnitsState } from '../../components/ChartDataSelectComponent'; -import { graphSlice } from '../../reducers/graph'; -import { groupsSlice } from '../../reducers/groups'; import { selectMapState } from '../../reducers/maps'; -import { metersSlice } from '../../reducers/meters'; -import { unitsSlice } from '../../reducers/units'; import { DataType } from '../../types/Datasources'; import { GroupedOption, SelectOption } from '../../types/items'; import { ChartTypes, MeterOrGroup } from '../../types/redux/graph'; @@ -22,19 +18,20 @@ import { } from '../../utils/calibration'; import { metersInGroup, unitsCompatibleWithMeters } from '../../utils/determineCompatibleUnits'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; -import { selectCurrentUser } from './authSelectors'; -// Destruct selectors from Slices (rtk5.0.2Beta) -// Selectors will be used as arguments for the Create Selectors. -// Ensure these selectors always return a stable reference. -const { meterState } = metersSlice.selectors; -const { groupState } = groupsSlice.selectors; -const { graphUnitID, selectedMeters, selectedGroups, chartToRender, graphAreaNormalization } = graphSlice.selectors; -const { unitsState } = unitsSlice.selectors; +import { selectCurrentUser } from '../../reducers/currentUser'; +import { + selectChartToRender, selectGraphAreaNormalization, selectGraphUnitID, + selectQueryTimeInterval, selectSelectedGroups, selectSelectedMeters +} from '../../reducers/graph'; +import { selectGroupState } from '../../reducers/groups'; +import { selectMeterState } from '../../reducers/meters'; +import { selectUnitsState } from '../../reducers/units'; + export const selectVisibleMetersAndGroups = createSelector( - meterState, - groupState, + selectMeterState, + selectGroupState, selectCurrentUser, (meterState, groupState, currentUser) => { // Holds all meters visible to the user @@ -69,12 +66,10 @@ export const selectVisibleMetersAndGroups = createSelector( ); export const selectCurrentUnitCompatibility = createSelector( - [ - selectVisibleMetersAndGroups, - meterState, - groupState, - graphUnitID - ], + selectVisibleMetersAndGroups, + selectMeterState, + selectGroupState, + selectGraphUnitID, (visible, meterState, groupState, graphUnitID) => { // meters and groups that can graph const compatibleMeters = new Set(); @@ -146,14 +141,12 @@ export const selectCurrentUnitCompatibility = createSelector( ) export const selectCurrentAreaCompatibility = createSelector( - [ - selectCurrentUnitCompatibility, - graphAreaNormalization, - graphUnitID, - meterState, - groupState, - unitsState - ], + selectCurrentUnitCompatibility, + selectGraphAreaNormalization, + selectGraphUnitID, + selectMeterState, + selectGroupState, + selectUnitsState, (currentUnitCompatibility, areaNormalization, unitID, meterState, groupState, unitState) => { // Deep Copy previous selector's values, and update as needed based on current Area Normalization setting const compatibleMeters = new Set(currentUnitCompatibility.compatibleMeters); @@ -197,9 +190,9 @@ export const selectCurrentAreaCompatibility = createSelector( export const selectChartTypeCompatibility = createSelector( selectCurrentAreaCompatibility, - chartToRender, - meterState, - groupState, + selectChartToRender, + selectMeterState, + selectGroupState, selectMapState, (areaCompat, chartToRender, meterState, groupState, mapState) => { // Deep Copy previous selector's values, and update as needed based on current ChartType(s) @@ -284,13 +277,11 @@ export const selectChartTypeCompatibility = createSelector( ) export const selectMeterGroupSelectData = createSelector( - [ - selectChartTypeCompatibility, - meterState, - groupState, - selectedMeters, - selectedGroups - ], + selectChartTypeCompatibility, + selectMeterState, + selectGroupState, + selectSelectedMeters, + selectSelectedGroups, (chartTypeCompatibility, meterState, groupState, selectedMeters, selectedGroups) => { // Destructure Previous Selectors's values const { compatibleMeters, incompatibleMeters, compatibleGroups, incompatibleGroups } = chartTypeCompatibility; @@ -346,7 +337,7 @@ export const selectMeterGroupSelectData = createSelector( * @returns an array of UnitData */ export const selectVisibleUnitOrSuffixState = createSelector( - unitsState, + selectUnitsState, selectCurrentUser, (unitState, currentUser) => { let visibleUnitsOrSuffixes; @@ -367,13 +358,11 @@ export const selectVisibleUnitOrSuffixState = createSelector( ) export const selectUnitSelectData = createSelector( - [ - unitsState, - selectVisibleUnitOrSuffixState, - selectedMeters, - selectedGroups, - graphAreaNormalization - ], + selectUnitsState, + selectVisibleUnitOrSuffixState, + selectSelectedMeters, + selectSelectedGroups, + selectGraphAreaNormalization, (unitState, visibleUnitsOrSuffixes, selectedMeters, selectedGroups, areaNormalization) => { // Holds all units that are compatible with selected meters/groups const compatibleUnits = new Set(); @@ -448,7 +437,7 @@ export const selectUnitSelectData = createSelector( * @returns Two Lists: Compatible, and Incompatible selectOptions for use as grouped React-Select options */ export function getSelectOptionsByItem(compatibleItems: Set, incompatibleItems: Set, state: UnitsState | MetersState | GroupsState) { - // TODO Refactor origina + // TODO Refactor original // redefined here for testing. // Holds the label of the select item, set dynamically according to the type of item passed in @@ -536,7 +525,7 @@ export function getSelectOptionsByItem(compatibleItems: Set, incompatibl } export const selectDateRangeInterval = createSelector( - graphSlice.selectors.graphTimeInterval, + selectQueryTimeInterval, timeInterval => { return timeInterval } diff --git a/src/client/app/types/redux/graph.ts b/src/client/app/types/redux/graph.ts index a66521c80..45c1bb5be 100644 --- a/src/client/app/types/redux/graph.ts +++ b/src/client/app/types/redux/graph.ts @@ -66,7 +66,6 @@ export interface GraphState { selectedGroups: number[]; selectedUnit: number; selectedAreaUnit: AreaUnitType; - timeInterval: TimeInterval; rangeSliderInterval: TimeInterval; barDuration: moment.Duration; comparePeriod: ComparePeriod; @@ -80,4 +79,11 @@ export interface GraphState { renderOnce: boolean; showMinMax: boolean; threeD: ThreeDState; + // Time interval that is used to query for data (either definite or TimeInterval.unbounded()) + queryTimeInterval: TimeInterval; + // Time Interval that handles the ''effect'' of querying an unbounded() time interval + // Querying a time interval returns the entire meter's readings with is our working time interval. + // E.X. query(unbounded) returned readings(working time interval): 12-01-01 => 12-01-02. This working time interval is initially an unknown + // On initial render, or parsing of the data returned, Set the returned data's max and min time intervals to be the current workingTimeInterval. + workingTimeInterval: TimeInterval; } From 12e022e23d6b8fb49989a46d8f083ae285ff63c7 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Mon, 16 Oct 2023 04:14:55 +0000 Subject: [PATCH 026/131] LoadingHook --- .../app/components/BarChartComponent.tsx | 5 +- .../app/components/DateRangeComponent.tsx | 2 +- .../app/components/LineChartComponent.tsx | 10 ++- .../MeterAndGroupSelectComponent.tsx | 37 +++++----- .../app/components/UnitSelectComponent.tsx | 6 ++ src/client/app/redux/componentHooks.ts | 72 +++++++++++++++++++ src/client/app/redux/hooks.ts | 2 +- src/client/app/redux/selectors/uiSelectors.ts | 2 +- 8 files changed, 109 insertions(+), 27 deletions(-) create mode 100644 src/client/app/redux/componentHooks.ts diff --git a/src/client/app/components/BarChartComponent.tsx b/src/client/app/components/BarChartComponent.tsx index 2b1ebb5c1..0582ab155 100644 --- a/src/client/app/components/BarChartComponent.tsx +++ b/src/client/app/components/BarChartComponent.tsx @@ -47,8 +47,8 @@ export default function BarChartComponent(props: ChartQueryProps groupsSlice.selectors.selectGroupDataByID(state)); // useQueryHooks for data fetching - const { data: meterReadings, isFetching: meterIsFetching } = readingsApi.useBarQuery(meterArgs, { skip: meterSkipQuery }); - const { data: groupData, isFetching: groupIsFetching } = readingsApi.useBarQuery(groupsArgs, { skip: groupSkipQuery }); + const { data: meterReadings, isLoading: meterIsFetching } = readingsApi.useBarQuery(meterArgs, { skip: meterSkipQuery }); + const { data: groupData, isLoading: groupIsFetching } = readingsApi.useBarQuery(groupsArgs, { skip: groupSkipQuery }); const datasets = []; if (meterIsFetching || groupIsFetching) { @@ -206,6 +206,7 @@ export default function BarChartComponent(props: ChartQueryProps groupsSlice.selectors.selectGroupDataByID(state)); // dataFetching Query Hooks - const { data: meterReadings, isFetching: meterIsFetching } = readingsApi.useLineQuery(meterArgs, { skip: meterSkipQuery }); - const { data: groupData, isFetching: groupIsFetching } = readingsApi.useLineQuery(groupsArgs, { skip: groupSkipQuery }); + const { data: meterReadings, isLoading: meterIsFetching } = readingsApi.useLineQuery(meterArgs, { skip: meterSkipQuery }); + const { data: groupData, isLoading: groupIsFetching } = readingsApi.useLineQuery(groupsArgs, { skip: groupSkipQuery }); const datasets = []; @@ -226,19 +226,15 @@ export default function LineChartComponent(props: ChartQueryProps { - // dispatch(graphSlice.actions.updateWorkingTimeInterval()) if (figure.layout.xaxis?.range) { const startTS = moment.utc(figure.layout.xaxis?.range[0]) const endTS = moment.utc(figure.layout.xaxis?.range[1]) const workingTimeInterval = new TimeInterval(startTS, endTS); dispatch(graphSlice.actions.updateWorkingTimeInterval(workingTimeInterval)) - - // console.log(figure.layout.xaxis?.range, figure.layout.xaxis?.rangeslider?.range, figure.layout.xaxis) } } const handleRelayout = (e: PlotRelayoutEvent) => { - // console.log(e, e['xaxis.range[0]'], e['xaxis.range[1]']) // This event emits an object that contains values indicating changes in the user's graph, such as zooming. // These values indicate when the user has zoomed or made other changes to the graph. if (e['xaxis.range[0]'] && e['xaxis.range[0]']) { @@ -248,6 +244,8 @@ export default function LineChartComponent(props: ChartQueryProps = { - valueContainer: base => ({ - ...base, - maxHeight: 150, - overflowY: 'scroll', - '&::-webkit-scrollbar': { - display: 'none' - }, - 'msOverflowStyle': 'none', - 'scrollbarWidth': 'none' - }) -}; + /** * Creates a React-Select component for the UI Options Panel. @@ -35,6 +24,7 @@ const customStyles: StylesConfig = { export default function MeterAndGroupSelectComponent(props: MeterAndGroupSelectProps) { const dispatch = useAppDispatch(); const meterAndGroupSelectOptions = useAppSelector(state => selectMeterGroupSelectData(state)); + const { somethingIsFetching } = getFetchingStates(); const { meterOrGroup } = props; // Set the current component's appropriate meter or group update from the graphSlice's Payload-Action Creator @@ -50,7 +40,7 @@ export default function MeterAndGroupSelectComponent(props: MeterAndGroupSelectP : meterAndGroupSelectOptions.groupsGroupedOptions - const onChange = async (newValues: MultiValue, meta: ActionMeta) => { + const onChange = (newValues: MultiValue, meta: ActionMeta) => { const newMetersOrGroups = newValues.map((option: SelectOption) => option.value); dispatch(graphSlice.actions.updateSelectedMetersOrGroups({ newMetersOrGroups, meta })); } @@ -73,6 +63,7 @@ export default function MeterAndGroupSelectComponent(props: MeterAndGroupSelectP // Included React-Select Animations components={animatedComponents} styles={customStyles} + isLoading={somethingIsFetching} />
) @@ -103,4 +94,18 @@ const divBottomPadding: React.CSSProperties = { const labelStyle: React.CSSProperties = { fontWeight: 'bold', margin: 0 -}; \ No newline at end of file +}; +const animatedComponents = makeAnimated(); +const customStyles: StylesConfig = { + valueContainer: base => ({ + ...base, + maxHeight: 150, + overflowY: 'scroll', + '&::-webkit-scrollbar': { + display: 'none' + }, + 'msOverflowStyle': 'none', + 'scrollbarWidth': 'none' + }) +}; + diff --git a/src/client/app/components/UnitSelectComponent.tsx b/src/client/app/components/UnitSelectComponent.tsx index 2eb1ec808..824eb1a4b 100644 --- a/src/client/app/components/UnitSelectComponent.tsx +++ b/src/client/app/components/UnitSelectComponent.tsx @@ -13,6 +13,8 @@ import { Badge } from 'reactstrap'; import { graphSlice } from '../reducers/graph'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; +import { getFetchingStates } from '../redux/componentHooks'; + /** * @returns A React-Select component for UI Options Panel @@ -22,6 +24,8 @@ export default function UnitSelectComponent() { const unitSelectOptions = useAppSelector(state => selectUnitSelectData(state)); const selectedUnitID = useAppSelector(state => state.graph.selectedUnit); const unitsByID = useAppSelector(state => state.units.units); + const { endpointsFetchingData } = getFetchingStates(); + let selectedUnitOption: SelectOption | null = null; // Only use if valid/selected unit which means it is not -99. @@ -33,6 +37,7 @@ export default function UnitSelectComponent() { isDisabled: false } as SelectOption; } + const onChange = (newValue: SelectOption) => dispatch(graphSlice.actions.updateSelectedUnit(newValue?.value)) return ( @@ -48,6 +53,7 @@ export default function UnitSelectComponent() { onChange={onChange} formatGroupLabel={formatGroupLabel} isClearable + isLoading={endpointsFetchingData.unitsData.unitsIsLoading} /> ) diff --git a/src/client/app/redux/componentHooks.ts b/src/client/app/redux/componentHooks.ts new file mode 100644 index 000000000..c6ce4ffdb --- /dev/null +++ b/src/client/app/redux/componentHooks.ts @@ -0,0 +1,72 @@ +// import * as React from 'react'; +import { groupsApi } from './api/groupsApi'; +import { metersApi } from './api/metersApi'; +import { readingsApi } from './api/readingsApi'; +import { useAppSelector } from './hooks'; +import { selectChartQueryArgs } from './selectors/dataSelectors'; +import { unitsApi } from './api/unitsApi'; + +// General purpose custom hook mostly useful for Select component loadingIndicators, and current graph loading state(s) +export const getFetchingStates = () => { + const queryArgs = useAppSelector(state => selectChartQueryArgs(state)); + const { isFetching: meterLineIsFetching, isLoading: meterLineIsLoading } = readingsApi.endpoints.line.useQueryState(queryArgs.line.meterArgs); + const { isFetching: groupLineIsFetching, isLoading: groupLineIsLoading } = readingsApi.endpoints.line.useQueryState(queryArgs.line.groupsArgs); + const { isFetching: meterBarIsFetching, isLoading: meterBarIsLoading } = readingsApi.endpoints.bar.useQueryState(queryArgs.bar.meterArgs); + const { isFetching: groupBarIsFetching, isLoading: groupBarIsLoading } = readingsApi.endpoints.bar.useQueryState(queryArgs.bar.groupsArgs); + const { isFetching: threeDIsFetching, isLoading: threeDIsLoading } = readingsApi.endpoints.threeD.useQueryState(queryArgs.threeD.args); + const { isFetching: metersFetching, isLoading: metersLoading } = metersApi.endpoints.getMeters.useQueryState(); + const { isFetching: groupsFetching, isLoading: groupsLoading } = groupsApi.endpoints.getGroups.useQueryState(); + const { isFetching: unitsIsFetching, isLoading: unitsIsLoading } = unitsApi.endpoints.getUnitsDetails.useQueryState(); + + + return { + endpointsFetchingData: { + lineMeterReadings: { meterLineIsFetching, meterLineIsLoading }, + lineGroupReadings: { groupLineIsFetching, groupLineIsLoading }, + barMeterReadings: { meterBarIsFetching, meterBarIsLoading }, + barGroupReadings: { groupBarIsFetching, groupBarIsLoading }, + threeDReadings: { threeDIsFetching, threeDIsLoading }, + meterData: { metersFetching, metersLoading }, + groupData: { groupsFetching, groupsLoading }, + unitsData: { unitsIsFetching, unitsIsLoading } + }, + somethingIsFetching: meterLineIsFetching || + groupLineIsFetching || + meterBarIsFetching || + groupBarIsFetching || + threeDIsFetching || + metersFetching || + groupsFetching || + unitsIsFetching + + } + // Since we're deriving data, we can useMemo() for stable references. + // const fetchInfo = React.useMemo(() => ({ + // endpointsFetchingData: { + // meterLineIsLoading, + // groupLineIsLoading, + // meterBarIsLoading, + // groupBarIsLoading, + // threeDIsLoading, + // metersLoading, + // groupsLoading, + // unitsIsLoading + // }, + // somethingIsFetching: meterLineIsLoading || + // groupLineIsLoading || + // meterBarIsLoading || + // groupBarIsLoading || + // threeDIsLoading || + // metersLoading || + // groupsLoading || + // unitsIsLoading + + // } + // ), [ + // meterLineIsLoading, groupLineIsLoading, + // meterBarIsLoading, groupBarIsLoading, + // threeDIsLoading, metersLoading, + // groupsLoading, unitsIsLoading + // ]) + +} diff --git a/src/client/app/redux/hooks.ts b/src/client/app/redux/hooks.ts index e59f17d0c..373643200 100644 --- a/src/client/app/redux/hooks.ts +++ b/src/client/app/redux/hooks.ts @@ -4,4 +4,4 @@ import type { RootState, AppDispatch } from '../store' // https://react-redux.js.org/using-react-redux/usage-with-typescript#define-typed-hooks // Use throughout your app instead of plain `useDispatch` and `useSelector` export const useAppDispatch: () => AppDispatch = useDispatch -export const useAppSelector: TypedUseSelectorHook = useSelector \ No newline at end of file +export const useAppSelector: TypedUseSelectorHook = useSelector diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts index 92f210858..8313a3715 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -529,4 +529,4 @@ export const selectDateRangeInterval = createSelector( timeInterval => { return timeInterval } -) \ No newline at end of file +) From a3344b230e5a649906865a906d1952494979de76 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Mon, 16 Oct 2023 04:29:49 +0000 Subject: [PATCH 027/131] Fix Merge property usage --- .../app/components/LineChartComponent.tsx | 43 ++++++++++--------- src/client/app/reducers/graph.ts | 6 ++- src/client/app/redux/api/readingsApi.ts | 7 ++- 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/client/app/components/LineChartComponent.tsx b/src/client/app/components/LineChartComponent.tsx index a5ca96b93..6980944c1 100644 --- a/src/client/app/components/LineChartComponent.tsx +++ b/src/client/app/components/LineChartComponent.tsx @@ -8,10 +8,6 @@ import { PlotRelayoutEvent } from 'plotly.js'; import * as React from 'react'; import Plot, { Figure } from 'react-plotly.js'; import { TimeInterval } from '../../../common/TimeInterval'; -import { graphSlice, selectSelectedGroups, selectSelectedMeters } from '../reducers/graph'; -import { groupsSlice } from '../reducers/groups'; -import { metersSlice } from '../reducers/meters'; -import { unitsSlice } from '../reducers/units'; import { readingsApi } from '../redux/api/readingsApi'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { ChartQueryProps, LineReadingApiArgs } from '../redux/selectors/dataSelectors'; @@ -21,7 +17,13 @@ import getGraphColor from '../utils/getGraphColor'; import { lineUnitLabel } from '../utils/graphics'; import translate from '../utils/translate'; import LogoSpinner from './LogoSpinner'; - +import { selectGroupDataByID } from '../reducers/groups'; +import { selectMeterDataByID, selectMeterState } from '../reducers/meters'; +import { selectUnitDataById } from '../reducers/units'; +import { + graphSlice, selectAreaUnit, selectGraphAreaNormalization, + selectLineGraphRate, selectSelectedGroups, selectSelectedMeters +} from '../reducers/graph'; /** * @param props qpi query @@ -35,23 +37,23 @@ export default function LineChartComponent(props: ChartQueryProps state.graph.selectedUnit); // The current selected rate - const currentSelectedRate = useAppSelector(state => state.graph.lineGraphRate); - const unitDataByID = useAppSelector(state => unitsSlice.selectors.selectUnitDataById(state)); - const selectedAreaNormalization = useAppSelector(state => state.graph.areaNormalization); - const selectedAreaUnit = useAppSelector(state => state.graph.selectedAreaUnit); - const selectedMeters = useAppSelector(state => selectSelectedMeters(state)); - const selectedGroups = useAppSelector(state => selectSelectedGroups(state)); - const metersState = useAppSelector(state => metersSlice.selectors.selectMeterState(state)); - const meterDataByID = useAppSelector(state => metersSlice.selectors.selectMeterDataByID(state)); - const groupDataByID = useAppSelector(state => groupsSlice.selectors.selectGroupDataByID(state)); + const currentSelectedRate = useAppSelector(selectLineGraphRate); + const unitDataByID = useAppSelector(selectUnitDataById); + const selectedAreaNormalization = useAppSelector(selectGraphAreaNormalization); + const selectedAreaUnit = useAppSelector(selectAreaUnit); + const selectedMeters = useAppSelector(selectSelectedMeters); + const selectedGroups = useAppSelector(selectSelectedGroups); + const metersState = useAppSelector(selectMeterState); + const meterDataByID = useAppSelector(selectMeterDataByID); + const groupDataByID = useAppSelector(selectGroupDataByID); // dataFetching Query Hooks - const { data: meterReadings, isLoading: meterIsFetching } = readingsApi.useLineQuery(meterArgs, { skip: meterSkipQuery }); - const { data: groupData, isLoading: groupIsFetching } = readingsApi.useLineQuery(groupsArgs, { skip: groupSkipQuery }); + const { data: meterReadings, isLoading: meterIsLoading } = readingsApi.useLineQuery(meterArgs, { skip: meterSkipQuery }); + const { data: groupData, isLoading: groupIsLoading } = readingsApi.useLineQuery(groupsArgs, { skip: groupSkipQuery }); const datasets = []; - if (meterIsFetching || groupIsFetching) { + if (meterIsLoading || groupIsLoading) { return // return } @@ -92,7 +94,7 @@ export default function LineChartComponent(props: ChartQueryProps state.chartToRender, selectThreeDMeterOrGroup: state => state.threeD.meterOrGroup, selectThreeDMeterOrGroupID: state => state.threeD.meterOrGroupID, - selectThreeDReadingInterval: state => state.threeD.readingInterval + selectThreeDReadingInterval: state => state.threeD.readingInterval, + selectLineGraphRate: state => state.lineGraphRate, + selectAreaUnit: state => state.selectedAreaUnit } }) @@ -256,5 +258,5 @@ export const { selectWorkingTimeInterval, selectGraphUnitID, selectGraphAreaNormalization, selectChartToRender, selectThreeDMeterOrGroup, selectThreeDMeterOrGroupID, - selectThreeDReadingInterval + selectThreeDReadingInterval, selectLineGraphRate, selectAreaUnit } = graphSlice.selectors \ No newline at end of file diff --git a/src/client/app/redux/api/readingsApi.ts b/src/client/app/redux/api/readingsApi.ts index f2d864c56..2319244a2 100644 --- a/src/client/app/redux/api/readingsApi.ts +++ b/src/client/app/redux/api/readingsApi.ts @@ -47,7 +47,10 @@ export const readingsApi = baseApi.injectEndpoints({ merge: (currentCacheData, responseData) => { // By default subsequent queries that resolve to the same cache entry will overwrite the existing data. // For our use case, many queries will point to the same resolved cache, therefore we must provide merge behavior to not lose data - return _.merge(currentCacheData, responseData) + + // it is important to note, + // Since this is wrapped with Immer, you may either mutate the currentCacheValue directly, or return a new value, but not both at once. + _.merge(currentCacheData, responseData) }, forceRefetch: ({ currentArg, endpointState }) => { // Since we modified the way the we serialize the args any subsequent query would return the cache data, even if new meters were requested @@ -101,7 +104,7 @@ export const readingsApi = baseApi.injectEndpoints({ bar: builder.query({ // Refer to line endpoint for detailed explanation as the logic is identical serializeQueryArgs: ({ queryArgs }) => _.omit(queryArgs, 'ids'), - merge: (currentCacheData, responseData) => _.merge(currentCacheData, responseData), + merge: (currentCacheData, responseData) => { _.merge(currentCacheData, responseData) }, forceRefetch: ({ currentArg, endpointState }) => { const currentData = endpointState?.data ? Object.keys(endpointState.data).map(Number) : undefined if (!currentData) { return true } From ca9a7f160a99ff40b9762de22aa65e86afd5e6d5 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Tue, 17 Oct 2023 22:33:06 +0000 Subject: [PATCH 028/131] Basic Graph State History --- .../app/components/BarChartComponent.tsx | 26 +---- .../app/components/DashboardComponent.tsx | 31 +++--- .../app/components/DateRangeComponent.tsx | 22 ++--- .../app/components/HistoryComponent.tsx | 28 ++++++ .../app/components/LineChartComponent.tsx | 41 +++----- src/client/app/components/ThreeDComponent.tsx | 13 +-- .../app/components/UIOptionsComponent.tsx | 1 - .../app/containers/MapChartContainer.ts | 30 +++--- src/client/app/reducers/graph.ts | 96 +++++++++++++++---- src/client/app/reducers/options.ts | 6 ++ src/client/app/redux/api/preferencesApi.ts | 22 +---- src/client/app/redux/api/readingsApi.ts | 27 ++---- .../app/redux/middleware/graphHistory.ts | 40 ++++++++ src/client/app/redux/middleware/middleware.ts | 6 ++ .../app/redux/selectors/dataSelectors.ts | 70 +++++++++----- .../app/redux/selectors/threeDSelectors.ts | 6 +- src/client/app/store.ts | 5 +- src/client/app/types/redux/graph.ts | 10 +- 18 files changed, 288 insertions(+), 192 deletions(-) create mode 100644 src/client/app/components/HistoryComponent.tsx create mode 100644 src/client/app/redux/middleware/graphHistory.ts create mode 100644 src/client/app/redux/middleware/middleware.ts diff --git a/src/client/app/components/BarChartComponent.tsx b/src/client/app/components/BarChartComponent.tsx index 0582ab155..ee8f1d508 100644 --- a/src/client/app/components/BarChartComponent.tsx +++ b/src/client/app/components/BarChartComponent.tsx @@ -6,7 +6,7 @@ import * as _ from 'lodash'; import * as moment from 'moment'; import { PlotRelayoutEvent } from 'plotly.js'; import * as React from 'react'; -import Plot, { Figure } from 'react-plotly.js'; +import Plot from 'react-plotly.js'; import { TimeInterval } from '../../../common/TimeInterval'; import { graphSlice, selectSelectedGroups, selectSelectedMeters } from '../reducers/graph'; import { groupsSlice } from '../reducers/groups'; @@ -14,7 +14,7 @@ import { metersSlice } from '../reducers/meters'; import { unitsSlice } from '../reducers/units'; import { readingsApi } from '../redux/api/readingsApi'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; -import { BarReadingApiArgs, ChartQueryProps } from '../redux/selectors/dataSelectors'; +import { BarReadingApiArgs, ChartMultiQueryProps } from '../redux/selectors/dataSelectors'; import { DataType } from '../types/Datasources'; import { UnitRepresentType } from '../types/redux/units'; import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; @@ -30,8 +30,8 @@ import SpinnerComponent from './SpinnerComponent'; * @param props query arguments to be used in the dataFetching Hooks. * @returns Plotly BarChart */ -export default function BarChartComponent(props: ChartQueryProps) { - const { meterArgs, groupsArgs, meterSkipQuery, groupSkipQuery } = props.queryProps; +export default function BarChartComponent(props: ChartMultiQueryProps) { + const { meterArgs, groupsArgs, meterSkipQuery, groupSkipQuery } = props.queryArgs; const dispatch = useAppDispatch(); const barDuration = useAppSelector(state => state.graph.barDuration); const barStacking = useAppSelector(state => state.graph.barStacking); @@ -183,20 +183,7 @@ export default function BarChartComponent(props: ChartQueryProps { - if (figure.layout.xaxis?.range) { - const startTS = moment.utc(figure.layout.xaxis?.range[0]) - const endTS = moment.utc(figure.layout.xaxis?.range[1]) - const workingTimeInterval = new TimeInterval(startTS, endTS); - dispatch(graphSlice.actions.updateWorkingTimeInterval(workingTimeInterval)) - - // console.log(figure.layout.xaxis?.range, figure.layout.xaxis?.rangeslider?.range, figure.layout.xaxis) - } - } - const handleRelayout = (e: PlotRelayoutEvent) => { - console.log(typeof e['xaxis.range[0]'], typeof e['xaxis.range[1]']) // This event emits an object that contains values indicating changes in the user's graph, such as zooming. // These values indicate when the user has zoomed or made other changes to the graph. if (e['xaxis.range[0]'] && e['xaxis.range[0]']) { @@ -206,7 +193,6 @@ export default function BarChartComponent(props: ChartQueryProps {`${translate('select.meter.group')}`} @@ -237,11 +222,10 @@ export default function BarChartComponent(props: ChartQueryProps
- +
- {chartToRender === ChartTypes.line && } - {chartToRender === ChartTypes.bar && } - { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - chartToRender === ChartTypes.compare && - } - { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - chartToRender === ChartTypes.map && - } - { - chartToRender === ChartTypes.threeD && - } +
+ + + {chartToRender === ChartTypes.line && } + {chartToRender === ChartTypes.bar && } + {chartToRender === ChartTypes.compare && } + {chartToRender === ChartTypes.map && } + {chartToRender === ChartTypes.threeD && } +
diff --git a/src/client/app/components/DateRangeComponent.tsx b/src/client/app/components/DateRangeComponent.tsx index beac3391b..2cf96bfbc 100644 --- a/src/client/app/components/DateRangeComponent.tsx +++ b/src/client/app/components/DateRangeComponent.tsx @@ -8,7 +8,7 @@ import { Value } from '@wojtekmaj/react-daterange-picker/dist/cjs/shared/types'; import * as React from 'react'; import 'react-calendar/dist/Calendar.css'; import { useDispatch } from 'react-redux'; -import { graphSlice } from '../reducers/graph'; +import { selectQueryTimeInterval, updateTimeInterval } from '../reducers/graph'; import { useAppSelector } from '../redux/hooks'; import { Dispatch } from '../types/redux/actions'; import { dateRangeToTimeInterval, timeIntervalToDateRange } from '../utils/dateRangeCompatibility'; @@ -19,24 +19,19 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; * @returns Date Range Calendar Picker */ export default function DateRangeComponent() { - const { selectWorkingTimeInterval: graphWorkingTimeInterval, selectQueryTimeInterval } = graphSlice.selectors const dispatch: Dispatch = useDispatch(); - const timeInterval = useAppSelector(selectQueryTimeInterval); - const workingTimeInterval = useAppSelector(graphWorkingTimeInterval); + const queryTimeInterval = useAppSelector(selectQueryTimeInterval); const locale = useAppSelector(state => state.options.selectedLanguage); const handleChange = (value: Value) => { - console.log(value) - - if (!value) { - // Value has been cleared - dispatch(graphSlice.actions.resetTimeInterval()) - } else { - dispatch(graphSlice.actions.updateTimeInterval(dateRangeToTimeInterval(value))) - + // Dispatch in all cases except when value have been cleared and time interval already unbounded + // A null value indicates that the picker has been cleared + if (!(!value && !queryTimeInterval.getIsBounded())) { + dispatch(updateTimeInterval(dateRangeToTimeInterval(value))) } } + return (

@@ -44,13 +39,12 @@ export default function DateRangeComponent() {

diff --git a/src/client/app/components/HistoryComponent.tsx b/src/client/app/components/HistoryComponent.tsx new file mode 100644 index 000000000..4104004f9 --- /dev/null +++ b/src/client/app/components/HistoryComponent.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { prevHistory, nextHistory } from '../reducers/graph'; +/** + * @returns Renders a history component with previous and next buttons. + */ +export default function HistoryComponent() { + const dispatch = useAppDispatch(); + const back = useAppSelector(state => state.graph.backHistoryStack) + const forward = useAppSelector(state => state.graph.forwardHistoryStack) + + return ( +
+ dispatch(prevHistory())} + > + + + dispatch(nextHistory())} + > + + +
+ ) +} \ No newline at end of file diff --git a/src/client/app/components/LineChartComponent.tsx b/src/client/app/components/LineChartComponent.tsx index 6980944c1..bc49627f0 100644 --- a/src/client/app/components/LineChartComponent.tsx +++ b/src/client/app/components/LineChartComponent.tsx @@ -6,31 +6,31 @@ import * as _ from 'lodash'; import * as moment from 'moment'; import { PlotRelayoutEvent } from 'plotly.js'; import * as React from 'react'; -import Plot, { Figure } from 'react-plotly.js'; +import Plot from 'react-plotly.js'; import { TimeInterval } from '../../../common/TimeInterval'; +import { + graphSlice, selectAreaUnit, selectGraphAreaNormalization, + selectLineGraphRate, selectSelectedGroups, selectSelectedMeters +} from '../reducers/graph'; +import { selectGroupDataByID } from '../reducers/groups'; +import { selectMeterDataByID, selectMeterState } from '../reducers/meters'; +import { selectUnitDataById } from '../reducers/units'; import { readingsApi } from '../redux/api/readingsApi'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; -import { ChartQueryProps, LineReadingApiArgs } from '../redux/selectors/dataSelectors'; +import { ChartMultiQueryProps, LineReadingApiArgs } from '../redux/selectors/dataSelectors'; import { DataType } from '../types/Datasources'; import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; import getGraphColor from '../utils/getGraphColor'; import { lineUnitLabel } from '../utils/graphics'; import translate from '../utils/translate'; import LogoSpinner from './LogoSpinner'; -import { selectGroupDataByID } from '../reducers/groups'; -import { selectMeterDataByID, selectMeterState } from '../reducers/meters'; -import { selectUnitDataById } from '../reducers/units'; -import { - graphSlice, selectAreaUnit, selectGraphAreaNormalization, - selectLineGraphRate, selectSelectedGroups, selectSelectedMeters -} from '../reducers/graph'; /** * @param props qpi query * @returns plotlyLine graphic */ -export default function LineChartComponent(props: ChartQueryProps) { - const { meterArgs, groupsArgs, meterSkipQuery, groupSkipQuery } = props.queryProps; +export default function LineChartComponent(props: ChartMultiQueryProps) { + const { meterArgs, groupsArgs, meterSkipQuery, groupSkipQuery } = props.queryArgs; const dispatch = useAppDispatch(); const selectedUnit = useAppSelector(state => state.graph.selectedUnit); @@ -48,8 +48,8 @@ export default function LineChartComponent(props: ChartQueryProps { - if (figure.layout.xaxis?.range) { - const startTS = moment.utc(figure.layout.xaxis?.range[0]) - const endTS = moment.utc(figure.layout.xaxis?.range[1]) - const workingTimeInterval = new TimeInterval(startTS, endTS); - dispatch(graphSlice.actions.updateWorkingTimeInterval(workingTimeInterval)) - } - } - const handleRelayout = (e: PlotRelayoutEvent) => { // This event emits an object that contains values indicating changes in the user's graph, such as zooming. // These values indicate when the user has zoomed or made other changes to the graph. @@ -246,8 +236,6 @@ export default function LineChartComponent(props: ChartQueryProps console.log(e)} style={{ width: '100%', height: '80%' }} useResizeHandler={true} config={{ @@ -286,6 +274,7 @@ export default function LineChartComponent(props: ChartQueryProps) { + const { args, skipQuery } = props.queryArgs; + const { data, isFetching } = readingsApi.endpoints.threeD.useQuery(args, { skip: skipQuery }); const metersState = useSelector((state: State) => state.meters); const groupsState = useSelector((state: State) => state.groups); const graphState = useSelector((state: State) => state.graph); diff --git a/src/client/app/components/UIOptionsComponent.tsx b/src/client/app/components/UIOptionsComponent.tsx index 388fec32d..0358411bb 100644 --- a/src/client/app/components/UIOptionsComponent.tsx +++ b/src/client/app/components/UIOptionsComponent.tsx @@ -34,7 +34,6 @@ export default function UIOptionsComponent() { - { /* Controls error bar, specifically for the line chart. */ chartToRender === ChartTypes.line && } diff --git a/src/client/app/containers/MapChartContainer.ts b/src/client/app/containers/MapChartContainer.ts index ad3a832b0..849e6c17c 100644 --- a/src/client/app/containers/MapChartContainer.ts +++ b/src/client/app/containers/MapChartContainer.ts @@ -2,20 +2,24 @@ * 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 _ from 'lodash'; import * as moment from 'moment'; +import Plot, { PlotParams } from 'react-plotly.js'; import { connect } from 'react-redux'; -import Plot from 'react-plotly.js'; +import { DataType } from '../types/Datasources'; import { State } from '../types/redux/state'; +import { UnitRepresentType } from '../types/redux/units'; import { - calculateScaleFromEndpoints, itemDisplayableOnMap, Dimensions, - CartesianPoint, normalizeImageDimensions, itemMapInfoOk, gpsToUserGrid + CartesianPoint, + Dimensions, + calculateScaleFromEndpoints, + gpsToUserGrid, + itemDisplayableOnMap, + itemMapInfoOk, + normalizeImageDimensions } from '../utils/calibration'; -import * as _ from 'lodash'; -import getGraphColor from '../utils/getGraphColor'; -import Locales from '../types/locales'; -import { DataType } from '../types/Datasources'; -import { UnitRepresentType } from '../types/redux/units'; import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; +import getGraphColor from '../utils/getGraphColor'; import translate from '../utils/translate'; function mapStateToProps(state: State) { @@ -339,14 +343,10 @@ function mapStateToProps(state: State) { * layout={layout} * onClick={({points, event}) => console.log(points, event)}> */ - const props: any = { + const props = { data, - layout, - config: { - locales: Locales // makes locales available for use - } - }; - props.config.locale = state.options.selectedLanguage; + layout + } as PlotParams return props; } diff --git a/src/client/app/reducers/graph.ts b/src/client/app/reducers/graph.ts index 287ed8146..71f8da8c6 100644 --- a/src/client/app/reducers/graph.ts +++ b/src/client/app/reducers/graph.ts @@ -4,11 +4,12 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit'; import * as moment from 'moment'; +import * as _ from 'lodash' import { ActionMeta } from 'react-select'; import { TimeInterval } from '../../../common/TimeInterval'; import { preferencesApi } from '../redux/api/preferencesApi'; import { SelectOption } from '../types/items'; -import { ChartTypes, GraphState, LineGraphRate, MeterOrGroup, ReadingInterval } from '../types/redux/graph'; +import { ChartTypes, GraphState, GraphStateHistory, LineGraphRate, MeterOrGroup, ReadingInterval } from '../types/redux/graph'; import { ComparePeriod, SortingOrder, calculateCompareTimeInterval } from '../utils/calculateCompare'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; @@ -18,7 +19,6 @@ const defaultState: GraphState = { selectedUnit: -99, selectedAreaUnit: AreaUnitType.none, queryTimeInterval: TimeInterval.unbounded(), - workingTimeInterval: TimeInterval.unbounded(), rangeSliderInterval: TimeInterval.unbounded(), barDuration: moment.duration(4, 'weeks'), comparePeriod: ComparePeriod.Week, @@ -36,7 +36,9 @@ const defaultState: GraphState = { meterOrGroupID: undefined, meterOrGroup: undefined, readingInterval: ReadingInterval.Hourly - } + }, + backHistoryStack: [], + forwardHistoryStack: [] }; export const graphSlice = createSlice({ @@ -73,9 +75,6 @@ export const graphSlice = createSlice({ updateTimeInterval: (state, action: PayloadAction) => { state.queryTimeInterval = action.payload }, - updateWorkingTimeInterval: (state, action: PayloadAction) => { - state.workingTimeInterval = action.payload - }, changeSliderRange: (state, action: PayloadAction) => { state.rangeSliderInterval = action.payload }, @@ -215,11 +214,29 @@ export const graphSlice = createSlice({ state.threeD.meterOrGroup = undefined } + }, + updateHistory: (state, action: PayloadAction) => { + state.backHistoryStack.push(action.payload) + // reset forward history on new visit + state.forwardHistoryStack = [] + }, + prevHistory: state => { + if (state.backHistoryStack.length > 1) { + state.forwardHistoryStack.push(state.backHistoryStack.pop()!) + } + Object.assign(state, state.backHistoryStack[state.backHistoryStack.length - 1]); + }, + nextHistory: state => { + if (state.forwardHistoryStack.length) { + state.backHistoryStack.push(state.forwardHistoryStack.pop()!) + Object.assign(state, state.backHistoryStack[state.backHistoryStack.length - 1]) + } + + }, resetTimeInterval: state => { if (!state.queryTimeInterval.equals(TimeInterval.unbounded())) { state.queryTimeInterval = TimeInterval.unbounded() - state.workingTimeInterval = TimeInterval.unbounded() } } }, @@ -228,6 +245,12 @@ export const graphSlice = createSlice({ if (state.selectedAreaUnit === AreaUnitType.none) { state.selectedAreaUnit = action.payload.defaultAreaUnit; } + if (!state.hotlinked) { + state.chartToRender = action.payload.defaultChartToRender + state.barStacking = action.payload.defaultBarStacking + state.areaNormalization = action.payload.defaultAreaNormalization + } + state.backHistoryStack.push(_.omit(state, ['backHistoryStack', 'forwardHistoryStack'])) }) }, // New Feature as of 2.0.0 Beta. @@ -238,7 +261,6 @@ export const graphSlice = createSlice({ selectSelectedMeters: state => state.selectedMeters, selectSelectedGroups: state => state.selectedGroups, selectQueryTimeInterval: state => state.queryTimeInterval, - selectWorkingTimeInterval: state => state.workingTimeInterval, selectGraphUnitID: state => state.selectedUnit, selectGraphAreaNormalization: state => state.areaNormalization, selectChartToRender: state => state.chartToRender, @@ -252,11 +274,53 @@ export const graphSlice = createSlice({ // Selectors that can be imported and used in 'useAppSelectors' and 'createSelectors' export const { - selectThreeDState, selectBarWidthDays, - selectGraphState, selectSelectedMeters, - selectSelectedGroups, selectQueryTimeInterval, - selectWorkingTimeInterval, selectGraphUnitID, - selectGraphAreaNormalization, selectChartToRender, - selectThreeDMeterOrGroup, selectThreeDMeterOrGroupID, - selectThreeDReadingInterval, selectLineGraphRate, selectAreaUnit -} = graphSlice.selectors \ No newline at end of file + selectThreeDState, + selectBarWidthDays, + selectGraphState, + selectSelectedMeters, + selectSelectedGroups, + selectQueryTimeInterval, + selectGraphUnitID, + selectGraphAreaNormalization, + selectChartToRender, + selectThreeDMeterOrGroup, + selectThreeDMeterOrGroupID, + selectThreeDReadingInterval, + selectLineGraphRate, + selectAreaUnit +} = graphSlice.selectors + +// actionCreators exports +export const { + confirmGraphRenderOnce, + updateSelectedMeters, + updateSelectedGroups, + updateSelectedUnit, + updateSelectedAreaUnit, + updateBarDuration, + updateTimeInterval, + changeSliderRange, + resetRangeSliderStack, + updateComparePeriod, + changeChartToRender, + toggleAreaNormalization, + setAreaNormalization, + toggleShowMinMax, + setShowMinMax, + changeBarStacking, + setBarStacking, + setHotlinked, + changeCompareSortingOrder, + toggleOptionsVisibility, + setOptionsVisibility, + updateLineGraphRate, + updateThreeDReadingInterval, + updateThreeDMeterOrGroupInfo, + updateThreeDMeterOrGroupID, + updateThreeDMeterOrGroup, + updateSelectedMetersOrGroups, + resetTimeInterval, + updateHistory, + prevHistory, + nextHistory +} = graphSlice.actions \ No newline at end of file diff --git a/src/client/app/reducers/options.ts b/src/client/app/reducers/options.ts index f01ef5d8a..4527c336f 100644 --- a/src/client/app/reducers/options.ts +++ b/src/client/app/reducers/options.ts @@ -2,6 +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 { preferencesApi } from '../redux/api/preferencesApi'; import { LanguageTypes } from '../types/redux/i18n'; import { OptionsState } from '../types/redux/options'; import { createSlice } from '@reduxjs/toolkit' @@ -18,5 +19,10 @@ export const optionsSlice = createSlice({ updateSelectedLanguage: (state, action: PayloadAction) => { state.selectedLanguage = action.payload } + }, + extraReducers: builder => { + builder.addMatcher(preferencesApi.endpoints.getPreferences.matchFulfilled, (state, action) => { + state.selectedLanguage = action.payload.defaultLanguage + }) } }); diff --git a/src/client/app/redux/api/preferencesApi.ts b/src/client/app/redux/api/preferencesApi.ts index dbb7097b1..88478876b 100644 --- a/src/client/app/redux/api/preferencesApi.ts +++ b/src/client/app/redux/api/preferencesApi.ts @@ -1,31 +1,11 @@ -import { updateSelectedLanguage } from '../../actions/options'; -import { graphSlice } from '../../reducers/graph'; import { PreferenceRequestItem } from '../../types/items'; -import { RootState } from './../../store'; import { baseApi } from './baseApi'; export const preferencesApi = baseApi.injectEndpoints({ endpoints: builder => ({ getPreferences: builder.query({ - query: () => 'api/preferences', - // Tags used for invalidation by mutation requests. - onQueryStarted: async (_arg, { queryFulfilled, getState, dispatch }) => { - try { - const response = await queryFulfilled - const state = getState() as RootState - if (!state.graph.hotlinked) { - dispatch(graphSlice.actions.changeChartToRender(response.data.defaultChartToRender)); - dispatch(graphSlice.actions.setBarStacking(response.data.defaultBarStacking)); - dispatch(graphSlice.actions.setAreaNormalization(response.data.defaultAreaNormalization)); - dispatch(updateSelectedLanguage(response.data.defaultLanguage)); - } - - } catch (e) { - console.log('error', e) - } - - } + query: () => 'api/preferences' }), submitPreferences: builder.mutation({ query: preferences => ({ diff --git a/src/client/app/redux/api/readingsApi.ts b/src/client/app/redux/api/readingsApi.ts index 2319244a2..b475eb5b4 100644 --- a/src/client/app/redux/api/readingsApi.ts +++ b/src/client/app/redux/api/readingsApi.ts @@ -1,31 +1,23 @@ import * as _ from 'lodash'; -import { BarReadingApiArgs, LineReadingApiArgs } from '../../redux/selectors/dataSelectors'; +import { BarReadingApiArgs, LineReadingApiArgs, ThreeDReadingApiArgs } from '../../redux/selectors/dataSelectors'; import { RootState } from '../../store'; import { BarReadings, LineReadings, ThreeDReading } from '../../types/readings'; -import { MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; import { baseApi } from './baseApi'; -export type ThreeDReadingApiParams = { - meterOrGroupID: number; - timeInterval: string; - unitID: number; - readingInterval: ReadingInterval; - meterOrGroup: MeterOrGroup; -}; export const readingsApi = baseApi.injectEndpoints({ endpoints: builder => ({ // threeD: the queryEndpoint name // builder.query - threeD: builder.query({ + threeD: builder.query({ // ThreeD request only single meters at a time which plays well with default cache behavior // No other properties are necessary for this endpoint // Refer to the line endpoint for an example of an endpoint with custom cache behavior - query: ({ meterOrGroupID, timeInterval, unitID, readingInterval, meterOrGroup }) => { + query: ({ id, timeInterval, unitID, readingInterval, meterOrGroup }) => { // destructure args that are passed into the callback, and generate the API url for the request. const endpoint = `api/unitReadings/threeD/${meterOrGroup}/` - const args = `${meterOrGroupID}?timeInterval=${timeInterval.toString()}&graphicUnitId=${unitID}&readingInterval=${readingInterval}` + const args = `${id}?timeInterval=${timeInterval}&graphicUnitId=${unitID}&readingInterval=${readingInterval}` return `${endpoint}${args}` } }), @@ -81,12 +73,12 @@ export const readingsApi = baseApi.injectEndpoints({ // map cache keys to a number array, if any const cachedIDs = cachedData ? Object.keys(cachedData).map(Number) : [] // get the args provided in the original request - const { ids, timeInterval, graphicUnitID, meterOrGroup } = args + const { ids, timeInterval, unitID, meterOrGroup } = args // subtract any already cached keys from the requested ids, and stringify the array for the url endpoint const idsToFetch = _.difference(ids, cachedIDs).join(',') // api url from derived request arguments - const endpointURL = `api/unitReadings/line/${meterOrGroup}/${idsToFetch}?timeInterval=${timeInterval}&graphicUnitId=${graphicUnitID}` + const endpointURL = `api/unitReadings/line/${meterOrGroup}/${idsToFetch}?timeInterval=${timeInterval}&graphicUnitId=${unitID}` // use the baseQuery from the queryFn with our url endpoint const { data, error } = await baseQuery(endpointURL) @@ -112,17 +104,16 @@ export const readingsApi = baseApi.injectEndpoints({ return !dataInCache ? true : false }, queryFn: async (args, queryApi, _extra, baseQuery) => { - const { ids, timeInterval, graphicUnitID, meterOrGroup, barWidthDays } = args + const { ids, timeInterval, unitID, meterOrGroup, barWidthDays } = args const state = queryApi.getState() as RootState const cachedData = readingsApi.endpoints.bar.select(args)(state).data const cachedIDs = cachedData ? Object.keys(cachedData).map(Number) : [] const idsToFetch = _.difference(ids, cachedIDs).join(',') const endpoint = `api/unitReadings/bar/${meterOrGroup}/${idsToFetch}?` - const queryArgs = `timeInterval=${timeInterval}&barWidthDays=${barWidthDays}&graphicUnitId=${graphicUnitID}` + const queryArgs = `timeInterval=${timeInterval}&barWidthDays=${barWidthDays}&graphicUnitId=${unitID}` const endpointURL = `${endpoint}${queryArgs}` const { data, error } = await baseQuery(endpointURL) - if (error) { return { error } } - return { data: data as LineReadings } + return error ? { error } : { data: data as LineReadings } } }) }) diff --git a/src/client/app/redux/middleware/graphHistory.ts b/src/client/app/redux/middleware/graphHistory.ts new file mode 100644 index 000000000..ae3ae8c30 --- /dev/null +++ b/src/client/app/redux/middleware/graphHistory.ts @@ -0,0 +1,40 @@ +// https://redux-toolkit.js.org/api/createListenerMiddleware#typescript-usage +import { createListenerMiddleware, isAnyOf } from '@reduxjs/toolkit' +import * as _ from 'lodash' +import { + graphSlice, + nextHistory, + prevHistory, + setHotlinked, + setOptionsVisibility, + toggleOptionsVisibility, + updateHistory +} from '../../reducers/graph' +import { AppStartListening } from './middleware' + +export const historyMiddleware = createListenerMiddleware() +// Typescript usage for middleware api +const startHistoryListening = historyMiddleware.startListening as AppStartListening + +startHistoryListening({ + matcher: isAnyOf( + // listen to all graphSlice actions, filter out the ones don't directly alter the graph, or ones which can cause infinite recursion + // we use updateHistory here, so listening for updateHistory would cause infinite loops etc. + ...Object.values(graphSlice.actions) + .filter(action => !( + action === nextHistory || + action === prevHistory || + action === updateHistory || + action === toggleOptionsVisibility || + action === setOptionsVisibility || + action === setHotlinked + ) + ) + ), + effect: (action, api) => { + const state = api.getState(); + // Graph Actions may occur on startup. Do not track history until init preferences are set. + const historyState = _.omit(state.graph, ['backHistoryStack', 'forwardHistoryStack']) + api.dispatch(updateHistory(historyState)) + } +}) diff --git a/src/client/app/redux/middleware/middleware.ts b/src/client/app/redux/middleware/middleware.ts new file mode 100644 index 000000000..ad2d9fe7f --- /dev/null +++ b/src/client/app/redux/middleware/middleware.ts @@ -0,0 +1,6 @@ +// listenerMiddleware.ts +// https://redux-toolkit.js.org/api/createListenerMiddleware#typescript-usage +import { type TypedStartListening, type TypedAddListener, addListener } from '@reduxjs/toolkit' +import type { RootState, AppDispatch } from '../../store' +export type AppStartListening = TypedStartListening +export const addAppListener = addListener as TypedAddListener \ No newline at end of file diff --git a/src/client/app/redux/selectors/dataSelectors.ts b/src/client/app/redux/selectors/dataSelectors.ts index 004c26edf..cdd2dbc2b 100644 --- a/src/client/app/redux/selectors/dataSelectors.ts +++ b/src/client/app/redux/selectors/dataSelectors.ts @@ -3,66 +3,85 @@ import * as _ from 'lodash'; import { selectGraphState } from '../../reducers/graph'; import { selectGroupDataByID } from '../../reducers/groups'; import { selectMeterDataByID } from '../../reducers/meters'; -import { ThreeDReadingApiParams } from '../../redux/api/readingsApi'; -import { MeterOrGroup } from '../../types/redux/graph'; +import { readingsApi } from '../../redux/api/readingsApi'; +import { MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; import { GroupDefinition } from '../../types/redux/groups'; import { MeterData } from '../../types/redux/meters'; import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; import { selectIsLoggedInAsAdmin } from './authSelectors'; // Props that are passed to plotly components -export interface ChartQueryProps { - queryProps: ChartQueryArgs +export interface ChartMultiQueryProps { + queryArgs: ChartMultiQueryArgs } -export interface ChartQueryArgs { +export interface ChartMultiQueryArgs { meterArgs: T groupsArgs: T meterSkipQuery: boolean groupSkipQuery: boolean + meta: ChartQueryArgsMeta } // query args that all graphs share -export interface commonArgs { +export interface commonArgsMultiID { ids: number[]; timeInterval: string; - graphicUnitID: number; + unitID: number; meterOrGroup: MeterOrGroup; } +export interface ChartSingleQueryProps { + queryArgs: ChartQuerySingleArgs +} + + +export interface ChartQuerySingleArgs { + args: T; + skipQuery: boolean; + meta: ChartQueryArgsMeta +} +export interface ChartQueryArgsMeta { + endpoint: string; +} +export interface commonArgsSingleID extends Omit { id: number } // endpoint specific args -export interface LineReadingApiArgs extends commonArgs { } -export interface BarReadingApiArgs extends commonArgs { barWidthDays: number } +export interface LineReadingApiArgs extends commonArgsMultiID { } +export interface BarReadingApiArgs extends commonArgsMultiID { barWidthDays: number } +export interface ThreeDReadingApiArgs extends commonArgsSingleID { readingInterval: ReadingInterval; } // Selector prepares the query args for each endpoint based on the current graph slice state export const selectChartQueryArgs = createSelector( selectGraphState, graphState => { // args that all meters queries share - const baseMeterArgs: commonArgs = { + const baseMeterArgs: commonArgsMultiID = { ids: graphState.selectedMeters, timeInterval: graphState.queryTimeInterval.toString(), - graphicUnitID: graphState.selectedUnit, + unitID: graphState.selectedUnit, meterOrGroup: MeterOrGroup.meters } // args that all groups queries share - const baseGroupArgs: commonArgs = { + const baseGroupArgs: commonArgsMultiID = { ids: graphState.selectedGroups, timeInterval: graphState.queryTimeInterval.toString(), - graphicUnitID: graphState.selectedUnit, + unitID: graphState.selectedUnit, meterOrGroup: MeterOrGroup.groups } // props to pass into the line chart component - const line: ChartQueryArgs = { + const line: ChartMultiQueryArgs = { meterArgs: baseMeterArgs, groupsArgs: baseGroupArgs, meterSkipQuery: !baseMeterArgs.ids.length, - groupSkipQuery: !baseGroupArgs.ids.length + groupSkipQuery: !baseGroupArgs.ids.length, + meta: { + endpoint: readingsApi.endpoints.line.name + } } // props to pass into the bar chart component - const bar: ChartQueryArgs = { + const bar: ChartMultiQueryArgs = { meterArgs: { ...baseMeterArgs, barWidthDays: Math.round(graphState.barDuration.asDays()) @@ -72,19 +91,24 @@ export const selectChartQueryArgs = createSelector( barWidthDays: Math.round(graphState.barDuration.asDays()) }, meterSkipQuery: !baseMeterArgs.ids.length, - groupSkipQuery: !baseGroupArgs.ids.length + groupSkipQuery: !baseGroupArgs.ids.length, + meta: { + endpoint: readingsApi.endpoints.bar.name + } } - - - const threeD = { + // TODO; Make 2 types for multi-id and single-id request ARGS + const threeD: ChartQuerySingleArgs = { args: { - meterOrGroupID: graphState.threeD.meterOrGroupID, + id: graphState.threeD.meterOrGroupID, timeInterval: roundTimeIntervalForFetch(graphState.queryTimeInterval).toString(), unitID: graphState.selectedUnit, readingInterval: graphState.threeD.readingInterval, meterOrGroup: graphState.threeD.meterOrGroup - } as ThreeDReadingApiParams, - skip: !graphState.threeD.meterOrGroupID || !graphState.queryTimeInterval.getIsBounded() + } as ThreeDReadingApiArgs, + skipQuery: !graphState.threeD.meterOrGroupID || !graphState.queryTimeInterval.getIsBounded(), + meta: { + endpoint: readingsApi.endpoints.threeD.name + } } return { line, bar, threeD } diff --git a/src/client/app/redux/selectors/threeDSelectors.ts b/src/client/app/redux/selectors/threeDSelectors.ts index 925afe10b..b4104c9e6 100644 --- a/src/client/app/redux/selectors/threeDSelectors.ts +++ b/src/client/app/redux/selectors/threeDSelectors.ts @@ -10,7 +10,7 @@ import { selectMeterState } from '../../reducers/meters'; import { MeterOrGroup } from '../../types/redux/graph'; import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; -import { ThreeDReadingApiParams } from '../api/readingsApi'; +import { ThreeDReadingApiArgs } from './dataSelectors'; // Memoized Selectors @@ -52,12 +52,12 @@ export const selectThreeDQueryArgs = createSelector( selectThreeDMeterOrGroup, (id, timeInterval, unitID, readingInterval, meterOrGroup) => { return { - meterOrGroupID: id, + id: id, timeInterval: roundTimeIntervalForFetch(timeInterval).toString(), unitID: unitID, readingInterval: readingInterval, meterOrGroup: meterOrGroup - } as ThreeDReadingApiParams + } as ThreeDReadingApiArgs } ) diff --git a/src/client/app/store.ts b/src/client/app/store.ts index 61b9c3af5..05399fc7a 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -5,6 +5,7 @@ import { configureStore } from '@reduxjs/toolkit' import { rootReducer } from './reducers'; import { baseApi } from './redux/api/baseApi'; +import { historyMiddleware } from './redux/middleware/graphHistory'; export const store = configureStore({ @@ -12,7 +13,9 @@ export const store = configureStore({ middleware: getDefaultMiddleware => getDefaultMiddleware({ // immutableCheck: false, serializableCheck: false - }).concat(baseApi.middleware) + }) + .prepend(historyMiddleware.middleware) + .concat(baseApi.middleware) }); // Infer the `RootState` and `AppDispatch` types from the store itself diff --git a/src/client/app/types/redux/graph.ts b/src/client/app/types/redux/graph.ts index 45c1bb5be..604675aba 100644 --- a/src/client/app/types/redux/graph.ts +++ b/src/client/app/types/redux/graph.ts @@ -79,11 +79,9 @@ export interface GraphState { renderOnce: boolean; showMinMax: boolean; threeD: ThreeDState; - // Time interval that is used to query for data (either definite or TimeInterval.unbounded()) queryTimeInterval: TimeInterval; - // Time Interval that handles the ''effect'' of querying an unbounded() time interval - // Querying a time interval returns the entire meter's readings with is our working time interval. - // E.X. query(unbounded) returned readings(working time interval): 12-01-01 => 12-01-02. This working time interval is initially an unknown - // On initial render, or parsing of the data returned, Set the returned data's max and min time intervals to be the current workingTimeInterval. - workingTimeInterval: TimeInterval; + backHistoryStack: GraphStateHistory[]; + forwardHistoryStack: GraphStateHistory[]; } +export interface GraphStateHistory extends Omit { +} \ No newline at end of file From fc0ca0d9094d888eac62d9bd0fb312b4e5e315a9 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Wed, 18 Oct 2023 02:00:04 +0000 Subject: [PATCH 029/131] Chart Data Select Update --- .../app/components/ChartSelectComponent.tsx | 86 ++++++++----------- .../app/components/DashboardComponent.tsx | 2 +- .../app/redux/middleware/graphHistory.ts | 1 - .../app/redux/selectors/dataSelectors.ts | 19 ++-- 4 files changed, 43 insertions(+), 65 deletions(-) diff --git a/src/client/app/components/ChartSelectComponent.tsx b/src/client/app/components/ChartSelectComponent.tsx index 83272e84e..871fbe181 100644 --- a/src/client/app/components/ChartSelectComponent.tsx +++ b/src/client/app/components/ChartSelectComponent.tsx @@ -3,36 +3,30 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import * as _ from 'lodash'; -import { ChartTypes } from '../types/redux/graph'; +import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import TooltipMarkerComponent from './TooltipMarkerComponent'; import { useDispatch, useSelector } from 'react-redux'; -import { State } from '../types/redux/state'; -import { useState } from 'react'; -import { SelectOption } from '../types/items'; -import { Dispatch } from '../types/redux/actions'; -import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap'; +import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; import { graphSlice } from '../reducers/graph'; +import { Dispatch } from '../types/redux/actions'; +import { ChartTypes } from '../types/redux/graph'; +import { State } from '../types/redux/state'; +import translate from '../utils/translate'; +import TooltipMarkerComponent from './TooltipMarkerComponent'; /** * A component that allows users to select which chart should be displayed. * @returns Chart select element */ export default function ChartSelectComponent() { - const divBottomPadding: React.CSSProperties = { - paddingBottom: '15px' - }; - const labelStyle: React.CSSProperties = { - fontWeight: 'bold', - margin: 0 - }; - + const currentChartToRender = useSelector((state: State) => state.graph.chartToRender) const dispatch: Dispatch = useDispatch(); const [expand, setExpand] = useState(false); - 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'); + + // TODO Re-write as selector to use elsewhere + // 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'); return (
@@ -42,42 +36,32 @@ export default function ChartSelectComponent() {

setExpand(!expand)}> - state.graph.chartToRender)} /> + - dispatch(graphSlice.actions.changeChartToRender(ChartTypes.line))} - > - - - dispatch(graphSlice.actions.changeChartToRender(ChartTypes.bar))} - > - - - dispatch(graphSlice.actions.changeChartToRender(ChartTypes.compare))} - > - - - { - dispatch(graphSlice.actions.changeChartToRender(ChartTypes.map)); - if (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 }); - } - }} - > - - - dispatch(graphSlice.actions.changeChartToRender(ChartTypes.threeD))} - > - - + { + // Make items for dropdown from enum + Object.values(ChartTypes) + // filter out current chart + .filter(chartType => chartType !== currentChartToRender) + // map to components + .map(chartType => + dispatch(graphSlice.actions.changeChartToRender(chartType))} + > + {translate(`${chartType}`)} + + ) + }
); } +const divBottomPadding: React.CSSProperties = { + paddingBottom: '15px' +}; +const labelStyle: React.CSSProperties = { + fontWeight: 'bold', + margin: 0 +}; \ No newline at end of file diff --git a/src/client/app/components/DashboardComponent.tsx b/src/client/app/components/DashboardComponent.tsx index 7142a4c05..749704bed 100644 --- a/src/client/app/components/DashboardComponent.tsx +++ b/src/client/app/components/DashboardComponent.tsx @@ -33,7 +33,7 @@ export default function DashboardComponent() {
-
+
{chartToRender === ChartTypes.line && } diff --git a/src/client/app/redux/middleware/graphHistory.ts b/src/client/app/redux/middleware/graphHistory.ts index ae3ae8c30..4f100b1dc 100644 --- a/src/client/app/redux/middleware/graphHistory.ts +++ b/src/client/app/redux/middleware/graphHistory.ts @@ -33,7 +33,6 @@ startHistoryListening({ ), effect: (action, api) => { const state = api.getState(); - // Graph Actions may occur on startup. Do not track history until init preferences are set. const historyState = _.omit(state.graph, ['backHistoryStack', 'forwardHistoryStack']) api.dispatch(updateHistory(historyState)) } diff --git a/src/client/app/redux/selectors/dataSelectors.ts b/src/client/app/redux/selectors/dataSelectors.ts index cdd2dbc2b..f190d70bb 100644 --- a/src/client/app/redux/selectors/dataSelectors.ts +++ b/src/client/app/redux/selectors/dataSelectors.ts @@ -5,8 +5,6 @@ import { selectGroupDataByID } from '../../reducers/groups'; import { selectMeterDataByID } from '../../reducers/meters'; import { readingsApi } from '../../redux/api/readingsApi'; import { MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; -import { GroupDefinition } from '../../types/redux/groups'; -import { MeterData } from '../../types/redux/meters'; import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; import { selectIsLoggedInAsAdmin } from './authSelectors'; @@ -47,7 +45,7 @@ export interface commonArgsSingleID extends Omit { id: // endpoint specific args export interface LineReadingApiArgs extends commonArgsMultiID { } export interface BarReadingApiArgs extends commonArgsMultiID { barWidthDays: number } -export interface ThreeDReadingApiArgs extends commonArgsSingleID { readingInterval: ReadingInterval; } +export interface ThreeDReadingApiArgs extends commonArgsSingleID { readingInterval: ReadingInterval } // Selector prepares the query args for each endpoint based on the current graph slice state export const selectChartQueryArgs = createSelector( @@ -98,13 +96,14 @@ export const selectChartQueryArgs = createSelector( } // TODO; Make 2 types for multi-id and single-id request ARGS const threeD: ChartQuerySingleArgs = { + // Fix not null assertion(s) args: { - id: graphState.threeD.meterOrGroupID, + id: graphState.threeD.meterOrGroupID!, timeInterval: roundTimeIntervalForFetch(graphState.queryTimeInterval).toString(), unitID: graphState.selectedUnit, readingInterval: graphState.threeD.readingInterval, - meterOrGroup: graphState.threeD.meterOrGroup - } as ThreeDReadingApiArgs, + meterOrGroup: graphState.threeD.meterOrGroup! + }, skipQuery: !graphState.threeD.meterOrGroupID || !graphState.queryTimeInterval.getIsBounded(), meta: { endpoint: readingsApi.endpoints.threeD.name @@ -126,12 +125,8 @@ export const selectVisibleMetersGroupsDataByID = createSelector( visibleMeters = meterDataByID visibleGroups = groupDataByID; } else { - visibleMeters = _.filter(meterDataByID, (meter: MeterData) => { - return meter.displayable === true - }); - visibleGroups = _.filter(groupDataByID, (group: GroupDefinition) => { - return group.displayable === true - }); + visibleMeters = _.filter(meterDataByID, meter => meter.displayable); + visibleGroups = _.filter(groupDataByID, group => group.displayable); } return { visibleMeters, visibleGroups } From b4b694de65d1b280c335949d2aed87d477c47830 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Sun, 22 Oct 2023 14:19:29 +0000 Subject: [PATCH 030/131] Routing & Unsaved Changes --- .../app/components/ChartSelectComponent.tsx | 1 + .../app/components/HeaderButtonsComponent.tsx | 2 +- .../app/components/RouteComponentWIP.tsx | 109 +++++++++--------- .../components/UnsavedWarningComponentWIP.tsx | 55 +++++++++ .../admin/UsersDetailComponentWIP.tsx | 30 +++-- src/client/app/redux/api/readingsApi.ts | 4 +- src/client/app/redux/api/userApi.ts | 4 +- 7 files changed, 140 insertions(+), 65 deletions(-) create mode 100644 src/client/app/components/UnsavedWarningComponentWIP.tsx diff --git a/src/client/app/components/ChartSelectComponent.tsx b/src/client/app/components/ChartSelectComponent.tsx index 871fbe181..b56e9b21c 100644 --- a/src/client/app/components/ChartSelectComponent.tsx +++ b/src/client/app/components/ChartSelectComponent.tsx @@ -47,6 +47,7 @@ export default function ChartSelectComponent() { // map to components .map(chartType => dispatch(graphSlice.actions.changeChartToRender(chartType))} > {translate(`${chartType}`)} diff --git a/src/client/app/components/HeaderButtonsComponent.tsx b/src/client/app/components/HeaderButtonsComponent.tsx index dc2b8454e..499868e1a 100644 --- a/src/client/app/components/HeaderButtonsComponent.tsx +++ b/src/client/app/components/HeaderButtonsComponent.tsx @@ -266,7 +266,7 @@ export default function HeaderButtonsComponent() { + to='login'> state.options.selectedLanguage) - const messages = (localeData as any)[lang]; - return ( - <> - - - {/* Compatibility layer for transitioning to react-router 6 Checkout https://github.com/remix-run/react-router/discussions/8753 */} - {/* - The largest barrier to completely transitioning is Reworking the UnsavedWarningComponent. - is not compatible with react-router v6, and will need to be completely reworked if router-migration goes moves forward. - The UnsavedWarningComponent is use in many of the admin routes, so it is likely that they will also need to be reworked. - */} - - } /> - } /> - } /> - {/* // Any Route in this must passthrough the admin outlet which checks for authentication status */} - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - }> - } /> - - {/* // Redirect any other invalid route to root */} - } /> - - - - - - ); -} + const useWaitForInit = () => { const dispatch = useAppDispatch(); @@ -101,7 +57,7 @@ const useWaitForInit = () => { return { isAdmin, currentUser, initComplete } } -const AdminOutlet = () => { +export const AdminOutlet = () => { const { isAdmin, initComplete } = useWaitForInit(); if (!initComplete) { @@ -119,7 +75,7 @@ const AdminOutlet = () => { } // Function that returns a JSX element. Either the requested route's Component, as outlet or back to root -const RoleOutlet = ({ UserRole }: { UserRole: UserRole }) => { +export const RoleOutlet = ({ UserRole }: { UserRole: UserRole }) => { const { currentUser, initComplete } = useWaitForInit(); // If state contains token it has been validated on startup or login. if (!initComplete) { @@ -132,13 +88,13 @@ const RoleOutlet = ({ UserRole }: { UserRole: UserRole }) => { return } -const NotFound = () => { +export const NotFound = () => { return } // TODO fix this route -const GraphLink = () => { +export const GraphLink = () => { const dispatch = useAppDispatch(); const [URLSearchParams] = useSearchParams(); const { initComplete } = useWaitForInit(); @@ -236,4 +192,49 @@ const GraphLink = () => { return +} + + +/// Router +const router = createBrowserRouter([ + { path: '/', element: }, + { path: 'login', element: }, + { + path: '/', + element: , + children: [ + { path: 'admin', element: }, + { path: 'calibration', element: }, + { path: 'maps', element: }, + { path: 'users/new', element: }, + { path: 'units', element: }, + { path: 'conversions', element: }, + { path: 'groups', element: }, + { path: 'meters', element: }, + { path: 'users', element: } + ] + }, + { + path: '/', + element: , + children: [ + { path: 'csv', element: } + ] + }, + { + path: '*', element: + } +]) + +/** + * @returns the router component Currently under migration! + */ +export default function RouteComponentWIP() { + const lang = useAppSelector(state => state.options.selectedLanguage) + const messages = (localeData as any)[lang]; + return ( + + + + ); } \ No newline at end of file diff --git a/src/client/app/components/UnsavedWarningComponentWIP.tsx b/src/client/app/components/UnsavedWarningComponentWIP.tsx new file mode 100644 index 000000000..903d25c6e --- /dev/null +++ b/src/client/app/components/UnsavedWarningComponentWIP.tsx @@ -0,0 +1,55 @@ +/* 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 { FormattedMessage } from 'react-intl'; +import { + unstable_BlockerFunction as BlockerFunction, + unstable_useBlocker as useBlocker +} from 'react-router-dom-v5-compat'; +// TODO migrate ReactRouter v6 & hooks +import { Button, Modal, ModalBody, ModalFooter } from 'reactstrap'; +import { userApi } from '../redux/api/userApi' +import { MutationTrigger } from '@reduxjs/toolkit/dist/query/react/buildHooks'; +export interface UnsavedWarningProps { + hasUnsavedChanges: boolean | BlockerFunction; + changes: any; + submitChanges: MutationTrigger< + typeof userApi.endpoints.editUsers.Types.MutationDefinition | + typeof userApi.endpoints.createUser.Types.MutationDefinition + >; +} + +/** + * @param props unsavedChanges Boolean + * @returns Component that prompts before navigating away from current page + */ +export function UnsavedWarningComponentWIP(props: UnsavedWarningProps) { + const { hasUnsavedChanges, submitChanges, changes } = props + const blocker = useBlocker(hasUnsavedChanges); + + console.log(props) + + return ( + + + {/* */} + + + + + + + + ) +} diff --git a/src/client/app/components/admin/UsersDetailComponentWIP.tsx b/src/client/app/components/admin/UsersDetailComponentWIP.tsx index a26dcd7a5..4e1de4367 100644 --- a/src/client/app/components/admin/UsersDetailComponentWIP.tsx +++ b/src/client/app/components/admin/UsersDetailComponentWIP.tsx @@ -6,16 +6,17 @@ import * as _ from 'lodash'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { Button, Input, Table } from 'reactstrap'; +import FooterContainer from '../../containers/FooterContainer'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; -import UnsavedWarningContainer from '../../containers/UnsavedWarningContainer'; import { userApi } from '../../redux/api/userApi'; import { User, UserRole } from '../../types/items'; import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; import translate from '../../utils/translate'; +import HeaderComponent from '../HeaderComponent'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; +import { UnsavedWarningComponentWIP } from '../UnsavedWarningComponentWIP'; import CreateUserLinkButtonComponent from './users/CreateUserLinkButtonComponent'; -import FooterContainer from '../../containers/FooterContainer'; -import HeaderComponent from '../HeaderComponent'; + /** * Component which shows user details @@ -25,9 +26,19 @@ export default function UserDetailComponentWIP() { const { data: users = [] } = userApi.useGetUsersQuery(undefined); const [submitUserEdits] = userApi.useEditUsersMutation(); const [localUsersChanges, setLocalUsersChanges] = React.useState([]); + const [hasChanges, setHasChanges] = React.useState(false); - // keep history in sync whenever query data changes React.useEffect(() => { setLocalUsersChanges(users) }, [users]) + React.useEffect( + () => { + if (!_.isEqual(users, localUsersChanges)) { + setHasChanges(true) + } else { + setHasChanges(false) + } + }, + [localUsersChanges] + ) const editUser = (e: React.ChangeEvent, targetUser: User) => { // copy user, and update role @@ -43,7 +54,7 @@ export default function UserDetailComponentWIP() { .then(() => { showSuccessNotification(translate('users.successfully.edit.users')); }) - .catch((e) => { + .catch(e => { console.log(e) showErrorNotification(translate('users.failed.to.edit.users')) }) @@ -56,8 +67,12 @@ export default function UserDetailComponentWIP() { return (
- +

@@ -111,6 +126,7 @@ export default function UserDetailComponentWIP() {

+
) @@ -133,4 +149,4 @@ const buttonsStyle: React.CSSProperties = { const tooltipStyle = { display: 'inline-block', fontSize: '50%' -}; \ No newline at end of file +}; diff --git a/src/client/app/redux/api/readingsApi.ts b/src/client/app/redux/api/readingsApi.ts index b475eb5b4..8bcb97c93 100644 --- a/src/client/app/redux/api/readingsApi.ts +++ b/src/client/app/redux/api/readingsApi.ts @@ -42,7 +42,7 @@ export const readingsApi = baseApi.injectEndpoints({ // it is important to note, // Since this is wrapped with Immer, you may either mutate the currentCacheValue directly, or return a new value, but not both at once. - _.merge(currentCacheData, responseData) + Object.assign(currentCacheData, responseData) }, forceRefetch: ({ currentArg, endpointState }) => { // Since we modified the way the we serialize the args any subsequent query would return the cache data, even if new meters were requested @@ -96,7 +96,7 @@ export const readingsApi = baseApi.injectEndpoints({ bar: builder.query({ // Refer to line endpoint for detailed explanation as the logic is identical serializeQueryArgs: ({ queryArgs }) => _.omit(queryArgs, 'ids'), - merge: (currentCacheData, responseData) => { _.merge(currentCacheData, responseData) }, + merge: (currentCacheData, responseData) => { Object.assign(currentCacheData, responseData) }, forceRefetch: ({ currentArg, endpointState }) => { const currentData = endpointState?.data ? Object.keys(endpointState.data).map(Number) : undefined if (!currentData) { return true } diff --git a/src/client/app/redux/api/userApi.ts b/src/client/app/redux/api/userApi.ts index 4a6dab595..b5ab97b16 100644 --- a/src/client/app/redux/api/userApi.ts +++ b/src/client/app/redux/api/userApi.ts @@ -25,7 +25,9 @@ export const userApi = baseApi.injectEndpoints({ query: users => ({ url: 'api/users/edit', method: 'POST', - body: { users } + body: { users }, + // Response not json. Use 'text' responseHandler to parsing errors avoid + responseHandler: 'text' }), invalidatesTags: ['Users'] }), From 54e03193765eb1f6d59ba3c288c53d1e380ad294 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Sun, 22 Oct 2023 18:00:17 +0000 Subject: [PATCH 031/131] Add ESLint for React-Hooks --- .eslintrc.json | 5 +- package-lock.json | 117 ++++++++++------------------------------------ package.json | 1 + 3 files changed, 29 insertions(+), 94 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index e9a414957..d38ee887b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -10,8 +10,9 @@ }, "extends": [ "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:jsdoc/recommended-typescript" + "plugin:@typescript-eslint/recommended", + "plugin:jsdoc/recommended-typescript", + "plugin:react-hooks/recommended" ], "parser": "@typescript-eslint/parser", "parserOptions": { diff --git a/package-lock.json b/package-lock.json index 06cdd8293..b586a4df7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "csv-parse": "~4.16.3", "csv-stringify": "~5.6.5", "dotenv": "~16.0.3", + "eslint-plugin-react-hooks": "~4.6.0", "express": "~4.17.3", "express-rate-limit": "~5.5.1", "history": "~4.7.2", @@ -105,7 +106,6 @@ "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -1936,7 +1936,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", - "dev": true, "dependencies": { "eslint-visitor-keys": "^3.3.0" }, @@ -1951,7 +1950,6 @@ "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.9.1.tgz", "integrity": "sha512-Y27x+MBLjXa+0JWDhykM3+JE+il3kHKAEqabfEWq3SDhZjLYb6/BHL/JKFnH3fe207JaXkyDo685Oc2Glt6ifA==", - "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -1960,7 +1958,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", - "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -1983,7 +1980,6 @@ "version": "13.23.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", - "dev": true, "dependencies": { "type-fest": "^0.20.2" }, @@ -1998,7 +1994,6 @@ "version": "8.48.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.48.0.tgz", "integrity": "sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -2301,7 +2296,6 @@ "version": "0.11.11", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", - "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", @@ -2315,7 +2309,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, "engines": { "node": ">=12.22" }, @@ -2327,8 +2320,7 @@ "node_modules/@humanwhocodes/object-schema": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", @@ -2456,7 +2448,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -2469,7 +2460,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "engines": { "node": ">= 8" } @@ -2478,7 +2468,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -3535,7 +3524,6 @@ "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -3556,7 +3544,6 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -3581,7 +3568,6 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3620,7 +3606,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "engines": { "node": ">=8" } @@ -3666,8 +3651,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/array-bounds": { "version": "1.0.1", @@ -4885,7 +4869,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -5220,7 +5203,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -5421,7 +5403,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, "dependencies": { "esutils": "^2.0.2" }, @@ -5811,7 +5792,6 @@ "version": "8.48.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.48.0.tgz", "integrity": "sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==", - "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -5929,11 +5909,21 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", + "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -5949,7 +5939,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -5961,7 +5950,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -5976,7 +5964,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5992,7 +5979,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -6003,14 +5989,12 @@ "node_modules/eslint/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "engines": { "node": ">=10" }, @@ -6022,7 +6006,6 @@ "version": "13.23.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", - "dev": true, "dependencies": { "type-fest": "^0.20.2" }, @@ -6037,7 +6020,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -6046,7 +6028,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -6058,7 +6039,6 @@ "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -6087,7 +6067,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -6099,7 +6078,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -6111,7 +6089,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, "engines": { "node": ">=4.0" } @@ -6332,8 +6309,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.3.1", @@ -6374,8 +6350,7 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fast-levenshtein": { "version": "2.0.6", @@ -6395,7 +6370,6 @@ "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, "dependencies": { "reusify": "^1.0.4" } @@ -6404,7 +6378,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", - "dev": true, "dependencies": { "flat-cache": "^3.0.4" }, @@ -6500,7 +6473,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -6525,7 +6497,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", - "dev": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -6538,8 +6509,7 @@ "node_modules/flatted": { "version": "3.2.9", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "dev": true + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" }, "node_modules/flatten-vertex-data": { "version": "1.0.2", @@ -6859,7 +6829,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -7103,8 +7072,7 @@ "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" }, "node_modules/grid-index": { "version": "1.1.0", @@ -7359,7 +7327,6 @@ "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true, "engines": { "node": ">= 4" } @@ -7423,7 +7390,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "engines": { "node": ">=0.8.19" } @@ -7616,7 +7582,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -7668,7 +7633,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -7738,7 +7702,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -7808,8 +7771,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/isobject": { "version": "3.0.1", @@ -7867,7 +7829,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -7899,8 +7860,7 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -7910,8 +7870,7 @@ "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/json-stable-stringify": { "version": "1.0.2", @@ -7928,8 +7887,7 @@ "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" }, "node_modules/json5": { "version": "2.2.3", @@ -8045,7 +8003,6 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, "dependencies": { "json-buffer": "3.0.1" } @@ -8063,7 +8020,6 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -8104,7 +8060,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -8853,8 +8808,7 @@ "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" }, "node_modules/needle": { "version": "2.9.1", @@ -9223,7 +9177,6 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, "dependencies": { "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", @@ -9246,7 +9199,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -9261,7 +9213,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -9374,7 +9325,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "engines": { "node": ">=8" } @@ -9391,7 +9341,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -9898,7 +9847,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, "engines": { "node": ">= 0.8.0" } @@ -9992,7 +9940,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true, "engines": { "node": ">=6" } @@ -10049,7 +9996,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -10994,7 +10940,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -11009,7 +10954,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, "dependencies": { "glob": "^7.1.3" }, @@ -11034,7 +10978,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -11300,7 +11243,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -11312,7 +11254,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -11678,7 +11619,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -11690,7 +11630,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, "engines": { "node": ">=8" }, @@ -11953,8 +11892,7 @@ "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" }, "node_modules/through2": { "version": "2.0.5", @@ -12249,7 +12187,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -12270,7 +12207,6 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, "engines": { "node": ">=10" }, @@ -12423,7 +12359,6 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -12744,7 +12679,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -12971,7 +12905,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "engines": { "node": ">=10" }, diff --git a/package.json b/package.json index 0164c5244..911f2a7fc 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "csv-parse": "~4.16.3", "csv-stringify": "~5.6.5", "dotenv": "~16.0.3", + "eslint-plugin-react-hooks": "~4.6.0", "express": "~4.17.3", "express-rate-limit": "~5.5.1", "history": "~4.7.2", From cffcaca49eb6c00aa8ca36074a5b449e594c1a3d Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Mon, 23 Oct 2023 01:53:08 +0000 Subject: [PATCH 032/131] Add Translation TS Completions --- .../app/translations/{data.js => data.ts} | 13 +++++++-- src/client/app/utils/translate.ts | 28 +++++++++---------- 2 files changed, 24 insertions(+), 17 deletions(-) rename src/client/app/translations/{data.js => data.ts} (99%) diff --git a/src/client/app/translations/data.js b/src/client/app/translations/data.ts similarity index 99% rename from src/client/app/translations/data.js rename to src/client/app/translations/data.ts index 4fac15175..1756b2584 100644 --- a/src/client/app/translations/data.js +++ b/src/client/app/translations/data.ts @@ -2,12 +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 { localeData } from 'moment'; /* eslint-disable */ // This file used to be a json file, but had issues with importing, so we declared the json variable in a js file instead. -const localeData = { +const LocaleTranslationData = { "en": { "3D": "3D", "400": "400 Bad Request", @@ -1405,4 +1404,12 @@ const localeData = { } } -export default localeData; +// Infer +export default LocaleTranslationData as typeof LocaleTranslationData; +export type TranslationKey = keyof typeof LocaleTranslationData +// All locales should share the same keys, but intersection over all to be safe? +// Will probably error when forgetting to add same key to all locales when using translate() +export type LocaleDataKey = + keyof typeof LocaleTranslationData['en'] & + keyof typeof LocaleTranslationData['es'] & + keyof typeof LocaleTranslationData['fr'] \ No newline at end of file diff --git a/src/client/app/utils/translate.ts b/src/client/app/utils/translate.ts index aaa699fc6..e8d29b243 100644 --- a/src/client/app/utils/translate.ts +++ b/src/client/app/utils/translate.ts @@ -1,30 +1,28 @@ /* 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/. */ +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { defineMessages, createIntl, createIntlCache } from 'react-intl'; +import { LocaleDataKey, TranslationKey } from '../translations/data'; import localeData from '../translations/data'; -import { store } from '../store'; -// TODO This used to be multiple types of: -// const enum AsTranslated {} -// export type TranslatedString = string & AsTranslated; -// but that started to cause problems and string was found to be okay. -// If this works then maybe we remove TranslatedString and just use string? -export type TranslatedString = string; +import { store } from '../store'; +// Function overloads to add TS Completions support +function translate(messageID: LocaleDataKey): string; +function translate(messageID: string): string; /** - * Translate a message + * Translate a message with given parameter as translation Key. * @param messageID identifier for a message - * @returns get translated string from original string + * @returns get translated string given a key */ -export default function translate(messageID: string): TranslatedString { +function translate(messageID: LocaleDataKey | string): string { // TODO BANDAID FIX // Application wasn't loading due to store.getState() returning undefined after adding call to translation in GraphicRateMenuComponent // My guess is that the call to store.getState() was too early as the store hadn't finished loading completely // For now, set the default language to english and any component subscribed to the language state should properly re-render if the language changes - let lang = 'en'; + let lang: TranslationKey = 'en'; if (store) { lang = store.getState().options.selectedLanguage; } @@ -33,8 +31,10 @@ export default function translate(messageID: string): TranslatedString { const lang = state.options.selectedLanguage; */ - const messages = (localeData as any)[lang]; + const messages = (localeData)[lang]; const cache = createIntlCache(); const intl = createIntl({ locale: lang, messages }, cache); - return intl.formatMessage(defineMessages({ [messageID]: { id: messageID } })[messageID]) as TranslatedString; + return intl.formatMessage(defineMessages({ [messageID]: { id: messageID } })[messageID]); } + +export default translate \ No newline at end of file From 8eb9114f5a8cc9f51eba5d766423a9a1d3c9442a Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Wed, 1 Nov 2023 00:05:59 +0000 Subject: [PATCH 033/131] Drop Meter, Group, and Unit Slices/Reducer - All data will now live in API reducers - Many Misc Changes to admin pages. --- src/client/app/actions/graph.ts | 4 - src/client/app/actions/groups.ts | 342 +++--- .../components/AreaUnitSelectComponent.tsx | 15 +- .../app/components/BarChartComponent.tsx | 13 +- src/client/app/components/ExportComponent.tsx | 87 +- .../components/GraphicRateMenuComponent.tsx | 11 +- .../app/components/HeaderButtonsComponent.tsx | 9 +- .../components/InitializationComponent.tsx | 11 +- .../app/components/LineChartComponent.tsx | 16 +- .../MeterAndGroupSelectComponent.tsx | 2 +- src/client/app/components/RouteComponent.tsx | 4 +- .../app/components/RouteComponentWIP.tsx | 67 +- src/client/app/components/TimeZoneSelect.tsx | 10 +- .../app/components/UnitSelectComponent.tsx | 4 +- .../components/UnsavedWarningComponent.tsx | 2 +- .../components/UnsavedWarningComponentWIP.tsx | 57 +- .../app/components/admin/AdminComponent.tsx | 20 +- .../admin/CreateUserComponentWIP.tsx | 3 +- .../admin/PreferencesComponentWIP.tsx | 377 ++++++ .../admin/UsersDetailComponentWIP.tsx | 35 +- .../conversion/ConversionsDetailComponent.tsx | 2 +- .../ConversionsDetailComponentWIP.tsx | 90 ++ .../CreateConversionModalComponentWIP.tsx | 335 +++++ .../csv/MetersCSVUploadComponent.tsx | 12 +- .../groups/CreateGroupModalComponent.tsx | 42 +- .../groups/CreateGroupModalComponentWIP.tsx | 564 +++++++++ .../groups/EditGroupModalComponent.tsx | 59 +- .../groups/EditGroupModalComponentWIP.tsx | 1087 +++++++++++++++++ .../components/groups/GroupViewComponent.tsx | 14 +- .../groups/GroupViewComponentWIP.tsx | 88 ++ .../groups/GroupsDetailComponent.tsx | 14 +- .../groups/GroupsDetailComponentWIP.tsx | 80 ++ .../meters/CreateMeterModalComponent.tsx | 26 +- .../meters/CreateMeterModalComponentWIP.tsx | 849 +++++++++++++ .../meters/EditMeterModalComponentWIP.tsx | 748 ++++++++++++ .../meters/MeterViewComponentWIP.tsx | 92 ++ .../meters/MetersDetailComponent.tsx | 5 +- .../meters/MetersDetailComponentWIP.tsx | 82 ++ .../unit/EditUnitModalComponent.tsx | 29 +- .../app/components/unit/UnitViewComponent.tsx | 3 +- .../components/unit/UnitsDetailComponent.tsx | 4 +- src/client/app/reducers/admin.ts | 27 +- src/client/app/reducers/groups.ts | 84 +- src/client/app/reducers/index.ts | 15 +- src/client/app/reducers/meters.ts | 15 +- src/client/app/reducers/options.ts | 7 + src/client/app/reducers/units.ts | 4 +- src/client/app/redux/api/conversionsApi.ts | 23 +- src/client/app/redux/api/groupsApi.ts | 93 +- src/client/app/redux/api/metersApi.ts | 63 +- src/client/app/redux/api/preferencesApi.ts | 3 +- src/client/app/redux/api/unitsApi.ts | 16 +- .../app/redux/selectors/adminSelectors.ts | 283 +++++ .../app/redux/selectors/dataSelectors.ts | 12 +- .../app/redux/selectors/threeDSelectors.ts | 19 +- src/client/app/redux/selectors/uiSelectors.ts | 139 +-- src/client/app/types/redux/groups.ts | 52 +- src/client/app/types/redux/meters.ts | 1 - src/client/app/utils/api/GroupsApi.ts | 8 +- .../app/utils/determineCompatibleUnits.ts | 71 +- src/client/app/utils/exportData.ts | 6 +- src/client/app/utils/notifications.ts | 5 +- 62 files changed, 5527 insertions(+), 733 deletions(-) create mode 100644 src/client/app/components/admin/PreferencesComponentWIP.tsx create mode 100644 src/client/app/components/conversion/ConversionsDetailComponentWIP.tsx create mode 100644 src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx create mode 100644 src/client/app/components/groups/CreateGroupModalComponentWIP.tsx create mode 100644 src/client/app/components/groups/EditGroupModalComponentWIP.tsx create mode 100644 src/client/app/components/groups/GroupViewComponentWIP.tsx create mode 100644 src/client/app/components/groups/GroupsDetailComponentWIP.tsx create mode 100644 src/client/app/components/meters/CreateMeterModalComponentWIP.tsx create mode 100644 src/client/app/components/meters/EditMeterModalComponentWIP.tsx create mode 100644 src/client/app/components/meters/MeterViewComponentWIP.tsx create mode 100644 src/client/app/components/meters/MetersDetailComponentWIP.tsx create mode 100644 src/client/app/redux/selectors/adminSelectors.ts diff --git a/src/client/app/actions/graph.ts b/src/client/app/actions/graph.ts index 7da9db504..d4a1d6c92 100644 --- a/src/client/app/actions/graph.ts +++ b/src/client/app/actions/graph.ts @@ -13,11 +13,9 @@ import { ComparePeriod, SortingOrder } from '../utils/calculateCompare'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; import { fetchNeededBarReadings } from './barReadings'; import { fetchNeededCompareReadings } from './compareReadings'; -import { fetchGroupsDetailsIfNeeded } from './groups'; import { fetchNeededLineReadings } from './lineReadings'; import { changeSelectedMap } from './map'; import { fetchNeededMapReadings } from './mapReadings'; -import { fetchMetersDetailsIfNeeded } from './meters'; import { fetchUnitsDetailsIfNeeded } from './units'; export function setHotlinkedAsync(hotlinked: boolean): Thunk { @@ -204,11 +202,9 @@ export function changeOptionsFromLink(options: LinkOptions) { const dispatchFirst: Thunk[] = [setHotlinkedAsync(true)]; const dispatchSecond: Array> = []; if (options.meterIDs) { - dispatchFirst.push(fetchMetersDetailsIfNeeded()); dispatchSecond.push(changeSelectedMeters(options.meterIDs)); } if (options.groupIDs) { - dispatchFirst.push(fetchGroupsDetailsIfNeeded()); dispatchSecond.push(changeSelectedGroups(options.groupIDs)); } if (options.meterOrGroupID && options.meterOrGroup) { diff --git a/src/client/app/actions/groups.ts b/src/client/app/actions/groups.ts index 90712cac2..45a2809c6 100644 --- a/src/client/app/actions/groups.ts +++ b/src/client/app/actions/groups.ts @@ -1,185 +1,185 @@ -/* 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/. */ +// /* 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 { Dispatch, GetState, Thunk } from '../types/redux/actions'; -import { State } from '../types/redux/state'; -import { showErrorNotification, showSuccessNotification } from '../utils/notifications'; -import * as t from '../types/redux/groups'; -import { groupsApi } from '../utils/api'; -import translate from '../utils/translate'; -import { groupsSlice } from '../reducers/groups'; +// import { Dispatch, GetState, Thunk } from '../types/redux/actions'; +// import { State } from '../types/redux/state'; +// import { showErrorNotification, showSuccessNotification } from '../utils/notifications'; +// import * as t from '../types/redux/groups'; +// import { groupsApi } from '../utils/api'; +// import translate from '../utils/translate'; +// import { groupsSlice } from '../reducers/groups'; +// export function fetchGroupsDetails(): Thunk { +// return async (dispatch: Dispatch, getState: GetState) => { +// dispatch(groupsSlice.actions.requestGroupsDetails()); +// // Returns the names, IDs and most info of all groups in the groups table. +// const groupsDetails = await groupsApi.details(); +// dispatch(groupsSlice.actions.receiveGroupsDetails(groupsDetails)); +// // If this is the first fetch, inform the store that the first fetch has been made +// if (!getState().groups.hasBeenFetchedOnce) { +// dispatch(groupsSlice.actions.confirmGroupsFetchedOnce()); +// } +// }; +// } -export function fetchGroupsDetails(): Thunk { - return async (dispatch: Dispatch, getState: GetState) => { - dispatch(groupsSlice.actions.requestGroupsDetails()); - // Returns the names, IDs and most info of all groups in the groups table. - const groupsDetails = await groupsApi.details(); - dispatch(groupsSlice.actions.receiveGroupsDetails(groupsDetails)); - // If this is the first fetch, inform the store that the first fetch has been made - if (!getState().groups.hasBeenFetchedOnce) { - dispatch(groupsSlice.actions.confirmGroupsFetchedOnce()); - } - }; -} +// function shouldFetchGroupsDetails(state: State): boolean { +// // If isFetching then don't do this. If already fetched then don't do this. +// return !state.groups.isFetching && !state.groups.hasBeenFetchedOnce; +// } -function shouldFetchGroupsDetails(state: State): boolean { - // If isFetching then don't do this. If already fetched then don't do this. - return !state.groups.isFetching && !state.groups.hasBeenFetchedOnce; -} +// export function fetchGroupsDetailsIfNeeded(): Thunk { +// return (dispatch: Dispatch, getState: GetState) => { +// if (shouldFetchGroupsDetails(getState())) { +// return dispatch(fetchGroupsDetails()); +// } +// return Promise.resolve(); +// }; +// } -export function fetchGroupsDetailsIfNeeded(): Thunk { - return (dispatch: Dispatch, getState: GetState) => { - if (shouldFetchGroupsDetails(getState())) { - return dispatch(fetchGroupsDetails()); - } - return Promise.resolve(); - }; -} +// // The following 3 functions do a single groups at a time. They were used +// // before the group modals. They are being left in case we want them in +// // the future, esp. if modals do not load all at start as they now do. +// // They used to have outdated but removed since not used by new code. +// function shouldFetchGroupChildren(state: State, groupID: number) { +// const group = state.groups.byGroupID[groupID]; +// // Check that it is not being fetched. +// return !group.isFetching; +// } -// The following 3 functions do a single groups at a time. They were used -// before the group modals. They are being left in case we want them in -// the future, esp. if modals do not load all at start as they now do. -// They used to have outdated but removed since not used by new code. -function shouldFetchGroupChildren(state: State, groupID: number) { - const group = state.groups.byGroupID[groupID]; - // Check that it is not being fetched. - return !group.isFetching; -} +// function fetchGroupChildren(groupID: number) { +// return async (dispatch: Dispatch) => { +// dispatch(groupsSlice.actions.requestGroupChildren(groupID)); +// const childGroupIDs = await groupsApi.children(groupID); +// dispatch(groupsSlice.actions.receiveGroupChildren({ groupID, data: childGroupIDs })); +// }; +// } -function fetchGroupChildren(groupID: number) { - return async (dispatch: Dispatch) => { - dispatch(groupsSlice.actions.requestGroupChildren(groupID)); - const childGroupIDs = await groupsApi.children(groupID); - dispatch(groupsSlice.actions.receiveGroupChildren({ groupID, data: childGroupIDs })); - }; -} +// export function fetchGroupChildrenIfNeeded(groupID: number) { +// return (dispatch: Dispatch, getState: GetState) => { +// if (shouldFetchGroupChildren(getState(), groupID)) { +// return dispatch(fetchGroupChildren(groupID)); +// } +// return Promise.resolve(); +// }; +// } -export function fetchGroupChildrenIfNeeded(groupID: number) { - return (dispatch: Dispatch, getState: GetState) => { - if (shouldFetchGroupChildren(getState(), groupID)) { - return dispatch(fetchGroupChildren(groupID)); - } - return Promise.resolve(); - }; -} +// // The following functions get the immediate children meters and groups of all groups. +// // They are not currently used but left for now. -// The following functions get the immediate children meters and groups of all groups. -// They are not currently used but left for now. +// function fetchAllGroupChildren(): Thunk { +// return async (dispatch: Dispatch, getState: GetState) => { +// // ensure a fetch is not currently happening +// if (!getState().groups.isFetchingAllChildren) { +// // set isFetching to true +// dispatch(groupsSlice.actions.requestAllGroupsChildren()); +// // Retrieve all groups children from database +// const groupsChildren = await groupsApi.getAllGroupsChildren(); +// // update the state with all groups children +// dispatch(groupsSlice.actions.receiveAllGroupsChildren(groupsChildren)); +// // If this is the first fetch, inform the store that the first fetch has been made +// if (!getState().groups.hasChildrenBeenFetchedOnce) { +// dispatch(groupsSlice.actions.confirmAllGroupsChildrenFetchedOnce()); +// } +// } +// } +// } -function fetchAllGroupChildren(): Thunk { - return async (dispatch: Dispatch, getState: GetState) => { - // ensure a fetch is not currently happening - if (!getState().groups.isFetchingAllChildren) { - // set isFetching to true - dispatch(groupsSlice.actions.requestAllGroupsChildren()); - // Retrieve all groups children from database - const groupsChildren = await groupsApi.getAllGroupsChildren(); - // update the state with all groups children - dispatch(groupsSlice.actions.receiveAllGroupsChildren(groupsChildren)); - // If this is the first fetch, inform the store that the first fetch has been made - if (!getState().groups.hasChildrenBeenFetchedOnce) { - dispatch(groupsSlice.actions.confirmAllGroupsChildrenFetchedOnce()); - } - } - } -} +// export function fetchAllGroupChildrenIfNeeded(): Thunk { +// return (dispatch: Dispatch, getState: GetState) => { +// // If groups have not been fetched once (or that reset) then try to fetch. +// if (!getState().groups.hasChildrenBeenFetchedOnce) { +// return dispatch(fetchAllGroupChildren()); +// } +// return Promise.resolve(); +// }; +// } -export function fetchAllGroupChildrenIfNeeded(): Thunk { - return (dispatch: Dispatch, getState: GetState) => { - // If groups have not been fetched once (or that reset) then try to fetch. - if (!getState().groups.hasChildrenBeenFetchedOnce) { - return dispatch(fetchAllGroupChildren()); - } - return Promise.resolve(); - }; -} +// /* +// * The `submitNewGroup` and `submitGroupEdits` functions are called by +// * `submitGroupInEditingIfNeeded` to handle sending the API request +// * and processing the response. +// */ +// export function submitNewGroup(group: t.GroupData): Dispatch { +// return async (dispatch: Dispatch) => { +// try { +// await groupsApi.create(group); +// // Update the groups 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 GroupData and it is not returned we do the get. +// // We also need to get the child meters/groups of the new group. +// // We can just fetch this one group but instead get all the groups since easier and this +// // is not a common operation. We must wait for the new group state so its substate for children can be set. +// dispatch(fetchGroupsDetails()).then(() => dispatch(fetchAllGroupChildren())); +// showSuccessNotification(translate('group.successfully.create.group')); +// } catch (err) { +// // Failure! ): +// // TODO Better way than popup with React but want to stay so user can read/copy. +// window.alert(translate('group.failed.to.edit.group') + '"' + err.response.data as string + '"'); +// // Clear our changes from to the submitting meters state +// // We must do this in case fetch failed to keep the store in sync with the database +// } +// }; +// } -/* - * The `submitNewGroup` and `submitGroupEdits` functions are called by - * `submitGroupInEditingIfNeeded` to handle sending the API request - * and processing the response. - */ -export function submitNewGroup(group: t.GroupData): Dispatch { - return async (dispatch: Dispatch) => { - try { - await groupsApi.create(group); - // Update the groups 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 GroupData and it is not returned we do the get. - // We also need to get the child meters/groups of the new group. We can just fetch this one group but instead get all the groups since easier and this - // is not a common operation. We must wait for the new group state so its substate for children can be set. - dispatch(fetchGroupsDetails()).then(() => dispatch(fetchAllGroupChildren())); - showSuccessNotification(translate('group.successfully.create.group')); - } catch (err) { - // Failure! ): - // TODO Better way than popup with React but want to stay so user can read/copy. - window.alert(translate('group.failed.to.edit.group') + '"' + err.response.data as string + '"'); - // Clear our changes from to the submitting meters state - // We must do this in case fetch failed to keep the store in sync with the database - } - }; -} +// /** +// * Pushes group changes out to DB. +// * @param group The group to update +// * @param reload If true, the window is reloaded to reset everything on change +// * @returns Function to do this for an action +// */ +// export function submitGroupEdits(group: t.GroupEditData, reload: boolean = true): Thunk { +// return async (dispatch: Dispatch) => { +// try { +// // deepMeters is part of the group state but it is not sent on edit route so remove. +// // Need deep copy so changes don't impact original but not really important if reload. +// const groupNoDeep = { ...group }; +// delete groupNoDeep.deepMeters; +// await groupsApi.edit(groupNoDeep); +// // See deleteGroup action for full description but we reload the window +// // to avoid issues with change from one group impacting another. +// // Update the store for all groups. +// // TODO We should limit this to the times it is needed and not all group edits. +// if (reload) { +// window.location.reload(); +// } else { +// // If we did not reload then we need to refresh the edited group's state with: +// dispatch(groupsSlice.actions.confirmEditedGroup(group)); +// // An then we need to fix up any other groups impacted. +// // This is removed since you won't see it. +// // Success! +// showSuccessNotification(translate('group.successfully.edited.group')); +// } +// } catch (e) { +// if (e.response.data.message && e.response.data.message === 'Cyclic group detected') { +// showErrorNotification(translate('you.cannot.create.a.cyclic.group')); +// } else { +// showErrorNotification(translate('group.failed.to.edit.group') + ' "' + e.response.data as string + '"'); +// } +// } +// }; +// } -/** - * Pushes group changes out to DB. - * @param group The group to update - * @param reload If true, the window is reloaded to reset everything on change - * @returns Function to do this for an action - */ -export function submitGroupEdits(group: t.GroupEditData, reload: boolean = true): Thunk { - return async (dispatch: Dispatch) => { - try { - // deepMeters is part of the group state but it is not sent on edit route so remove. - // Need deep copy so changes don't impact original but not really important if reload. - const groupNoDeep = { ...group }; - delete groupNoDeep.deepMeters; - await groupsApi.edit(groupNoDeep); - // See deleteGroup action for full description but we reload the window - // to avoid issues with change from one group impacting another. - // Update the store for all groups. - // TODO We should limit this to the times it is needed and not all group edits. - if (reload) { - window.location.reload(); - } else { - // If we did not reload then we need to refresh the edited group's state with: - dispatch(groupsSlice.actions.confirmEditedGroup(group)); - // An then we need to fix up any other groups impacted. - // This is removed since you won't see it. - // Success! - showSuccessNotification(translate('group.successfully.edited.group')); - } - } catch (e) { - if (e.response.data.message && e.response.data.message === 'Cyclic group detected') { - showErrorNotification(translate('you.cannot.create.a.cyclic.group')); - } else { - showErrorNotification(translate('group.failed.to.edit.group') + ' "' + e.response.data as string + '"'); - } - } - }; -} - -export function deleteGroup(group: t.GroupEditData): Dispatch { - // TODO This no longer does a dispatch so it may need to be reworked. - // For now, get to ignore eslint issue. - /* eslint-disable @typescript-eslint/no-unused-vars */ - return async (dispatch: Dispatch) => { - /* eslint-enable @typescript-eslint/no-unused-vars */ - try { - await groupsApi.delete(group.id); - // We need to remove this group from Redux state. Also, other groups - // can be changed if they included this group. It should only impact - // the immediate group children and the deep meters. If any of these - // groups are being graphed then their readings, etc. need to be updated. - // Given this isn't done very often and only by an admin, the code - // reloads the browser so the state is fixed and any graphing is removed. - // We could just fix the state but that is more complex and the code was - // having issues redoing the useEffect for edit in this case. - window.location.reload(); - } catch (e) { - showErrorNotification(translate('failed.to.delete.group')); - } - }; -} +// export function deleteGroup(group: t.GroupEditData): Dispatch { +// // TODO This no longer does a dispatch so it may need to be reworked. +// // For now, get to ignore eslint issue. +// /* eslint-disable @typescript-eslint/no-unused-vars */ +// return async (dispatch: Dispatch) => { +// /* eslint-enable @typescript-eslint/no-unused-vars */ +// try { +// await groupsApi.delete(group.id); +// // We need to remove this group from Redux state. Also, other groups +// // can be changed if they included this group. It should only impact +// // the immediate group children and the deep meters. If any of these +// // groups are being graphed then their readings, etc. need to be updated. +// // Given this isn't done very often and only by an admin, the code +// // reloads the browser so the state is fixed and any graphing is removed. +// // We could just fix the state but that is more complex and the code was +// // having issues redoing the useEffect for edit in this case. +// window.location.reload(); +// } catch (e) { +// showErrorNotification(translate('failed.to.delete.group')); +// } +// }; +// } diff --git a/src/client/app/components/AreaUnitSelectComponent.tsx b/src/client/app/components/AreaUnitSelectComponent.tsx index 1171b15bf..af2c87b5a 100644 --- a/src/client/app/components/AreaUnitSelectComponent.tsx +++ b/src/client/app/components/AreaUnitSelectComponent.tsx @@ -4,15 +4,16 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import Select from 'react-select'; +import { useAppSelector } from '../redux/hooks'; +import { graphSlice } from '../reducers/graph'; +import { selectUnitDataById } from '../redux/api/unitsApi'; import { StringSelectOption } from '../types/items'; -import { State } from '../types/redux/state'; +import { UnitRepresentType } from '../types/redux/units'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; import translate from '../utils/translate'; -import { UnitRepresentType } from '../types/redux/units'; import TooltipMarkerComponent from './TooltipMarkerComponent'; -import { graphSlice } from '../reducers/graph'; /** * React Component that creates the area unit selector dropdown @@ -21,8 +22,8 @@ import { graphSlice } from '../reducers/graph'; export default function AreaUnitSelectComponent() { const dispatch = useDispatch(); - const graphState = useSelector((state: State) => state.graph); - const unitState = useSelector((state: State) => state.units.units); + const graphState = useAppSelector(state => state.graph); + const { data: unitDataById = {} } = useAppSelector(selectUnitDataById); // Array of select options created from the area unit enum const unitOptions: StringSelectOption[] = []; @@ -46,7 +47,7 @@ export default function AreaUnitSelectComponent() { margin: 0 }; - if (graphState.selectedUnit != -99 && unitState[graphState.selectedUnit].unitRepresent === UnitRepresentType.raw) { + if (graphState.selectedUnit != -99 && unitDataById[graphState.selectedUnit].unitRepresent === UnitRepresentType.raw) { return null; } diff --git a/src/client/app/components/BarChartComponent.tsx b/src/client/app/components/BarChartComponent.tsx index ee8f1d508..bd60741d7 100644 --- a/src/client/app/components/BarChartComponent.tsx +++ b/src/client/app/components/BarChartComponent.tsx @@ -9,9 +9,7 @@ import * as React from 'react'; import Plot from 'react-plotly.js'; import { TimeInterval } from '../../../common/TimeInterval'; import { graphSlice, selectSelectedGroups, selectSelectedMeters } from '../reducers/graph'; -import { groupsSlice } from '../reducers/groups'; -import { metersSlice } from '../reducers/meters'; -import { unitsSlice } from '../reducers/units'; +import { selectMeterDataById } from '../redux/api/metersApi'; import { readingsApi } from '../redux/api/readingsApi'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { BarReadingApiArgs, ChartMultiQueryProps } from '../redux/selectors/dataSelectors'; @@ -22,6 +20,8 @@ import getGraphColor from '../utils/getGraphColor'; import { barUnitLabel } from '../utils/graphics'; import translate from '../utils/translate'; import SpinnerComponent from './SpinnerComponent'; +import { selectGroupDataById } from '../redux/api/groupsApi'; +import { selectUnitDataById } from '../redux/api/unitsApi'; /** * Passes the current redux state of the barchart, and turns it into props for the React @@ -38,13 +38,14 @@ export default function BarChartComponent(props: ChartMultiQueryProps state.graph.selectedUnit); // The unit label depends on the unit which is in selectUnit state. const graphingUnit = useAppSelector(state => state.graph.selectedUnit); - const unitDataByID = useAppSelector(state => unitsSlice.selectors.selectUnitDataById(state)); + const { data: unitDataByID = {} } = useAppSelector(selectUnitDataById); + const selectedAreaNormalization = useAppSelector(state => state.graph.areaNormalization); const selectedAreaUnit = useAppSelector(state => state.graph.selectedAreaUnit); const selectedMeters = useAppSelector(selectSelectedMeters); const selectedGroups = useAppSelector(selectSelectedGroups); - const meterDataByID = useAppSelector(state => metersSlice.selectors.selectMeterDataByID(state)); - const groupDataByID = useAppSelector(state => groupsSlice.selectors.selectGroupDataByID(state)); + const { data: meterDataByID = {} } = useAppSelector(selectMeterDataById); + const { data: groupDataByID = {} } = useAppSelector(selectGroupDataById); // useQueryHooks for data fetching const { data: meterReadings, isLoading: meterIsFetching } = readingsApi.useBarQuery(meterArgs, { skip: meterSkipQuery }); diff --git a/src/client/app/components/ExportComponent.tsx b/src/client/app/components/ExportComponent.tsx index c928455f2..be9f9983a 100644 --- a/src/client/app/components/ExportComponent.tsx +++ b/src/client/app/components/ExportComponent.tsx @@ -2,23 +2,24 @@ * 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 { Button } from 'reactstrap'; import * as _ from 'lodash'; -import graphExport, { downloadRawCSV } from '../utils/exportData'; +import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { metersApi } from '../utils/api' -import TooltipMarkerComponent from './TooltipMarkerComponent'; -import { State } from '../types/redux/state'; -import { useSelector } from 'react-redux'; -import { hasToken } from '../utils/token'; -import { usersApi } from '../utils/api' +import { Button } from 'reactstrap'; +import { selectGroupDataById } from '../redux/api/groupsApi'; +import { selectMeterDataById } from '../redux/api/metersApi'; +import { selectUnitDataById } from '../redux/api/unitsApi'; +import { useAppSelector } from '../redux/hooks'; import { UserRole } from '../types/items'; -import translate from '../utils/translate'; -import { ChartTypes, MeterOrGroup } from '../types/redux/graph'; -import { lineUnitLabel, barUnitLabel } from '../utils/graphics'; import { ConversionData } from '../types/redux/conversions'; +import { ChartTypes, MeterOrGroup } from '../types/redux/graph'; +import { metersApi, usersApi } from '../utils/api'; +import graphExport, { downloadRawCSV } from '../utils/exportData'; import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; +import { barUnitLabel, lineUnitLabel } from '../utils/graphics'; +import { hasToken } from '../utils/token'; +import translate from '../utils/translate'; +import TooltipMarkerComponent from './TooltipMarkerComponent'; /** * Creates export buttons and does code for handling export to CSV files. @@ -26,21 +27,21 @@ import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConvers */ export default function ExportComponent() { // Meters state - const metersState = useSelector((state: State) => state.meters.byMeterID); + const { data: metersDataById = {} } = useAppSelector(selectMeterDataById); // Groups state - const groupsState = useSelector((state: State) => state.groups.byGroupID); + const { data: groupsDataById = {} } = useAppSelector(selectGroupDataById); // Units state - const unitsState = useSelector((state: State) => state.units.units); + const { data: unitsDataById = {} } = useAppSelector(selectUnitDataById); // Conversion state - const conversionState = useSelector((state: State) => state.conversions.conversions); + const conversionState = useAppSelector(state => state.conversions.conversions); // graph state - const graphState = useSelector((state: State) => state.graph); + const graphState = useAppSelector(state => state.graph); // admin state - const adminState = useSelector((state: State) => state.admin); + const adminState = useAppSelector(state => state.admin); // readings state - const readingsState = useSelector((state: State) => state.readings); + const readingsState = useAppSelector(state => state.readings); // error bar state - const errorBarState = useSelector((state: State) => state.graph.showMinMax); + const errorBarState = useAppSelector(state => state.graph.showMinMax); // Time range of graphic const timeInterval = graphState.queryTimeInterval; @@ -49,28 +50,28 @@ export default function ExportComponent() { // What unit is being graphed. Unit of all lines to export. const unitId = graphState.selectedUnit; // This is the graphic unit identifier - const unitIdentifier = unitsState[unitId].identifier; + const unitIdentifier = unitsDataById[unitId].identifier; // What type of chart/graphic is being displayed. const chartName = graphState.chartToRender; if (chartName === ChartTypes.line) { // Exporting a line chart // Get the full y-axis unit label for a line - const returned = lineUnitLabel(unitsState[unitId], graphState.lineGraphRate, graphState.areaNormalization, graphState.selectedAreaUnit); + const returned = lineUnitLabel(unitsDataById[unitId], graphState.lineGraphRate, graphState.areaNormalization, graphState.selectedAreaUnit); const unitLabel = returned.unitLabel // The rate will be 1 if it is per hour (since state readings are per hour) or no rate scaling so no change. const rateScaling = returned.needsRateScaling ? graphState.lineGraphRate.rate : 1; // Loop over the displayed meters and export one-by-one. Does nothing if no meters selected. for (const meterId of graphState.selectedMeters) { - const meterArea = metersState[meterId].area; + const meterArea = metersDataById[meterId].area; // export if area normalization is off or the meter can be normalized - if (!graphState.areaNormalization || (meterArea > 0 && metersState[meterId].areaUnit !== AreaUnitType.none)) { + if (!graphState.areaNormalization || (meterArea > 0 && metersDataById[meterId].areaUnit !== AreaUnitType.none)) { // Line readings data for this meter. const byMeterID = readingsState.line.byMeterID[meterId]; // Make sure it exists in case state is not there yet. if (byMeterID !== undefined) { // Convert the meter area into the proper unit if normalizing by area or use 1 if not so won't change reading values. const areaScaling = graphState.areaNormalization ? - meterArea * getAreaUnitConversion(metersState[meterId].areaUnit, graphState.selectedAreaUnit) : 1; + meterArea * getAreaUnitConversion(metersDataById[meterId].areaUnit, graphState.selectedAreaUnit) : 1; // Divide areaScaling into the rate so have complete scaling factor for readings. const scaling = rateScaling / areaScaling; // Get the readings for the time range and unit graphed @@ -87,7 +88,7 @@ export default function ExportComponent() { // Sort by start timestamp. const sortedReadings = _.sortBy(readings, item => item.startTimestamp, 'asc'); // Identifier for current meter. - const meterIdentifier = metersState[meterId].identifier; + const meterIdentifier = metersDataById[meterId].identifier; graphExport(sortedReadings, meterIdentifier, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.meter, errorBarState); } } @@ -96,16 +97,16 @@ export default function ExportComponent() { } // Loop over the displayed groups and export one-by-one. Does nothing if no groups selected. for (const groupId of graphState.selectedGroups) { - const groupArea = groupsState[groupId].area; + const groupArea = groupsDataById[groupId].area; // export if area normalization is off or the group can be normalized - if (!graphState.areaNormalization || (groupArea > 0 && groupsState[groupId].areaUnit !== AreaUnitType.none)) { + if (!graphState.areaNormalization || (groupArea > 0 && groupsDataById[groupId].areaUnit !== AreaUnitType.none)) { // Line readings data for this group. const byGroupID = readingsState.line.byGroupID[groupId]; // Make sure it exists in case state is not there yet. if (byGroupID !== undefined) { // Convert the group area into the proper unit if normalizing by area or use 1 if not so won't change reading values. const areaScaling = graphState.areaNormalization ? - groupArea * getAreaUnitConversion(groupsState[groupId].areaUnit, graphState.selectedAreaUnit) : 1; + groupArea * getAreaUnitConversion(groupsDataById[groupId].areaUnit, graphState.selectedAreaUnit) : 1; // Divide areaScaling into the rate so have complete scaling factor for readings. const scaling = rateScaling / areaScaling; @@ -123,7 +124,7 @@ export default function ExportComponent() { // Sort by start timestamp. const sortedReadings = _.sortBy(readings, item => item.startTimestamp, 'asc'); // Identifier for current group. - const groupName = groupsState[groupId].name; + const groupName = groupsDataById[groupId].name; graphExport(sortedReadings, groupName, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.group); } } @@ -133,13 +134,13 @@ export default function ExportComponent() { } else if (chartName === ChartTypes.bar) { // Exporting a bar chart // Get the full y-axis unit label for a bar - const unitLabel = barUnitLabel(unitsState[unitId], graphState.areaNormalization, graphState.selectedAreaUnit); + const unitLabel = barUnitLabel(unitsDataById[unitId], graphState.areaNormalization, graphState.selectedAreaUnit); // Time width of the bars const barDuration = graphState.barDuration; // Loop over the displayed meters and export one-by-one. Does nothing if no meters selected. for (const meterId of graphState.selectedMeters) { // export if area normalization is off or the meter can be normalized - if (!graphState.areaNormalization || (metersState[meterId].area > 0 && metersState[meterId].areaUnit !== AreaUnitType.none)) { + if (!graphState.areaNormalization || (metersDataById[meterId].area > 0 && metersDataById[meterId].areaUnit !== AreaUnitType.none)) { // Bar readings data for this meter. const byMeterID = readingsState.bar.byMeterID[meterId]; // Make sure it exists in case state is not there yet. @@ -148,7 +149,7 @@ export default function ExportComponent() { let scaling = 1; if (graphState.areaNormalization) { // convert the meter area into the proper unit, if needed - scaling *= getAreaUnitConversion(metersState[meterId].areaUnit, graphState.selectedAreaUnit); + scaling *= getAreaUnitConversion(metersDataById[meterId].areaUnit, graphState.selectedAreaUnit); } const byTimeInterval = byMeterID[timeInterval.toString()]; if (byTimeInterval !== undefined) { @@ -166,7 +167,7 @@ export default function ExportComponent() { // Sort by start timestamp. const sortedReadings = _.sortBy(readings, item => item.startTimestamp, 'asc'); // Identifier for current meter. - const meterIdentifier = metersState[meterId].identifier; + const meterIdentifier = metersDataById[meterId].identifier; graphExport(sortedReadings, meterIdentifier, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.meter); } } @@ -177,7 +178,7 @@ export default function ExportComponent() { // Loop over the displayed groups and export one-by-one. Does nothing if no groups selected. for (const groupId of graphState.selectedGroups) { // export if area normalization is off or the group can be normalized - if (!graphState.areaNormalization || (groupsState[groupId].area > 0 && groupsState[groupId].areaUnit !== AreaUnitType.none)) { + if (!graphState.areaNormalization || (groupsDataById[groupId].area > 0 && groupsDataById[groupId].areaUnit !== AreaUnitType.none)) { // Bar readings data for this group. const byGroupID = readingsState.bar.byGroupID[groupId]; // Make sure it exists in case state is not there yet. @@ -186,7 +187,7 @@ export default function ExportComponent() { let scaling = 1; if (graphState.areaNormalization) { // convert the meter area into the proper unit, if needed - scaling *= getAreaUnitConversion(groupsState[groupId].areaUnit, graphState.selectedAreaUnit); + scaling *= getAreaUnitConversion(groupsDataById[groupId].areaUnit, graphState.selectedAreaUnit); } const byTimeInterval = byGroupID[timeInterval.toString()]; if (byTimeInterval !== undefined) { @@ -204,7 +205,7 @@ export default function ExportComponent() { // Sort by start timestamp. const sortedReadings = _.sortBy(readings, item => item.startTimestamp, 'asc'); // Identifier for current group. - const groupName = groupsState[groupId].name; + const groupName = groupsDataById[groupId].name; graphExport(sortedReadings, groupName, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.group); } } @@ -261,13 +262,13 @@ export default function ExportComponent() { // Loop over each selected meter in graphic. Does nothing if no meters selected. for (const meterId of graphState.selectedMeters) { // export if area normalization is off or the meter can be normalized - if (!graphState.areaNormalization || (metersState[meterId].area > 0 && metersState[meterId].areaUnit !== AreaUnitType.none)) { + if (!graphState.areaNormalization || (metersDataById[meterId].area > 0 && metersDataById[meterId].areaUnit !== AreaUnitType.none)) { // Which selected meter being processed. // const currentMeter = graphState.selectedMeters[i]; // Identifier for current meter. - const currentMeterIdentifier = metersState[meterId].identifier; + const currentMeterIdentifier = metersDataById[meterId].identifier; // The unit of the currentMeter. - const meterUnitId = metersState[meterId].unitId; + const meterUnitId = metersDataById[meterId].unitId; // Note that each meter can have a different unit so look up for each one. let unitIdentifier; // A complication is that a unit associated with a meter is not the one the user @@ -286,17 +287,17 @@ export default function ExportComponent() { if (anyConversion == undefined) { // Could not find a conversion with this meter. This should never happen. // Use the identifier of currentMeter unit and extra info. - unitIdentifier = unitsState[meterUnitId].identifier + + unitIdentifier = unitsDataById[meterUnitId].identifier + ' (this is the meter unit which is unusual)'; // Nice if logged warning but no easy way so don't. } else { // Use this conversion but give slope/destination since changes values. - unitIdentifier = unitsState[anyConversion.destinationId].identifier + + unitIdentifier = unitsDataById[anyConversion.destinationId].identifier + ` (but conversion from meter values of slope = ${anyConversion.slope} and intercept = ${anyConversion.intercept}`; } } else { // This is the typical case where there was a conversion from the meter of 1, 0. - unitIdentifier = unitsState[conversion.destinationId].identifier; + unitIdentifier = unitsDataById[conversion.destinationId].identifier; } // TODO The new line readings route for graphs allows one to get the raw data. Maybe we should try to switch to that and then modify diff --git a/src/client/app/components/GraphicRateMenuComponent.tsx b/src/client/app/components/GraphicRateMenuComponent.tsx index 499eb7349..1fc0f2393 100644 --- a/src/client/app/components/GraphicRateMenuComponent.tsx +++ b/src/client/app/components/GraphicRateMenuComponent.tsx @@ -4,12 +4,13 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import Select from 'react-select'; +import { selectUnitDataById } from '../redux/api/unitsApi'; import { graphSlice } from '../reducers/graph'; +import { useAppSelector } from '../redux/hooks'; import { SelectOption } from '../types/items'; import { LineGraphRate, LineGraphRates } from '../types/redux/graph'; -import { State } from '../types/redux/state'; import { UnitRepresentType } from '../types/redux/units'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; @@ -22,10 +23,10 @@ export default function GraphicRateMenuComponent() { const dispatch = useDispatch(); // Graph state - const graphState = useSelector((state: State) => state.graph); + const graphState = useAppSelector(state => state.graph); // Unit state - const unitDataById = useSelector((state: State) => state.units.units); + const { data: unitDataById = {} } = useAppSelector(selectUnitDataById); // Unit data by Id const selectedUnitData = unitDataById[graphState.selectedUnit]; @@ -40,7 +41,7 @@ export default function GraphicRateMenuComponent() { } } // Also don't show if not the line graphic. - if (graphState.chartToRender !== 'line'){ + if (graphState.chartToRender !== 'line') { shouldRender = false; } // Array of select options created from the rates diff --git a/src/client/app/components/HeaderButtonsComponent.tsx b/src/client/app/components/HeaderButtonsComponent.tsx index 499868e1a..1bfcee1a8 100644 --- a/src/client/app/components/HeaderButtonsComponent.tsx +++ b/src/client/app/components/HeaderButtonsComponent.tsx @@ -34,7 +34,7 @@ export default function HeaderButtonsComponent() { // OED version is needed for help redirect const version = useSelector((state: State) => state.version.version); // Help URL location - let helpUrl = BASE_URL + version; + const helpUrl = BASE_URL + version; // options help const optionsHelp = helpUrl + '/optionsMenu.html'; @@ -84,10 +84,9 @@ export default function HeaderButtonsComponent() { // Must update in case the version was not set when the page was loaded. useEffect(() => { - helpUrl = BASE_URL + version; setState(prevState => ({ ...prevState, - pageChoicesHelp: helpUrl + pageChoicesHelp: BASE_URL + version })); }, [version]); @@ -163,7 +162,7 @@ export default function HeaderButtonsComponent() { pageChoicesHelp: currentPageChoicesHelp, showOptionsStyle: currentShowOptionsStyle })); - }, [currentUser, helpUrl]); + }, [currentPage, currentUser, helpUrl]); // Handle actions on logout. const handleLogOut = () => { @@ -266,7 +265,7 @@ export default function HeaderButtonsComponent() { + to='/login'> selectIsLoggedInAsAdmin(state)); // With RTKQuery, Mutations are used for POST, PUT, PATCH, etc. // The useMutation() hooks returns a tuple containing triggerFunction that can be called to initiate the request @@ -43,12 +40,12 @@ export default function InitializationComponent() { metersApi.useGetMetersQuery(); // Use Query hooks return an object with various derived values related to the query's status which can be destructured as flows - const { data: groupData, isFetching: groupDataIsFetching } = groupsApi.useGetGroupsQuery(); + groupsApi.useGetGroupsQuery(); // Queries can be conditionally fetched based if optional parameter skip is true; // Skip this query if user is not admin // When user is an admin, ensure that the initial Group data exists and is not currently fetching - groupsApi.useGetAllGroupsChildrenQuery(undefined, { skip: (!isAdmin || !groupData || groupDataIsFetching) }); + // groupsApi.useGetAllGroupsChildrenQuery(undefined, { skip: (!isAdmin || !groupData || groupDataIsFetching) }); @@ -73,7 +70,7 @@ export default function InitializationComponent() { // dispatch(fetchPreferencesIfNeeded()); // dispatch(fetchUnitsDetailsIfNeeded()); // dispatch(fetchConversionsDetailsIfNeeded()); - }, []); + }, [dispatch, verifyTokenTrigger]); return (
diff --git a/src/client/app/components/LineChartComponent.tsx b/src/client/app/components/LineChartComponent.tsx index bc49627f0..62ef78edd 100644 --- a/src/client/app/components/LineChartComponent.tsx +++ b/src/client/app/components/LineChartComponent.tsx @@ -12,9 +12,8 @@ import { graphSlice, selectAreaUnit, selectGraphAreaNormalization, selectLineGraphRate, selectSelectedGroups, selectSelectedMeters } from '../reducers/graph'; -import { selectGroupDataByID } from '../reducers/groups'; -import { selectMeterDataByID, selectMeterState } from '../reducers/meters'; -import { selectUnitDataById } from '../reducers/units'; +import { selectGroupDataById } from '../redux/api/groupsApi'; +import { selectMeterDataById } from '../redux/api/metersApi'; import { readingsApi } from '../redux/api/readingsApi'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { ChartMultiQueryProps, LineReadingApiArgs } from '../redux/selectors/dataSelectors'; @@ -24,6 +23,7 @@ import getGraphColor from '../utils/getGraphColor'; import { lineUnitLabel } from '../utils/graphics'; import translate from '../utils/translate'; import LogoSpinner from './LogoSpinner'; +import { selectUnitDataById } from '../redux/api/unitsApi'; /** * @param props qpi query @@ -38,14 +38,14 @@ export default function LineChartComponent(props: ChartMultiQueryProps state.graph.selectedUnit); // The current selected rate const currentSelectedRate = useAppSelector(selectLineGraphRate); - const unitDataByID = useAppSelector(selectUnitDataById); + const { data: unitDataByID = {} } = useAppSelector(selectUnitDataById); const selectedAreaNormalization = useAppSelector(selectGraphAreaNormalization); const selectedAreaUnit = useAppSelector(selectAreaUnit); const selectedMeters = useAppSelector(selectSelectedMeters); const selectedGroups = useAppSelector(selectSelectedGroups); - const metersState = useAppSelector(selectMeterState); - const meterDataByID = useAppSelector(selectMeterDataByID); - const groupDataByID = useAppSelector(selectGroupDataByID); + const { data: meterDataByID = {} } = useAppSelector(selectMeterDataById); + const { data: groupDataByID = {} } = useAppSelector(selectGroupDataById); + // dataFetching Query Hooks const { data: groupData, isLoading: groupIsLoading } = readingsApi.useLineQuery(groupsArgs, { skip: groupSkipQuery }); @@ -87,7 +87,7 @@ export default function LineChartComponent(props: ChartMultiQueryProps 0 && meterDataByID[meterID].areaUnit != AreaUnitType.none)) { // Convert the meter area into the proper unit if normalizing by area or use 1 if not so won't change reading values. diff --git a/src/client/app/components/MeterAndGroupSelectComponent.tsx b/src/client/app/components/MeterAndGroupSelectComponent.tsx index 628983705..d88236ca4 100644 --- a/src/client/app/components/MeterAndGroupSelectComponent.tsx +++ b/src/client/app/components/MeterAndGroupSelectComponent.tsx @@ -23,7 +23,7 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; */ export default function MeterAndGroupSelectComponent(props: MeterAndGroupSelectProps) { const dispatch = useAppDispatch(); - const meterAndGroupSelectOptions = useAppSelector(state => selectMeterGroupSelectData(state)); + const meterAndGroupSelectOptions = useAppSelector(selectMeterGroupSelectData); const { somethingIsFetching } = getFetchingStates(); const { meterOrGroup } = props; diff --git a/src/client/app/components/RouteComponent.tsx b/src/client/app/components/RouteComponent.tsx index d1306238e..b044f0050 100644 --- a/src/client/app/components/RouteComponent.tsx +++ b/src/client/app/components/RouteComponent.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { Route, Router, Switch, Redirect } from 'react-router-dom'; import { IntlProvider } from 'react-intl'; -import localeData from '../translations/data'; +import LocaleTranslationData from '../translations/data'; import { browserHistory } from '../utils/history'; import * as _ from 'lodash'; import * as moment from 'moment'; @@ -283,7 +283,7 @@ export default class RouteComponent extends React.Component { */ public render() { const lang = this.props.selectedLanguage; - const messages = (localeData as any)[lang]; + const messages = (LocaleTranslationData as any)[lang]; return (
diff --git a/src/client/app/components/RouteComponentWIP.tsx b/src/client/app/components/RouteComponentWIP.tsx index 6a67bd922..6470c575c 100644 --- a/src/client/app/components/RouteComponentWIP.tsx +++ b/src/client/app/components/RouteComponentWIP.tsx @@ -8,12 +8,16 @@ import * as React from 'react'; import { IntlProvider } from 'react-intl'; import { Navigate, Outlet, RouterProvider, createBrowserRouter, useSearchParams } from 'react-router-dom-v5-compat'; import { TimeInterval } from '../../../common/TimeInterval'; +import CreateUserContainer from '../containers/admin/CreateUserContainer'; +import UploadCSVContainer from '../containers/csv/UploadCSVContainer'; +import MapCalibrationContainer from '../containers/maps/MapCalibrationContainer'; +import MapsDetailContainer from '../containers/maps/MapsDetailContainer'; import { selectCurrentUser } from '../reducers/currentUser'; import { graphSlice } from '../reducers/graph'; import { baseApi } from '../redux/api/baseApi'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { selectIsLoggedInAsAdmin } from '../redux/selectors/authSelectors'; -import localeData from '../translations/data'; +import LocaleTranslationData from '../translations/data'; import { UserRole } from '../types/items'; import { ChartTypes, LineGraphRate, MeterOrGroup } from '../types/redux/graph'; import { validateComparePeriod, validateSortingOrder } from '../utils/calculateCompare'; @@ -23,15 +27,11 @@ import translate from '../utils/translate'; import HomeComponent from './HomeComponent'; import LoginComponent from './LoginComponent'; import SpinnerComponent from './SpinnerComponent'; -import CreateUserContainer from '../containers/admin/CreateUserContainer'; -import UploadCSVContainer from '../containers/csv/UploadCSVContainer'; -import MapCalibrationContainer from '../containers/maps/MapCalibrationContainer'; -import MapsDetailContainer from '../containers/maps/MapsDetailContainer'; import AdminComponent from './admin/AdminComponent'; import UsersDetailComponentWIP from './admin/UsersDetailComponentWIP'; -import ConversionsDetailComponent from './conversion/ConversionsDetailComponent'; -import GroupsDetailComponent from './groups/GroupsDetailComponent'; -import MetersDetailComponent from './meters/MetersDetailComponent'; +import ConversionsDetailComponentWIP from './conversion/ConversionsDetailComponentWIP'; +import GroupsDetailComponentWIP from './groups/GroupsDetailComponentWIP'; +import MetersDetailComponentWIP from './meters/MetersDetailComponentWIP'; import UnitsDetailComponent from './unit/UnitsDetailComponent'; @@ -49,43 +49,56 @@ const useWaitForInit = () => { await Promise.all(dispatch(baseApi.util.getRunningQueriesThunk())) setInitComplete(true) // TODO Fix crashing in components on startup if data has yet to be returned, for now readyToNav works. - // This Could be avoided if these components were written to handle such cases upon startup + // This Could be avoided if these components were written to handle such cases upon startup| undefined data } waitForInit(); - }, []); + }, [dispatch]); return { isAdmin, currentUser, initComplete } } export const AdminOutlet = () => { - const { isAdmin, initComplete } = useWaitForInit(); + const { isAdmin + // , initComplete + } = useWaitForInit(); - if (!initComplete) { - // Return a spinner until all init queries return and populate cache with data - return - } + // if (!initComplete) { + // // Return a spinner until all init queries return and populate cache with data + // return + // } + // Keeping for now in case changes are desired if (isAdmin) { return } + return + // For now this functionality is disabled. + // If no longer desired can remove this and close PR // No other cases means user doesn't have the permissions. - return + // return } // Function that returns a JSX element. Either the requested route's Component, as outlet or back to root export const RoleOutlet = ({ UserRole }: { UserRole: UserRole }) => { - const { currentUser, initComplete } = useWaitForInit(); - // If state contains token it has been validated on startup or login. - if (!initComplete) { - return - } + const { currentUser + // , initComplete + } = useWaitForInit(); + // // If state contains token it has been validated on startup or login. + // if (!initComplete) { + // return + // } + + // Keeping for now in case changes are desired if (currentUser.profile?.role === UserRole) { return } - return + // If no longer desired can remove this and close PR + // For now this functionality is disabled. + // return + return } export const NotFound = () => { @@ -198,7 +211,7 @@ export const GraphLink = () => { /// Router const router = createBrowserRouter([ { path: '/', element: }, - { path: 'login', element: }, + { path: '/login', element: }, { path: '/', element: , @@ -208,9 +221,9 @@ const router = createBrowserRouter([ { path: 'maps', element: }, { path: 'users/new', element: }, { path: 'units', element: }, - { path: 'conversions', element: }, - { path: 'groups', element: }, - { path: 'meters', element: }, + { path: 'conversions', element: }, + { path: 'groups', element: }, + { path: 'meters', element: }, { path: 'users', element: } ] }, @@ -231,7 +244,7 @@ const router = createBrowserRouter([ */ export default function RouteComponentWIP() { const lang = useAppSelector(state => state.options.selectedLanguage) - const messages = (localeData as any)[lang]; + const messages = (LocaleTranslationData as any)[lang]; return ( diff --git a/src/client/app/components/TimeZoneSelect.tsx b/src/client/app/components/TimeZoneSelect.tsx index 15696e916..ef124836b 100644 --- a/src/client/app/components/TimeZoneSelect.tsx +++ b/src/client/app/components/TimeZoneSelect.tsx @@ -24,7 +24,7 @@ const TimeZoneSelect: React.FC = ({ current, handleClick }) if (!optionsLoaded) { axios.get('/api/timezones').then(res => { const timeZones = res.data; - const resetTimeZone = [{value: null, label: translate('timezone.no')}]; + const resetTimeZone = [{ value: null, label: translate('timezone.no') }]; const allTimeZones = (timeZones.map((timezone: TimeZones) => { return { value: timezone.name, label: `${timezone.name} (${timezone.abbrev}) ${timezone.offset}` }; })); @@ -32,14 +32,18 @@ const TimeZoneSelect: React.FC = ({ current, handleClick }) setOptionsLoaded(true); }); } - }, []); + }, [optionsLoaded]); const handleChange = (selectedOption: TimeZoneOption) => { handleClick(selectedOption.value); }; return (options !== null ? - value === current)} + options={options} + onChange={handleChange} + /> : Please Reload); }; diff --git a/src/client/app/components/UnitSelectComponent.tsx b/src/client/app/components/UnitSelectComponent.tsx index 824eb1a4b..9d774152a 100644 --- a/src/client/app/components/UnitSelectComponent.tsx +++ b/src/client/app/components/UnitSelectComponent.tsx @@ -14,6 +14,7 @@ import { graphSlice } from '../reducers/graph'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; import { getFetchingStates } from '../redux/componentHooks'; +import { selectUnitDataById } from '../redux/api/unitsApi'; /** @@ -23,7 +24,8 @@ export default function UnitSelectComponent() { const dispatch = useAppDispatch(); const unitSelectOptions = useAppSelector(state => selectUnitSelectData(state)); const selectedUnitID = useAppSelector(state => state.graph.selectedUnit); - const unitsByID = useAppSelector(state => state.units.units); + const { data: unitsByID = {} } = useAppSelector(selectUnitDataById); + const { endpointsFetchingData } = getFetchingStates(); let selectedUnitOption: SelectOption | null = null; diff --git a/src/client/app/components/UnsavedWarningComponent.tsx b/src/client/app/components/UnsavedWarningComponent.tsx index f0069aa62..92ff1d73b 100644 --- a/src/client/app/components/UnsavedWarningComponent.tsx +++ b/src/client/app/components/UnsavedWarningComponent.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -// TODO migrate ReactRouterv6 & hooks +// TODO migrate ReactRouterV6 & hooks import { Prompt, withRouter, RouteComponentProps } from 'react-router-dom'; import { deleteToken } from '../utils/token'; import {store} from '../store'; diff --git a/src/client/app/components/UnsavedWarningComponentWIP.tsx b/src/client/app/components/UnsavedWarningComponentWIP.tsx index 903d25c6e..11fc3f9fa 100644 --- a/src/client/app/components/UnsavedWarningComponentWIP.tsx +++ b/src/client/app/components/UnsavedWarningComponentWIP.tsx @@ -2,23 +2,22 @@ * 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 { MutationTrigger } from '@reduxjs/toolkit/dist/query/react/buildHooks'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { - unstable_BlockerFunction as BlockerFunction, - unstable_useBlocker as useBlocker -} from 'react-router-dom-v5-compat'; +import { unstable_useBlocker as useBlocker } from 'react-router-dom-v5-compat'; // TODO migrate ReactRouter v6 & hooks import { Button, Modal, ModalBody, ModalFooter } from 'reactstrap'; -import { userApi } from '../redux/api/userApi' -import { MutationTrigger } from '@reduxjs/toolkit/dist/query/react/buildHooks'; +import { LocaleDataKey } from '../translations/data'; +import { showErrorNotification, showSuccessNotification } from '../utils/notifications'; +import translate from '../utils/translate'; + export interface UnsavedWarningProps { - hasUnsavedChanges: boolean | BlockerFunction; changes: any; - submitChanges: MutationTrigger< - typeof userApi.endpoints.editUsers.Types.MutationDefinition | - typeof userApi.endpoints.createUser.Types.MutationDefinition - >; + hasUnsavedChanges: boolean; + successMessage: LocaleDataKey; + failureMessage: LocaleDataKey; + submitChanges: MutationTrigger; } /** @@ -27,14 +26,39 @@ export interface UnsavedWarningProps { */ export function UnsavedWarningComponentWIP(props: UnsavedWarningProps) { const { hasUnsavedChanges, submitChanges, changes } = props - const blocker = useBlocker(hasUnsavedChanges); + const blocker = useBlocker(hasUnsavedChanges) + const handleSubmit = async () => { + submitChanges(changes) + .unwrap() + .then(() => { + showSuccessNotification(translate('updated.preferences')) + if (blocker.state === 'blocked') { + blocker.proceed() + } + }) + .catch(() => { + showErrorNotification(translate('failed.to.submit.changes')) + if (blocker.state === 'blocked') { + blocker.proceed() + } + }) + } + React.useEffect(() => { + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + if (blocker.state === 'blocked') { + e.preventDefault(); + } + } + + window.addEventListener('beforeunload', handleBeforeUnload) + return () => window.removeEventListener('beforeunload', handleBeforeUnload) + }, [hasUnsavedChanges, blocker]) - console.log(props) + // console.log(props) return ( - {/* */} - diff --git a/src/client/app/components/admin/AdminComponent.tsx b/src/client/app/components/admin/AdminComponent.tsx index be1f864c5..e9bd83a60 100644 --- a/src/client/app/components/admin/AdminComponent.tsx +++ b/src/client/app/components/admin/AdminComponent.tsx @@ -3,14 +3,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import FooterContainer from '../../containers/FooterContainer'; -import PreferencesContainer from '../../containers/admin/PreferencesContainer'; -import ManageUsersLinkButtonComponent from './users/ManageUsersLinkButtonComponent'; -import TooltipMarkerComponent from '../TooltipMarkerComponent'; import { FormattedMessage } from 'react-intl'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; -import UnsavedWarningContainer from '../../containers/UnsavedWarningContainer'; import HeaderComponent from '../../components/HeaderComponent'; +import FooterContainer from '../../containers/FooterContainer'; +import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +// import PreferencesContainer from '../../containers/admin/PreferencesContainer'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; +import ManageUsersLinkButtonComponent from './users/ManageUsersLinkButtonComponent'; +import PreferencesComponentWIP from './PreferencesComponentWIP' /** * React component that defines the admin page @@ -27,7 +27,7 @@ export default function AdminComponent() { margin: 0, paddingBottom: '5px' } - const titleStyle: React.CSSProperties ={ + const titleStyle: React.CSSProperties = { textAlign: 'center' }; const tooltipStyle = { @@ -36,14 +36,13 @@ export default function AdminComponent() { }; return (
-

- +

@@ -54,7 +53,8 @@ export default function AdminComponent() {
- + {/* */} +
diff --git a/src/client/app/components/admin/CreateUserComponentWIP.tsx b/src/client/app/components/admin/CreateUserComponentWIP.tsx index 321ad1ebb..1dff21ee5 100644 --- a/src/client/app/components/admin/CreateUserComponentWIP.tsx +++ b/src/client/app/components/admin/CreateUserComponentWIP.tsx @@ -32,7 +32,8 @@ export default function CreateUserComponentWIP() { .then(() => { showSuccessNotification(translate('users.successfully.create.user')) - }).catch(() => { + }) + .catch(() => { showErrorNotification(translate('users.failed.to.create.user')); }) diff --git a/src/client/app/components/admin/PreferencesComponentWIP.tsx b/src/client/app/components/admin/PreferencesComponentWIP.tsx new file mode 100644 index 000000000..17e40bc28 --- /dev/null +++ b/src/client/app/components/admin/PreferencesComponentWIP.tsx @@ -0,0 +1,377 @@ +/* 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 _ from 'lodash'; +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Button, Input } from 'reactstrap'; +import { UnsavedWarningComponentWIP } from '../../components/UnsavedWarningComponentWIP'; +import { preferencesApi } from '../../redux/api/preferencesApi'; +import { PreferenceRequestItem, TrueFalseType } from '../../types/items'; +import { ChartTypes } from '../../types/redux/graph'; +import { LanguageTypes } from '../../types/redux/i18n'; +import { AreaUnitType } from '../../utils/getAreaUnitConversion'; +import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; +import translate from '../../utils/translate'; +import TimeZoneSelect from '../TimeZoneSelect'; + + +// TODO: Add warning for invalid data +/** + * @returns Preferences Component for Administrative use + */ +export default function PreferencesComponentWIP() { + const { data: adminPreferences = {} as PreferenceRequestItem } = preferencesApi.useGetPreferencesQuery(); + const [localAdminPref, setLocalAdminPref] = React.useState(_.cloneDeep(adminPreferences)) + const [submitPreferences] = preferencesApi.useSubmitPreferencesMutation(); + const [hasChanges, setHasChanges] = React.useState(false); + + // mutation will invalidate preferences tag and will be re-fetched. + // On query response, reset local changes to response + React.useEffect(() => { setLocalAdminPref(_.cloneDeep(adminPreferences)) }, [adminPreferences]) + // Compare the API response against the localState to determine changes + React.useEffect(() => { setHasChanges(!_.isEqual(adminPreferences, localAdminPref)) }, [localAdminPref, adminPreferences]) + + const makeLocalChanges = (key: keyof PreferenceRequestItem, value: PreferenceRequestItem[keyof PreferenceRequestItem]) => { + setLocalAdminPref({ ...localAdminPref, [key]: value }) + } + + return ( +
+ +
+

+ {`${translate('default.site.title')}:`} +

+ makeLocalChanges('displayTitle', e.target.value)} + maxLength={50} + /> +
+
+

+ : +

+
+ +
+
+ +
+
+ +
+
+ +
+
+

+ : +

+
+ +
+
+ +
+
+

+ {translate('default.area.unit')} + +

+
+ +
+
+ +
+
+
+

+ {translate('default.language')} +

+
+ +
+
+ +
+
+ +
+
+
+

+ {`${translate('default.time.zone')}:`} +

+ makeLocalChanges('defaultTimezone', e)} /> +
+
+

+ {`${translate('default.warning.file.size')}:`} + +

+ makeLocalChanges('defaultWarningFileSize', e.target.value)} + maxLength={50} + /> +
+
+

+ {`${translate('default.file.size.limit')}:`} +

+ makeLocalChanges('defaultFileSizeLimit', e.target.value)} + maxLength={50} + /> +
+ {/* Reuse same style as title. */} +
+

+ {`${translate('default.meter.reading.frequency')}:`} +

+ makeLocalChanges('defaultMeterReadingFrequency', e.target.value)} + /> +
+
+

+ {`${translate('default.meter.minimum.value')}:`} +

+ makeLocalChanges('defaultMeterMinimumValue', e.target.value)} + maxLength={50} + /> +
+
+

+ {`${translate('default.meter.maximum.value')}:`} +

+ makeLocalChanges('defaultMeterMaximumValue', e.target.value)} + maxLength={50} + /> +
+
+

+ {`${translate('default.meter.minimum.date')}:`} +

+ makeLocalChanges('defaultMeterMinimumDate', e.target.value)} + placeholder='YYYY-MM-DD HH:MM:SS' + /> +
+
+

+ {`${translate('default.meter.maximum.date')}:`} +

+ makeLocalChanges('defaultMeterMaximumDate', e.target.value)} + placeholder='YYYY-MM-DD HH:MM:SS' + /> +
+
+

+ {`${translate('default.meter.reading.gap')}:`} +

+ makeLocalChanges('defaultMeterReadingGap', e.target.value)} + maxLength={50} + /> +
+
+

+ {`${translate('default.meter.maximum.errors')}:`} +

+ makeLocalChanges('defaultMeterMaximumErrors', e.target.value)} + maxLength={50} + /> +
+
+

+ {`${translate('default.meter.disable.checks')}:`} +

+ makeLocalChanges('defaultMeterDisableChecks', e.target.value)}> + {Object.keys(TrueFalseType).map(key => { + return () + })} + +
+ +
+ ); +} + + + + + +const labelStyle: React.CSSProperties = { + fontWeight: 'bold', + margin: 0 +}; +const bottomPaddingStyle: React.CSSProperties = { + paddingBottom: '15px' +}; + +const titleStyle: React.CSSProperties = { + fontWeight: 'bold', + margin: 0, + paddingBottom: '5px' +}; \ No newline at end of file diff --git a/src/client/app/components/admin/UsersDetailComponentWIP.tsx b/src/client/app/components/admin/UsersDetailComponentWIP.tsx index 4e1de4367..926411cfb 100644 --- a/src/client/app/components/admin/UsersDetailComponentWIP.tsx +++ b/src/client/app/components/admin/UsersDetailComponentWIP.tsx @@ -28,38 +28,29 @@ export default function UserDetailComponentWIP() { const [localUsersChanges, setLocalUsersChanges] = React.useState([]); const [hasChanges, setHasChanges] = React.useState(false); - React.useEffect(() => { setLocalUsersChanges(users) }, [users]) - React.useEffect( - () => { - if (!_.isEqual(users, localUsersChanges)) { - setHasChanges(true) - } else { - setHasChanges(false) - } - }, - [localUsersChanges] - ) - const editUser = (e: React.ChangeEvent, targetUser: User) => { - // copy user, and update role - const updatedUser: User = { ...targetUser, role: e.target.value as UserRole } - // make new list from existing local user state - const updatedList = localUsersChanges.map(user => (user.email === targetUser.email) ? updatedUser : user) - setLocalUsersChanges(updatedList) - // editUser(user.email, target.value as UserRole); - } + React.useEffect(() => { setLocalUsersChanges(users) }, [users]) + React.useEffect(() => { !_.isEqual(users, localUsersChanges) ? setHasChanges(true) : setHasChanges(false) }, [localUsersChanges, users]) const submitChanges = async () => { submitUserEdits(localUsersChanges) .unwrap() .then(() => { showSuccessNotification(translate('users.successfully.edit.users')); }) - .catch(e => { - console.log(e) + .catch(() => { showErrorNotification(translate('users.failed.to.edit.users')) }) } + const editUser = (e: React.ChangeEvent, targetUser: User) => { + // copy user, and update role + const updatedUser: User = { ...targetUser, role: e.target.value as UserRole } + // make new list from existing local user state + const updatedList = localUsersChanges.map(user => (user.email === targetUser.email) ? updatedUser : user) + setLocalUsersChanges(updatedList) + // editUser(user.email, target.value as UserRole); + } + const deleteUser = (email: string) => { setLocalUsersChanges(localUsersChanges.filter(user => user.email !== email)) } @@ -72,6 +63,8 @@ export default function UserDetailComponentWIP() { hasUnsavedChanges={hasChanges} changes={localUsersChanges} submitChanges={submitUserEdits} + successMessage='users.successfully.edit.users' + failureMessage='failed.to.submit.changes' />
diff --git a/src/client/app/components/conversion/ConversionsDetailComponent.tsx b/src/client/app/components/conversion/ConversionsDetailComponent.tsx index 1987ed922..2e4fddf8e 100644 --- a/src/client/app/components/conversion/ConversionsDetailComponent.tsx +++ b/src/client/app/components/conversion/ConversionsDetailComponent.tsx @@ -30,7 +30,7 @@ export default function ConversionsDetailComponent() { useEffect(() => { // Makes async call to conversions API for conversions details if one has not already been made somewhere else, stores conversion by ids in state dispatch(fetchConversionsDetailsIfNeeded()); - }, []); + }, [dispatch]); // Conversions state const conversionsState = useSelector((state: State) => state.conversions.conversions); diff --git a/src/client/app/components/conversion/ConversionsDetailComponentWIP.tsx b/src/client/app/components/conversion/ConversionsDetailComponentWIP.tsx new file mode 100644 index 000000000..179d06377 --- /dev/null +++ b/src/client/app/components/conversion/ConversionsDetailComponentWIP.tsx @@ -0,0 +1,90 @@ +/* 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 { FormattedMessage } from 'react-intl'; +import HeaderComponent from '../../components/HeaderComponent'; +import SpinnerComponent from '../../components/SpinnerComponent'; +import FooterContainer from '../../containers/FooterContainer'; +import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import { conversionsApi } from '../../redux/api/conversionsApi'; +import { unitsApi } from '../../redux/api/unitsApi'; +import { ConversionData } from '../../types/redux/conversions'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; +import ConversionViewComponent from './ConversionViewComponent'; +import CreateConversionModalComponentWIP from './CreateConversionModalComponentWIP'; + +/** + * Defines the conversions page card view + * @returns Conversion page element + */ +export default function ConversionsDetailComponent() { + // The route stops you from getting to this page if not an admin. + + // Conversions state + const { data: conversionsState = [], isFetching: conversionsFetching } = conversionsApi.useGetConversionsDetailsQuery(); + // Units DataById + const { data: unitDataById = {}, isFetching: unitsFetching } = unitsApi.useGetUnitsDetailsQuery() + // const x = useAppSelector(state => conversionsApi.endpoints.refresh.select()(state)) + + // unnecessary? Currently this occurs as a side effect of the mutation which will invalidate meters/group + // unused for now, until decided + // const isUpdatingCikAndDBViews = useAppSelector(state => state.admin.isUpdatingCikAndDBViews); + + // Check if the units state is fully loaded + + const titleStyle: React.CSSProperties = { + textAlign: 'center' + }; + + const tooltipStyle = { + display: 'inline-block', + fontSize: '50%', + // For now, only an admin can see the conversion page. + tooltipConversionView: 'help.admin.conversionview' + }; + + return ( +
+ {(conversionsFetching || unitsFetching) ? ( +
+ + +
+ ) : ( +
+ + + +
+

+ +
+ +
+

+ {unitDataById && +
+ +
} +
+ {/* Attempt to create a ConversionViewComponent for each ConversionData in Conversions State after sorting by + the combination of the identifier of the source and destination of the conversion. */} + {unitDataById && Object.values(conversionsState) + .sort((conversionA: ConversionData, conversionB: ConversionData) => + ((unitDataById[conversionA.sourceId].identifier + unitDataById[conversionA.destinationId].identifier).toLowerCase() > + (unitDataById[conversionB.sourceId].identifier + unitDataById[conversionB.destinationId].identifier).toLowerCase()) ? 1 : + (((unitDataById[conversionB.sourceId].identifier + unitDataById[conversionB.destinationId].identifier).toLowerCase() > + (unitDataById[conversionA.sourceId].identifier + unitDataById[conversionA.destinationId].identifier).toLowerCase()) ? -1 : 0)) + .map(conversionData => (' + (conversionData as ConversionData).destinationId)} + units={unitDataById} />))} +
+
+ +
+ )} +
+ ); +} diff --git a/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx b/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx new file mode 100644 index 000000000..17e4b6052 --- /dev/null +++ b/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx @@ -0,0 +1,335 @@ +/* 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 _ from 'lodash'; +import * as React from 'react'; +import { useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; +import { selectIsValidConversion } from '../../redux/selectors/adminSelectors'; +import { UnitData } from '../../types/redux/units'; +import { addConversion } from '../../actions/conversions'; +import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import { selectUnitDataById } from '../../redux/api/unitsApi'; +import { useAppDispatch, useAppSelector } from '../../redux/hooks'; +import '../../styles/modal.css'; +import { tooltipBaseStyle } from '../../styles/modalStyle'; +import { TrueFalseType } from '../../types/items'; +import translate from '../../utils/translate'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; + +/** + * Defines the create conversion modal form + * @returns Conversion create element + */ +export default function CreateConversionModalComponent() { + + const dispatch = useAppDispatch(); + const { data: unitDataById = {} } = useAppSelector(selectUnitDataById) + // Want units in sorted order by identifier regardless of case. + const unitsSorted = _.sortBy(Object.values(unitDataById), unit => unit.identifier.toLowerCase(), 'asc'); + + const defaultValues = { + // Invalid source/destination ids arbitrarily set to -999. + // Meter Units are not allowed to be a destination. + sourceId: -999, + sourceOptions: unitsSorted as UnitData[], + destinationId: -999, + destinationOptions: unitsSorted.filter(unit => unit.typeOfUnit !== 'meter') as UnitData[], + bidirectional: true, + slope: 0, + intercept: 0, + note: '' + } + + /* State */ + // Modal show + const [showModal, setShowModal] = useState(false); + const handleClose = () => { + setShowModal(false); + resetState(); + }; + const handleShow = () => setShowModal(true); + + // Handlers for each type of input change + const [conversionState, setConversionState] = useState(defaultValues); + + const handleStringChange = (e: React.ChangeEvent) => { + setConversionState({ ...conversionState, [e.target.name]: e.target.value }); + } + + const handleBooleanChange = (e: React.ChangeEvent) => { + setConversionState({ ...conversionState, [e.target.name]: JSON.parse(e.target.value) }); + } + + const handleNumberChange = (e: React.ChangeEvent) => { + // once a source or destination is selected, it will be removed from the other options. + if (e.target.name === 'sourceId') { + setConversionState(state => ({ + ...state, + sourceId: Number(e.target.value), + destinationOptions: defaultValues.destinationOptions.filter(destination => destination.id !== Number(e.target.value)) + })); + } else if (e.target.name === 'destinationId') { + setConversionState(state => ({ + ...state, + destinationId: Number(e.target.value), + sourceOptions: defaultValues.sourceOptions.filter(source => source.id !== Number(e.target.value)) + })); + } else { + setConversionState(state => ({ ...state, [e.target.name]: Number(e.target.value) })); + } + } + + // If the currently selected conversion is valid + const validConversion = useAppSelector( + state => selectIsValidConversion( + state, + conversionState.sourceId, + conversionState.destinationId, + conversionState.bidirectional) + ) + /* End State */ + + + // //Update the valid conversion state any time the source id, destination id, or bidirectional status changes + // useEffect(() => { + // /** + // * Checks if conversion is valid + // * @param sourceId New conversion sourceId + // * @param destinationId New conversion destinationId + // * @param bidirectional New conversion bidirectional status + // * @returns boolean representing if new conversion is valid or not + // */ + // const isValidConversion = (sourceId: number, destinationId: number, bidirectional: boolean) => { + // /* Create Conversion Validation: + // Source equals destination: invalid conversion + // Conversion exists: invalid conversion + // Conversion does not exist: + // Inverse exists: + // Conversion is bidirectional: invalid conversion + // Destination cannot be a meter + // Cannot mix unit represent + // TODO Some of these can go away when we make the menus dynamic. + // */ + + // // The destination cannot be a meter unit. + // if (destinationId !== -999 && unitDataById[destinationId].typeOfUnit === UnitType.meter) { + // notifyUser(translate('conversion.create.destination.meter')); + // return false; + // } + + // // Source or destination not set + // if (sourceId === -999 || destinationId === -999) { + // return false + // } + + // // Conversion already exists + // if ((conversionState.findIndex(conversionData => (( + // conversionData.sourceId === state.sourceId) && + // conversionData.destinationId === state.destinationId))) !== -1) { + // notifyUser(translate('conversion.create.exists')); + // return false; + // } + + // // You cannot have a conversion between units that differ in unit_represent. + // // This means you cannot mix quantity, flow & raw. + // if (unitDataById[sourceId].unitRepresent !== unitDataById[destinationId].unitRepresent) { + // notifyUser(translate('conversion.create.mixed.represent')); + // return false; + // } + + + // let isValid = true; + // // Loop over conversions and check for existence of inverse of conversion passed in + // // If there exists an inverse that is bidirectional, then there is no point in making a conversion since it is essentially a duplicate. + // // If there is a non bidirectional inverse, then it is a valid conversion + // Object.values(conversionState).forEach(conversion => { + // // Inverse exists + // if ((conversion.sourceId === destinationId) && (conversion.destinationId === sourceId)) { + // // Inverse is bidirectional + // if (conversion.bidirectional) { + // isValid = false; + // } + // // Inverse is not bidirectional + // else { + // // Do not allow for a bidirectional conversion with an inverse that is not bidirectional + // if (bidirectional) { + // // The new conversion is bidirectional + // isValid = false; + // } + // } + // } + // }); + // if (!isValid) { + // notifyUser(translate('conversion.create.exists.inverse')); + // } + // return isValid; + // } + + + // setValidConversion(isValidConversion(state.sourceId, state.destinationId, state.bidirectional)); + // }, [state.sourceId, state.destinationId, state.bidirectional, unitDataById, conversionState]); + + // Reset the state to default values + const resetState = () => { + setConversionState(defaultValues); + } + + // Unlike edit, we decided to discard and inputs when you choose to leave the page. The reasoning is + // that create starts from an empty template. + + // Submit + const handleSubmit = () => { + // Close modal first to avoid repeat clicks + setShowModal(false); + // Add the new conversion and update the store + dispatch(addConversion(conversionState)); + resetState(); + }; + + const tooltipStyle = { + ...tooltipBaseStyle, + tooltipCreateConversionView: 'help.admin.conversioncreate' + }; + + return ( + <> + {/* Show modal button */} + + + + + + +
+ +
+
+ {/* when any of the conversion are changed call one of the functions. */} + + + + + {/* Source unit input*/} + + + handleNumberChange(e)} + invalid={conversionState.sourceId === -999}> + {} + {Object.values(conversionState.sourceOptions).map(unitData => { + return () + })} + + + + + + + + {/* Destination unit input*/} + + + handleNumberChange(e)} + invalid={conversionState.destinationId === -999}> + {} + {Object.values(conversionState.destinationOptions).map(unitData => { + return () + })} + + + + + + + + {/* Bidirectional Y/N input*/} + + + handleBooleanChange(e)}> + {Object.keys(TrueFalseType).map(key => { + return () + })} + + + + + {/* Slope input*/} + + + handleNumberChange(e)} /> + + + + {/* Intercept input*/} + + + handleNumberChange(e)} /> + + + + {/* Note input*/} + + + handleStringChange(e)} + value={conversionState.note} /> + + + + + {/* Hides the modal */} + + {/* On click calls the function handleSaveChanges in this component */} + + +
+ + ); +} diff --git a/src/client/app/components/csv/MetersCSVUploadComponent.tsx b/src/client/app/components/csv/MetersCSVUploadComponent.tsx index de73a49ca..851ff7937 100644 --- a/src/client/app/components/csv/MetersCSVUploadComponent.tsx +++ b/src/client/app/components/csv/MetersCSVUploadComponent.tsx @@ -3,13 +3,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { Button, Input, Form, FormGroup, Label } from 'reactstrap'; -import { MetersCSVUploadProps } from '../../types/csvUploadForm'; -import FormFileUploaderComponent from '../FormFileUploaderComponent'; import { FormattedMessage } from 'react-intl'; -import { MODE } from '../../containers/csv/UploadCSVContainer'; +import { Button, Form, FormGroup, Input, Label } from 'reactstrap'; import { fetchMetersDetails } from '../../actions/meters'; -import {store} from '../../store'; +import { MODE } from '../../containers/csv/UploadCSVContainer'; +import { MetersCSVUploadProps } from '../../types/csvUploadForm'; +import FormFileUploaderComponent from '../FormFileUploaderComponent'; export default class MetersCSVUploadComponent extends React.Component { private fileInput: React.RefObject; @@ -37,7 +36,8 @@ export default class MetersCSVUploadComponent extends React.Component; @@ -42,7 +42,6 @@ interface CreateGroupModalComponentProps { * @returns Group create element */ export default function CreateGroupModalComponent(props: CreateGroupModalComponentProps) { - const dispatch: Dispatch = useDispatch(); // Meter state const metersState = useSelector((state: State) => state.meters.byMeterID); @@ -219,7 +218,8 @@ export default function CreateGroupModalComponent(props: CreateGroupModalCompone // The input passed validation. // GPS may have been updated so create updated state to submit. const submitState = { ...state, gps: gps }; - dispatch(submitNewGroup(submitState)); + console.log('removeMe', submitState) + // dispatch(submitNewGroup(submitState)); resetState(); } else { // Tell user that not going to update due to input issues. @@ -250,7 +250,7 @@ export default function CreateGroupModalComponent(props: CreateGroupModalCompone // pik is needed since the compatible units is not correct until pik is available. // metersState normally does not change but can so include. // groupState can change if another group is created/edited and this can change ones displayed in menus. - }, [ConversionArray.pikAvailable(), metersState, groupsState, state.defaultGraphicUnit, state.deepMeters]); + }, [metersState, groupsState, state.defaultGraphicUnit, state.deepMeters]); // Update compatible default graphic units set. useEffect(() => { diff --git a/src/client/app/components/groups/CreateGroupModalComponentWIP.tsx b/src/client/app/components/groups/CreateGroupModalComponentWIP.tsx new file mode 100644 index 000000000..9c77ee573 --- /dev/null +++ b/src/client/app/components/groups/CreateGroupModalComponentWIP.tsx @@ -0,0 +1,564 @@ +/* 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 _ from 'lodash'; +import * as React from 'react'; +import { useEffect, useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { + Button, Col, Container, FormFeedback, FormGroup, Input, InputGroup, + Label, Modal, ModalBody, ModalFooter, ModalHeader, Row +} from 'reactstrap'; +import { GroupData } from 'types/redux/groups'; +import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import { groupsApi, selectGroupDataById } from '../../redux/api/groupsApi'; +import { selectMeterDataById } from '../../redux/api/metersApi'; +import { selectUnitDataById } from '../../redux/api/unitsApi'; +import { useAppSelector } from '../../redux/hooks'; +import { selectPossibleGraphicUnits } from '../../redux/selectors/adminSelectors'; +import '../../styles/modal.css'; +import { tooltipBaseStyle } from '../../styles/modalStyle'; +import { SelectOption, TrueFalseType } from '../../types/items'; +import { UnitData } from '../../types/redux/units'; +import { GPSPoint, isValidGPSInput } from '../../utils/calibration'; +import { + getGroupMenuOptionsForGroup, + getMeterMenuOptionsForGroup, + metersInChangedGroup, + unitsCompatibleWithMeters +} from '../../utils/determineCompatibleUnits'; +import { AreaUnitType, getAreaUnitConversion } from '../../utils/getAreaUnitConversion'; +import { getGPSString, notifyUser } from '../../utils/input'; +import translate from '../../utils/translate'; +import ListDisplayComponent from '../ListDisplayComponent'; +import MultiSelectComponent from '../MultiSelectComponent'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; + +/** + * Defines the create group modal form + * @returns Group create element + */ +export default function CreateGroupModalComponentWIP() { + const [createGroup] = groupsApi.useCreateGroupMutation() + + // Meters state + const { data: metersDataById = {} } = useAppSelector(selectMeterDataById); + // Groups state + const { data: groupsDataById = {} } = useAppSelector(selectGroupDataById); + // Units state + const { data: unitsDataById = {} } = useAppSelector(selectUnitDataById); + + // Check for admin status + const possibleGraphicUnits = useAppSelector(selectPossibleGraphicUnits) + + // Since creating group the initial values are effectively nothing or the desired defaults. + const defaultValues: GroupData = { + // ID not needed, assigned by DB, add here for TS + id: -1, + name: '', + childMeters: [] as number[], + childGroups: [] as number[], + deepMeters: [] as number[], + gps: null, + displayable: false, + note: '', + area: 0, + // default is no unit or -99. + defaultGraphicUnit: -99, + areaUnit: AreaUnitType.none + } + + // The information on the children of this group for state. Except for selected, the + // values are set by the useEffect functions. + const groupChildrenDefaults = { + // The meter selections in format for selection dropdown and initially empty. + meterSelectOptions: [] as SelectOption[], + // The group selections in format for selection dropdown and initially empty. + groupSelectOptions: [] as SelectOption[] + } + + // Information on the default graphic unit values. + const graphicUnitsStateDefaults = { + possibleGraphicUnits: possibleGraphicUnits, + compatibleGraphicUnits: possibleGraphicUnits, + incompatibleGraphicUnits: new Set() + } + + /* State */ + // State for the created group. + const [state, setState] = useState(defaultValues); + + // Handlers for each type of input change + + const handleStringChange = (e: React.ChangeEvent) => { + setState({ ...state, [e.target.name]: e.target.value }); + } + + const handleBooleanChange = (e: React.ChangeEvent) => { + setState({ ...state, [e.target.name]: JSON.parse(e.target.value) }); + } + + const handleNumberChange = (e: React.ChangeEvent) => { + setState({ ...state, [e.target.name]: Number(e.target.value) }); + } + + // Unlike EditGroupsModalComponent, we don't pass show and close via props. + // Modal show + const [showModal, setShowModal] = useState(false); + + // Dropdowns state + const [groupChildrenState, setGroupChildrenState] = useState(groupChildrenDefaults) + const [graphicUnitsState, setGraphicUnitsState] = useState(graphicUnitsStateDefaults); + + /* Create Group Validation: + Name cannot be blank + Area must be positive or zero + If area is nonzero, area unit must be set + Group must have at least one child (i.e has deep child meters) + */ + const [validGroup, setValidGroup] = useState(false); + useEffect(() => { + setValidGroup( + state.name !== '' && + (state.area === 0 || (state.area > 0 && state.areaUnit !== AreaUnitType.none)) && + (state.deepMeters.length > 0) + ); + }, [state.area, state.areaUnit, state.name, state.deepMeters]); + /* End State */ + + // Sums the area of the group's deep meters. It will tell the admin if any meters are omitted from the calculation, + // or if any other errors are encountered. + const handleAutoCalculateArea = () => { + if (state.deepMeters.length > 0) { + if (state.areaUnit != AreaUnitType.none) { + let areaSum = 0; + let notifyMsg = ''; + state.deepMeters.forEach(meterID => { + const meter = metersDataById[meterID]; + if (meter.area > 0) { + if (meter.areaUnit != AreaUnitType.none) { + areaSum += meter.area * getAreaUnitConversion(meter.areaUnit, state.areaUnit); + } else { + // This shouldn't happen because of the other checks in place when editing/creating a meter. + // However, there could still be edge cases (i.e meters from before area units were added) that could violate this. + notifyMsg += '\n"' + meter.identifier + '"' + translate('group.area.calculate.error.unit'); + } + } else { + notifyMsg += '\n"' + meter.identifier + '"' + translate('group.area.calculate.error.zero'); + } + }); + let msg = translate('group.area.calculate.header') + areaSum + ' ' + translate(`AreaUnitType.${state.areaUnit}`); + if (notifyMsg != '') { + msg += '\n' + translate('group.area.calculate.error.header') + notifyMsg; + } + if (window.confirm(msg)) { + // the + here converts back into a number + setState({ ...state, ['area']: +areaSum.toPrecision(6) }); + } + } else { + notifyUser(translate('group.area.calculate.error.group.unit')); + } + } else { + notifyUser(translate('group.area.calculate.error.no.meters')); + } + } + + const handleClose = () => { + setShowModal(false); + resetState(); + }; + const handleShow = () => { setShowModal(true); } + + // Reset the state to default value so each time starts from scratch. + const resetState = () => { + setState(defaultValues); + setGroupChildrenState(groupChildrenDefaults); + setGraphicUnitsState(graphicUnitsStateDefaults); + } + + // Unlike edit, we decided to discard inputs when you choose to leave the page. The reasoning is + // that create starts from an empty template. + + // Save changes + const handleSubmit = () => { + // Close modal first to avoid repeat clicks + setShowModal(false); + + // true if inputted values are okay. Then can submit. + let inputOk = true; + + // Check GPS entered. + const gpsInput = state.gps; + let gps: GPSPoint | null = null; + const latitudeIndex = 0; + const longitudeIndex = 1; + // If the user input a value then gpsInput should be a string. + // null came from the DB and it is okay to just leave it - Not a string. + if (typeof gpsInput === 'string') { + if (isValidGPSInput(gpsInput)) { + // Clearly gpsInput is a string but TS complains about the split so cast. + const gpsValues = (gpsInput as string).split(',').map((value: string) => parseFloat(value)); + // It is valid and needs to be in this format for routing. + gps = { + longitude: gpsValues[longitudeIndex], + latitude: gpsValues[latitudeIndex] + }; + // gpsInput must be of type string but TS does not think so so cast. + } else if ((gpsInput as string).length !== 0) { + // GPS not okay. Only true if some input. + // TODO isValidGPSInput currently pops up an alert so not doing it here, may change + // so leaving code commented out. + // notifyUser(translate('input.gps.range') + state.gps + '.'); + inputOk = false; + } + } + + if (inputOk) { + // The input passed validation. + // GPS may have been updated so create updated state to submit. + const submitState = { ...state, gps: gps }; + console.log('removeMe', submitState) + + createGroup(submitState) + resetState(); + } else { + // Tell user that not going to update due to input issues. + notifyUser(translate('group.input.error')); + } + }; + + // Determine allowed child meters/groups for menu. + useEffect(() => { + // Can only vary if admin and only used then. + // This is the current deep meters of this group including any changes. + // The id is not really needed so set to -1 since same function for edit. + const groupDeepMeter = metersInChangedGroup(state); + // Get meters that okay for this group in a format the component can display. + const possibleMeters = getMeterMenuOptionsForGroup(state.defaultGraphicUnit, groupDeepMeter); + // Get groups okay for this group. Similar to meters. + // Since creating a group, the group cannot yet exist in the Redux state. Thus, the id is not used + // in this case so set to -1 so it never matches in this function. + const possibleGroups = getGroupMenuOptionsForGroup(-1, state.defaultGraphicUnit, groupDeepMeter); + // Update the state + setGroupChildrenState(groupChildrenState => ({ + ...groupChildrenState, + meterSelectOptions: possibleMeters, + groupSelectOptions: possibleGroups + })); + // pik is needed since the compatible units is not correct until pik is available. + // metersState normally does not change but can so include. + // groupState can change if another group is created/edited and this can change ones displayed in menus. + }, [state]); + + // Update compatible default graphic units set. + useEffect(() => { + // Graphic units compatible with currently selected meters/groups. + const compatibleGraphicUnits = new Set(); + // Graphic units incompatible with currently selected meters/groups. + const incompatibleGraphicUnits = new Set(); + // First must get a set from the array of deep meter numbers which is all meters currently in this group. + const deepMetersSet = new Set(state.deepMeters); + // Get the units that are compatible with this set of meters. + const allowedDefaultGraphicUnit = unitsCompatibleWithMeters(deepMetersSet); + // No unit allowed so modify allowed ones. Should not be there but will be fine if is. + allowedDefaultGraphicUnit.add(-99); + graphicUnitsState.possibleGraphicUnits.forEach(unit => { + // If current graphic unit exists in the set of allowed graphic units then compatible and not otherwise. + if (allowedDefaultGraphicUnit.has(unit.id)) { + compatibleGraphicUnits.add(unit); + } + else { + incompatibleGraphicUnits.add(unit); + } + }); + // Update the state + setGraphicUnitsState(graphicUnitsState => ({ + ...graphicUnitsState, + compatibleGraphicUnits: compatibleGraphicUnits, + incompatibleGraphicUnits: incompatibleGraphicUnits + })); + // If any of these change then it needs to be updated. + // metersState normally does not change but can so include. + // pik is needed since the compatible units is not correct until pik is available. + }, [graphicUnitsState.possibleGraphicUnits, state.deepMeters]); + + const tooltipStyle = { + ...tooltipBaseStyle, + tooltipCreateGroupView: 'help.admin.groupcreate' + }; + + return ( + <> + {/* Show modal button */} + + + + + +
+ +
+
+ {/* when any of the group properties are changed call one of the functions. */} + + + {/* Name input */} + + + handleStringChange(e)} + required value={state.name} + invalid={state.name === ''} /> + + + + + {/* default graphic unit input */} + + + handleNumberChange(e)}> + {/* First list the selectable ones and then the rest as disabled. */} + {Array.from(graphicUnitsState.compatibleGraphicUnits).map(unit => { + return () + })} + {Array.from(graphicUnitsState.incompatibleGraphicUnits).map(unit => { + return () + })} + + + + {/* Displayable input */} + + + handleBooleanChange(e)}> + {Object.keys(TrueFalseType).map(key => { + return () + })} + + + {/* GPS input */} + + + handleStringChange(e)} + value={getGPSString(state.gps)} /> + + + {/* Area input */} + + + + handleNumberChange(e)} + invalid={state.area < 0} /> + {/* Calculate sum of meter areas */} + + + + + + + + {/* meter area unit input */} + + + handleStringChange(e)} + invalid={state.area > 0 && state.areaUnit === AreaUnitType.none}> + {Object.keys(AreaUnitType).map(key => { + return () + })} + + + + + + + {/* Note input */} + + + handleStringChange(e)} + value={state.note} /> + + {/* The child meters in this group */} + { + + : + { + // The meters changed so update the current list of deep meters + // Get the currently included/selected meters as an array of the ids. + const updatedChildMeters = newSelectedMeterOptions.map(meter => { return meter.value; }); + // The id is not really needed so set to -1 since same function for edit. + const newDeepMeters = metersInChangedGroup({ ...state, childMeters: updatedChildMeters, id: -1 }); + // The choice may have invalidated the default graphic unit so it needs + // to be reset to no unit. + // The selection encodes this information in the color but recalculate + // to see if this is the case. + // Get the units compatible with the new set of deep meters in group. + const newAllowedDGU = unitsCompatibleWithMeters(new Set(newDeepMeters)); + // Add no unit (-99) since that is okay so no change needed if current default graphic unit. + newAllowedDGU.add(-99); + let dgu = state.defaultGraphicUnit; + if (!newAllowedDGU.has(dgu)) { + // The current default graphic unit is not compatible so set to no unit and warn admin. + notifyUser(`${translate('group.create.nounit')} "${unitsDataById[dgu].identifier}"`); + dgu = -99; + } + // Update the deep meter, child meter & default graphic unit state based on the changes. + // Note could update child meters above to avoid updating state value for metersInChangedGroup but want + // to avoid too many state updates. + // It is possible the default graphic unit is unchanged but just do this. + setState({ ...state, deepMeters: newDeepMeters, childMeters: updatedChildMeters, defaultGraphicUnit: dgu }); + }} + /> + + } + {/* The child groups in this group */} + { + : + { + // The groups changed so update the current list of deep meters + // Get the currently included/selected meters as an array of the ids. + const updatedChildGroups = newSelectedGroupOptions.map(group => { return group.value; }); + // The id is not really needed so set to -1 since same function for edit. + const newDeepMeters = metersInChangedGroup({ ...state, childGroups: updatedChildGroups, id: -1 }); + // The choice may have invalidated the default graphic unit so it needs + // to be reset to no unit. + // The selection encodes this information in the color but recalculate + // to see if this is the case. + // Get the units compatible with the new set of deep meters in group. + const newAllowedDGU = unitsCompatibleWithMeters(new Set(newDeepMeters)); + // Add no unit (-99) since that is okay so no change needed if current default graphic unit. + newAllowedDGU.add(-99); + let dgu = state.defaultGraphicUnit; + if (!newAllowedDGU.has(dgu)) { + // The current default graphic unit is not compatible so set to no unit and warn admin. + notifyUser(`${translate('group.create.nounit')} "${unitsDataById[dgu].identifier}"`); + dgu = -99; + } + // Update the deep meter, child meter & default graphic unit state based on the changes. + // Note could update child groups above to avoid updating state value for metersInChangedGroup but want + // to avoid too many state updates. + // It is possible the default graphic unit is unchanged but just do this. + setState({ ...state, deepMeters: newDeepMeters, childGroups: updatedChildGroups, defaultGraphicUnit: dgu }); + }} + /> + + } + {/* All (deep) meters in this group */} + + : + + + + + {/* Hides the modal */} + + {/* On click calls the function handleSaveChanges in this component */} + + +
+ + ); + + /** + * Converts the child meters of this group to options for menu sorted by identifier + * @returns SelectOptions sorted for child meters of group creating. + */ + function metersToSelectOptions(): SelectOption[] { + // In format for the display component for menu. + const selectedMetersUnsorted: SelectOption[] = []; + state.childMeters.forEach(groupId => { + selectedMetersUnsorted.push({ + value: groupId, + label: metersDataById[groupId].identifier + // isDisabled not needed since only used for selected and not display. + } as SelectOption + ); + }); + // Want chosen in sorted order. + return _.sortBy(selectedMetersUnsorted, item => item.label.toLowerCase(), 'asc'); + } + + /** + * Converts the child groups of this group to options for menu sorted by name + * @returns SelectOptions sorted for child groups of group editing. + */ + function groupsToSelectOptions(): SelectOption[] { + // In format for the display component for menu. + const selectedGroupsUnsorted: SelectOption[] = []; + state.childGroups.forEach(groupId => { + selectedGroupsUnsorted.push({ + value: groupId, + label: groupsDataById[groupId].name + // isDisabled not needed since only used for selected and not display. + } as SelectOption + ); + }); + // Want chosen in sorted order. + return _.sortBy(selectedGroupsUnsorted, item => item.label.toLowerCase(), 'asc'); + } + + /** + * Converts the deep meters of this group to list options sorted by identifier. + * @returns names of all child meters in sorted order. + */ + function deepMetersToList() { + // Create list of meter identifiers. + const listedDeepMeters: string[] = []; + state.deepMeters.forEach(meterId => { + listedDeepMeters.push(metersDataById[meterId].identifier); + }); + // Sort for display. + return listedDeepMeters.sort(); + } +} diff --git a/src/client/app/components/groups/EditGroupModalComponent.tsx b/src/client/app/components/groups/EditGroupModalComponent.tsx index 050ef0f06..3a5eddad1 100644 --- a/src/client/app/components/groups/EditGroupModalComponent.tsx +++ b/src/client/app/components/groups/EditGroupModalComponent.tsx @@ -5,40 +5,40 @@ import * as _ from 'lodash'; import * as React from 'react'; // Realize that * is already imported from react -import { useState, useEffect } from 'react'; -import MultiSelectComponent from '../MultiSelectComponent'; -import { SelectOption } from '../../types/items'; -import { useDispatch, useSelector } from 'react-redux'; -import { State } from 'types/redux/state'; +import { useEffect, useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { useSelector } from 'react-redux'; import { Button, Col, Container, FormFeedback, FormGroup, Input, InputGroup, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; -import { FormattedMessage } from 'react-intl'; -import translate from '../../utils/translate'; -import TooltipMarkerComponent from '../TooltipMarkerComponent'; +import { State } from 'types/redux/state'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; -import ListDisplayComponent from '../ListDisplayComponent'; -import '../../styles/modal.css'; import '../../styles/card-page.css'; -import { deleteGroup, submitGroupEdits } from '../../actions/groups'; -import { TrueFalseType } from '../../types/items'; -import { isRoleAdmin } from '../../utils/hasPermissions'; +import '../../styles/modal.css'; +import { tooltipBaseStyle } from '../../styles/modalStyle'; +import { DataType } from '../../types/Datasources'; +import { ConversionArray } from '../../types/conversionArray'; +import { SelectOption, TrueFalseType } from '../../types/items'; +import { GroupData } from '../../types/redux/groups'; import { UnitData } from '../../types/redux/units'; +import { groupsApi } from '../../utils/api'; +import { GPSPoint, isValidGPSInput } from '../../utils/calibration'; import { - unitsCompatibleWithMeters, getMeterMenuOptionsForGroup, getGroupMenuOptionsForGroup, - getCompatibilityChangeCase, GroupCase + GroupCase, + getCompatibilityChangeCase, + getGroupMenuOptionsForGroup, + getMeterMenuOptionsForGroup, + unitsCompatibleWithMeters } from '../../utils/determineCompatibleUnits'; -import { ConversionArray } from '../../types/conversionArray'; -import { GPSPoint, isValidGPSInput } from '../../utils/calibration'; -import { notifyUser, getGPSString, nullToEmptyString } from '../../utils/input'; -import { GroupDefinition } from '../../types/redux/groups'; -import ConfirmActionModalComponent from '../ConfirmActionModalComponent' -import { DataType } from '../../types/Datasources'; -import { groupsApi } from '../../utils/api'; -import { tooltipBaseStyle } from '../../styles/modalStyle'; import { AreaUnitType, getAreaUnitConversion } from '../../utils/getAreaUnitConversion'; -import { Dispatch } from 'types/redux/actions'; +import { isRoleAdmin } from '../../utils/hasPermissions'; +import { getGPSString, notifyUser, nullToEmptyString } from '../../utils/input'; +import translate from '../../utils/translate'; +import ConfirmActionModalComponent from '../ConfirmActionModalComponent'; +import ListDisplayComponent from '../ListDisplayComponent'; +import MultiSelectComponent from '../MultiSelectComponent'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; interface EditGroupModalComponentProps { show: boolean; @@ -56,7 +56,7 @@ interface EditGroupModalComponentProps { * @returns Group edit element */ export default function EditGroupModalComponent(props: EditGroupModalComponentProps) { - const dispatch: Dispatch = useDispatch(); + // const dispatch: Dispatch = useDispatch(); // Meter state const metersState = useSelector((state: State) => state.meters.byMeterID); @@ -167,7 +167,7 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr // Do not call the handler function because we do not want to open the parent modal setShowDeleteConfirmationModal(false); // Delete the group using the state object where only really need id. - dispatch(deleteGroup(groupState)); + // dispatch(deleteGroup(groupState)); } /* End Confirm Delete Modal */ @@ -314,7 +314,6 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr } // For all changed groups, save the new group to the DB. - let i = 1; groupsChanged.forEach(groupId => { const thisGroupState = editGroupsState[groupId]; // There are extra properties in the state so only include the desired ones for edit submit. @@ -324,10 +323,10 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr childGroups: thisGroupState.childGroups, gps: gps, displayable: thisGroupState.displayable, note: thisGroupState.note, area: thisGroupState.area, defaultGraphicUnit: thisGroupState.defaultGraphicUnit, areaUnit: thisGroupState.areaUnit } + console.log(submitState, 'removeme') // This saves group to the DB and then refreshes the window if the last group being updated and // changes were made to the children. This avoid a reload on name change, etc. - dispatch(submitGroupEdits(submitState, (i === groupsChanged.length ? true : false) && (childMeterChanges || childGroupChanges))); - i++; + // dispatch(submitGroupEdits(submitState, (i === groupsChanged.length ? true : false) && (childMeterChanges || childGroupChanges))); }); // The next line is unneeded since do refresh. // dispatch(removeUnsavedChanges()); @@ -1057,7 +1056,7 @@ function calculateMetersInGroup(groupId: number, groupState: any, times: number return []; } // Group to get the deep meters for. - const groupToCheck = groupState[groupId] as GroupDefinition; + const groupToCheck = groupState[groupId] as GroupData; // Use a set to avoid duplicates. // The deep meters are the direct child meters of this group plus the direct child meters // of all included meters, recursively. diff --git a/src/client/app/components/groups/EditGroupModalComponentWIP.tsx b/src/client/app/components/groups/EditGroupModalComponentWIP.tsx new file mode 100644 index 000000000..371f746cf --- /dev/null +++ b/src/client/app/components/groups/EditGroupModalComponentWIP.tsx @@ -0,0 +1,1087 @@ +/* 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 _ from 'lodash'; +import * as React from 'react'; +// Realize that * is already imported from react +import { useEffect, useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { + Button, Col, Container, FormFeedback, FormGroup, Input, InputGroup, + Label, Modal, ModalBody, ModalFooter, ModalHeader, Row +} from 'reactstrap'; +import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import { groupsApi, selectGroupDataById } from '../../redux/api/groupsApi'; +import { selectMeterDataById } from '../../redux/api/metersApi'; +import { useAppSelector } from '../../redux/hooks'; +import { selectPossibleGraphicUnits } from '../../redux/selectors/adminSelectors'; +import { selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; +import { store } from '../../store'; +import '../../styles/card-page.css'; +import '../../styles/modal.css'; +import { tooltipBaseStyle } from '../../styles/modalStyle'; +import { DataType } from '../../types/Datasources'; +import { SelectOption, TrueFalseType } from '../../types/items'; +import { GroupData } from '../../types/redux/groups'; +import { UnitData } from '../../types/redux/units'; +import { GPSPoint, isValidGPSInput } from '../../utils/calibration'; +import { + GroupCase, + getCompatibilityChangeCase, + getGroupMenuOptionsForGroup, + getMeterMenuOptionsForGroup, + unitsCompatibleWithMeters +} from '../../utils/determineCompatibleUnits'; +import { AreaUnitType, getAreaUnitConversion } from '../../utils/getAreaUnitConversion'; +import { getGPSString, notifyUser, nullToEmptyString } from '../../utils/input'; +import translate from '../../utils/translate'; +import ConfirmActionModalComponent from '../ConfirmActionModalComponent'; +import ListDisplayComponent from '../ListDisplayComponent'; +import MultiSelectComponent from '../MultiSelectComponent'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; + +interface EditGroupModalComponentProps { + show: boolean; + groupId: number; + // passed in to handle opening the modal + handleShow: () => void; + // passed in to handle closing the modal + handleClose: () => void; +} + +/** + * Defines the edit group modal form + * @param props state variables needed to define the component + * @returns Group edit element + */ +export default function EditGroupModalComponentWIP(props: EditGroupModalComponentProps) { + const [submitGroupEdits] = groupsApi.useEditGroupMutation() + const [deleteGroup] = groupsApi.useDeleteGroupMutation() + // Meter state + const { data: metersState = {} } = useAppSelector(selectMeterDataById); + // Group state used on other pages + const { data: globalGroupsState = {} } = useAppSelector(selectGroupDataById); + // Make a local copy of the group data so we can update during the edit process. + // When the group is saved the values will be synced again with the global state. + // This needs to be a deep clone so the changes are only local. + const [editGroupsState, setEditGroupsState] = useState(_.cloneDeep(globalGroupsState)); + const possibleGraphicUnits = useAppSelector(selectPossibleGraphicUnits) + + // The current groups state of group being edited of the local copy. It should always be valid. + const groupState = editGroupsState[props.groupId]; + + // Check for admin status + const loggedInAsAdmin = useAppSelector(selectIsLoggedInAsAdmin); + + // The information on the allowed children of this group that can be selected in the menus. + const groupChildrenDefaults = { + // The meter selections in format for selection dropdown. + meterSelectOptions: [] as SelectOption[], + // The group selections in format for selection dropdown. + groupSelectOptions: [] as SelectOption[] + } + + // Information on the default graphic unit values. + const graphicUnitsStateDefaults = { + possibleGraphicUnits: possibleGraphicUnits, + compatibleGraphicUnits: possibleGraphicUnits, + incompatibleGraphicUnits: new Set() + } + + /* State */ + // Handlers for each type of input change where update the local edit state. + + const handleStringChange = (e: React.ChangeEvent) => { + setEditGroupsState({ + ...editGroupsState, + [groupState.id]: { + ...editGroupsState[groupState.id], + [e.target.name]: e.target.value + } + }) + } + + const handleBooleanChange = (e: React.ChangeEvent) => { + setEditGroupsState({ + ...editGroupsState, + [groupState.id]: { + ...editGroupsState[groupState.id], + [e.target.name]: JSON.parse(e.target.value) + } + }) + } + + const handleNumberChange = (e: React.ChangeEvent) => { + setEditGroupsState({ + ...editGroupsState, + [groupState.id]: { + ...editGroupsState[groupState.id], + [e.target.name]: Number(e.target.value) + } + }) + } + + // Dropdowns state + const [groupChildrenState, setGroupChildrenState] = useState(groupChildrenDefaults) + const [graphicUnitsState, setGraphicUnitsState] = useState(graphicUnitsStateDefaults); + + /* Edit Group Validation: + Name cannot be blank + Area must be positive or zero + If area is nonzero, area unit must be set + Group must have at least one child (i.e has deep child meters) + */ + const [validGroup, setValidGroup] = useState(false); + useEffect(() => { + setValidGroup( + groupState.name !== '' && + (groupState.area === 0 || (groupState.area > 0 && groupState.areaUnit !== AreaUnitType.none)) && + (groupState.deepMeters.length > 0) + ); + }, [groupState.area, groupState.areaUnit, groupState.name, groupState.deepMeters]); + /* End State */ + + /* Confirm Delete Modal */ + // Separate from state comment to keep everything related to the warning confirmation modal together + const [showDeleteConfirmationModal, setShowDeleteConfirmationModal] = useState(false); + const deleteConfirmationMessage = translate('group.delete.group') + ' "' + groupState.name + '"?'; + const deleteConfirmText = translate('group.delete.group'); + const deleteRejectText = translate('cancel'); + // The first two handle functions below are required because only one Modal can be open at a time. + // The messages for delete are a modal so a separate one. Note other user messages are window popups. + // TODO We should probably go all to modal or popups for messages. + const handleDeleteConfirmationModalClose = () => { + // Hide the warning modal + setShowDeleteConfirmationModal(false); + // Show the edit modal + handleShow(); + } + const handleDeleteConfirmationModalOpen = () => { + // Hide the edit modal + handleClose(); + // Show the warning modal + setShowDeleteConfirmationModal(true); + } + const handleDeleteGroup = () => { + // Closes the warning modal + // Do not call the handler function because we do not want to open the parent modal + setShowDeleteConfirmationModal(false); + // Delete the group using the state object where only really need id. + deleteGroup(groupState.id) + } + /* End Confirm Delete Modal */ + + // Sums the area of the group's deep meters. It will tell the admin if any meters are omitted from the calculation, + // or if any other errors are encountered. + const handleAutoCalculateArea = () => { + if (groupState.deepMeters.length > 0) { + if (groupState.areaUnit != AreaUnitType.none) { + let areaSum = 0; + let notifyMsg = ''; + groupState.deepMeters.forEach(meterID => { + const meter = metersState[meterID]; + if (meter.area > 0) { + if (meter.areaUnit != AreaUnitType.none) { + areaSum += meter.area * getAreaUnitConversion(meter.areaUnit, groupState.areaUnit); + } else { + // This shouldn't happen because of the other checks in place when editing/creating a meter. + // However, there could still be edge cases (i.e meters from before area units were added) that could violate this. + notifyMsg += '\n"' + meter.identifier + '"' + translate('group.area.calculate.error.unit'); + } + } else { + notifyMsg += '\n"' + meter.identifier + '"' + translate('group.area.calculate.error.zero'); + } + }); + let msg = translate('group.area.calculate.header') + areaSum + ' ' + translate(`AreaUnitType.${groupState.areaUnit}`); + if (notifyMsg != '') { + msg += '\n' + translate('group.area.calculate.error.header') + notifyMsg; + } + if (window.confirm(msg)) { + setEditGroupsState({ + ...editGroupsState, + [groupState.id]: { + ...editGroupsState[groupState.id], + // the + here converts back into a number. this method also removes trailing zeroes. + ['area']: +areaSum.toPrecision(6) + } + }); + } + } else { + notifyUser(translate('group.area.calculate.error.group.unit')); + } + } else { + notifyUser(translate('group.area.calculate.error.no.meters')); + } + } + + // Reset the state to default values. + // To be used for the discard changes button + // Different use case from CreateGroupModalComponent's resetState + // This allows us to reset our state to match the store in the event of an edit failure + // Failure to edit groups will not trigger a re-render, as no state has changed. Therefore, we must manually reset the values + const resetState = () => { + // Set back to the global group values for this group. As before, need a deep copy. + setEditGroupsState(_.cloneDeep(globalGroupsState)); + // Set back to the default values for the menus. + setGroupChildrenState(groupChildrenDefaults); + setGraphicUnitsState(graphicUnitsStateDefaults); + } + + // Should show the modal for editing. + const handleShow = () => { + props.handleShow(); + } + + // Note this differs from the props.handleClose(). This is only called when the user + // clicks to discard or close the modal. + const handleClose = () => { + props.handleClose(); + if (loggedInAsAdmin) { + // State cannot change if you are not an admin. + resetState(); + } + } + + // Save changes - done when admin clicks the save button. + const handleSubmit = () => { + // Close the modal first to avoid repeat clicks + props.handleClose(); + + // true if inputted values are okay. Then can submit. + let inputOk = true; + + // Check for changes by comparing the original, global state to edited state. + // This is the unedited state of the group being edited to compare to for changes. + const originalGroupState = globalGroupsState[groupState.id]; + // Check children separately since lists. + const childMeterChanges = !_.isEqual(originalGroupState.childMeters, groupState.childMeters); + const childGroupChanges = !_.isEqual(originalGroupState.childGroups, groupState.childGroups); + const groupHasChanges = + ( + originalGroupState.name != groupState.name || + originalGroupState.displayable != groupState.displayable || + originalGroupState.gps != groupState.gps || + originalGroupState.note != groupState.note || + originalGroupState.area != groupState.area || + originalGroupState.defaultGraphicUnit != groupState.defaultGraphicUnit || + childMeterChanges || + childGroupChanges || + originalGroupState.areaUnit != groupState.areaUnit + ); + // Only validate and store if any changes. + if (groupHasChanges) { + //Check GPS is okay. + const gpsInput = groupState.gps; + let gps: GPSPoint | null = null; + const latitudeIndex = 0; + const longitudeIndex = 1; + // If the user input a value then gpsInput should be a string + // null came from DB and it is okay to just leave it - Not a String. + if (typeof gpsInput === 'string') { + if (isValidGPSInput(gpsInput)) { + // Clearly gpsInput is a string but TS complains about the split so cast. + const gpsValues = (gpsInput as string).split(',').map((value: string) => parseFloat(value)); + // It is valid and needs to be in this format for routing + gps = { + longitude: gpsValues[longitudeIndex], + latitude: gpsValues[latitudeIndex] + }; + } else if ((gpsInput as string).length !== 0) { + // GPS not okay and there since non-zero length value. + // TODO isValidGPSInput currently pops up an alert so not doing it here, may change + // so leaving code commented out. + // notifyUser(translate('input.gps.range') + groupState.gps + '.'); + inputOk = false; + } + } + + if (inputOk) { + // The input passed validation so okay to save. + + // A change in this group may have changed other group's default graphic unit. Thus, create a list of + // all groups needing to be saved. The change would have already + // been made in the edit state. + const groupsChanged: number[] = []; + Object.values(editGroupsState).forEach(group => { + if (group.defaultGraphicUnit !== globalGroupsState[group.id].defaultGraphicUnit) { + groupsChanged.push(group.id); + } + }); + // Make sure the group being edited is on the list. + if (!groupsChanged.includes(groupState.id)) { + // Add the edited one to the list. + groupsChanged.push(groupState.id); + } + + // For all changed groups, save the new group to the DB. + groupsChanged.forEach(groupId => { + const thisGroupState = editGroupsState[groupId]; + // There are extra properties in the state so only include the desired ones for edit submit. + // GPS is one above since may differ from the state. + const submitState = { + id: thisGroupState.id, name: thisGroupState.name, childMeters: thisGroupState.childMeters, + childGroups: thisGroupState.childGroups, gps: gps, displayable: thisGroupState.displayable, + note: thisGroupState.note, area: thisGroupState.area, defaultGraphicUnit: thisGroupState.defaultGraphicUnit, areaUnit: thisGroupState.areaUnit + } + console.log(submitState, 'removeme') + // This saves group to the DB and then refreshes the window if the last group being updated and + // changes were made to the children. This avoid a reload on name change, etc. + submitGroupEdits(submitState) + }); + // The next line is unneeded since do refresh. + // dispatch(removeUnsavedChanges()); + } else { + notifyUser(translate('group.input.error')); + } + } + }; + + // Determine allowed child meters/groups . + useEffect(() => { + // Can only vary if admin and only used then. + if (loggedInAsAdmin) { + // Get meters that okay for this group in a format the component can display. + const possibleMeters = getMeterMenuOptionsForGroup(groupState.defaultGraphicUnit, groupState.deepMeters); + // Get groups okay for this group. Similar to meters. + const possibleGroups = getGroupMenuOptionsForGroup(groupState.id, groupState.defaultGraphicUnit, groupState.deepMeters); + // Update the state + setGroupChildrenState(groupChildrenState => ({ + ...groupChildrenState, + meterSelectOptions: possibleMeters, + groupSelectOptions: possibleGroups + })); + } + // pik is needed since the compatible units is not correct until pik is available. + // metersState normally does not change but can so include. + // globalGroupsState can change if another group is created/edited and this can change ones displayed in menus. + }, [groupState.deepMeters, groupState.defaultGraphicUnit, groupState.id, loggedInAsAdmin]); + + // Update default graphic units set. + useEffect(() => { + // Only shown to an admin. + if (loggedInAsAdmin) { + // Graphic units compatible with currently selected meters/groups. + const compatibleGraphicUnits = new Set(); + // Graphic units incompatible with currently selected meters/groups. + const incompatibleGraphicUnits = new Set(); + // First must get a set from the array of deep meter numbers which is all meters currently in this group. + const deepMetersSet = new Set(groupState.deepMeters); + // Get the units that are compatible with this set of meters. + const allowedDefaultGraphicUnit = unitsCompatibleWithMeters(deepMetersSet); + // No unit allowed so modify allowed ones. Should not be there but will be fine if is since set. + allowedDefaultGraphicUnit.add(-99); + graphicUnitsState.possibleGraphicUnits.forEach(unit => { + // If current graphic unit exists in the set of allowed graphic units then compatible and not otherwise. + if (allowedDefaultGraphicUnit.has(unit.id)) { + compatibleGraphicUnits.add(unit); + } else { + incompatibleGraphicUnits.add(unit); + } + }); + // Update the state + setGraphicUnitsState(graphicUnitsState => ({ + ...graphicUnitsState, + compatibleGraphicUnits: compatibleGraphicUnits, + incompatibleGraphicUnits: incompatibleGraphicUnits + })); + } + // If any of these change then it needs to be updated. + // pik is needed since the compatible units is not correct until pik is available. + // metersState normally does not change but can so include. + // If another group that is included in this group is changed then it must be redone + // but we currently do a refresh so it is covered. It should still be okay if + // the deep meters of this group are properly updated. + }, [graphicUnitsState.possibleGraphicUnits, groupState.deepMeters, loggedInAsAdmin]); + + const tooltipStyle = { + ...tooltipBaseStyle, + // Switch help depending if admin or not. + tooltipEditGroupView: loggedInAsAdmin ? 'help.admin.groupedit' : 'help.groups.groupdetails' + }; + + return ( + <> + {/* This is for the modal for delete. */} + + + {/* In a number of the items that follow, what is shown varies on whether you are an admin. */} + + + +
+ +
+
+ + {loggedInAsAdmin ? + + {/* Name input for admin*/} + + + handleStringChange(e)} + required value={groupState.name} + invalid={groupState.name === ''} /> + + + + + {/* default graphic unit input for admin */} + + + handleNumberChange(e)}> + {/* First list the selectable ones and then the rest as disabled. */} + {Array.from(graphicUnitsState.compatibleGraphicUnits).map(unit => { + return () + })} + {Array.from(graphicUnitsState.incompatibleGraphicUnits).map(unit => { + return () + })} + + + + : <> + {/* Name display for non-admin */} + + + + + {/* default graphic unit display for non-admin */} + + + {/* TODO: This component still displays a dropdown arrow, even though a user cannot use the dropdown */} + + {Array.from(graphicUnitsState.compatibleGraphicUnits).map(unit => { + return () + })} + + + } + {loggedInAsAdmin && <> + + + {/* Displayable input, only for admin. */} + + + handleBooleanChange(e)}> + {Object.keys(TrueFalseType).map(key => { + return () + })} + + + + + {/* GPS input, only for admin. */} + + + handleStringChange(e)} + value={getGPSString(groupState.gps)} /> + + + + + + {/* Area input, only for admin. */} + + + + handleNumberChange(e)} + invalid={groupState.area < 0} /> + {/* Calculate sum of meter areas */} + + + + + + + + + + {/* meter area unit input */} + + + handleStringChange(e)} + invalid={groupState.area > 0 && groupState.areaUnit === AreaUnitType.none}> + {Object.keys(AreaUnitType).map(key => { + return () + })} + + + + + + + + {/* Note input, only for admin. */} + + + handleStringChange(e)} + value={nullToEmptyString(groupState.note)} /> + + } + {/* The child meters in this group */} + {loggedInAsAdmin ? + + : + { + // The meters changed so verify update is okay and deal with appropriately. + // The length of selected meters should only vary by 1 since each change is handled separately. + // Compare the new length to the original length that is the same as + // the number of child meters of group being edited. + if (newSelectedMeterOptions.length === groupState.childMeters.length + 1) { + // A meter was selected so it is considered for adding. + // The newly selected item is always the last one. + // Now attempt to add the child to see if okay. + const childAdded = await assignChildToGroup(newSelectedMeterOptions[newSelectedMeterOptions.length - 1].value, DataType.Meter); + if (!childAdded) { + // The new child meter was rejected so remove it. It is the last one. + newSelectedMeterOptions.pop(); + } + } else { + // Could have removed any item so figure out which one it is. Need to convert options to ids. + const removedMeter = _.difference(groupState.childMeters, newSelectedMeterOptions.map(item => { return item.value; })); + // There should only be one removed item. + const removedMeterId = removedMeter[0]; + const childRemoved = removeChildFromGroup(removedMeterId, DataType.Meter) + if (!childRemoved) { + // The new child meter removal was rejected so put it back. Should only be one item so no need to sort. + newSelectedMeterOptions.push({ + value: removedMeterId, + label: metersState[removedMeterId].identifier + // isDisabled not needed since only used for selected and not display. + } as SelectOption + ); + } + } + }} + /> + + : + + : + + + } + {/* The child groups in this group */} + {loggedInAsAdmin ? + + : + { + // The groups changed so verify update is okay and deal with appropriately. + // The length of of selected groups should only vary by 1 since each change is handled separately. + // Compare the new length to the original length that is the same as + // the number of child groups of group being edited. + if (newSelectedGroupOptions.length === groupState.childGroups.length + 1) { + // A group was selected so it is considered for adding. + // The newly selected item is always the last one. + // Now attempt to add the child to see if okay. + const childAdded = await assignChildToGroup(newSelectedGroupOptions[newSelectedGroupOptions.length - 1].value, DataType.Group); + if (!childAdded) { + // The new child meter was rejected so remove it. It is the last one. + newSelectedGroupOptions.pop(); + } + } else { + // Could have removed any item so figure out which one it is. Need to convert options to ids. + const removedGroup = _.difference(groupState.childGroups, newSelectedGroupOptions.map(item => { return item.value; })); + // There should only be one removed item. + const removedGroupId = removedGroup[0]; + const childRemoved = removeChildFromGroup(removedGroupId, DataType.Group) + if (!childRemoved) { + // The new child group removal was rejected so put it back. Should only be one item so no need to sort. + newSelectedGroupOptions.push({ + value: removedGroupId, + // The name should not have changed since cannot be group editing but use the edit state to be consistent. + label: editGroupsState[removedGroupId].name + // isDisabled not needed since only used for selected and not display. + } as SelectOption + ); + } + } + }} + /> + + : + + : + + + } + {/* All (deep) meters in this group */} + : + + + + {/* Delete, discard & save buttons if admin and close button if not. */} + {loggedInAsAdmin ? +
+ {/* delete group */} + + {/* Hides the modal */} + + {/* On click calls the function handleSaveChanges in this component */} + +
+ : + + } +
+
+ + ); + + // The following functions are nested so can easily get and set the state that is local to the outer function. + + /** + * Validates and warns user when adding a child group/meter to a specific group. + * If the check pass, update the edited group and related groups. + * @param childId The group/meter's id to add to the parent group. + * @param childType Can be group or meter. + * @returns true if the child was assigned and false otherwise + */ + async function assignChildToGroup(childId: number, childType: DataType): Promise { + // Create a deep copy of the edit state before adding the child. We only need some of the state but this is easier. + // This copy is directly changed without using the Redux hooks since it is not used by React. + // This means that changes to the group do not happen unless the change is accepted and this copy is + // put back into the edit state. + const tempGroupsState = _.cloneDeep(editGroupsState); + + // Add the child to the group being edited in temp so can decide if want change. + // This assumes there are no duplicates which is not allowed by menus + if (childType === DataType.Meter) { + tempGroupsState[groupState.id].childMeters.push(childId); + } else { + tempGroupsState[groupState.id].childGroups.push(childId); + } + // The deep meters of any group can change for any group containing the group that just had a meter/group added. + // Since groups can be indirectly included in another group it is hard to know which ones where impacted so + // just redo them all for now. Also do this group since it likely changed. + // Returned value tells if update should happen. + let shouldUpdate = !Object.values(tempGroupsState).some(group => { + const deepMeters = calculateMetersInGroup(group.id, tempGroupsState); + if (deepMeters.length === 0) { + // There is a circular dependency so this change is not allowed. + // Cannot be case of no children since adding child. + // Let the user know. + notifyUser(`${translate('group.edit.circular')}\n\n${translate('group.edit.cancelled')}`); + // Stops processing and will return this result (negated). + return true; + } else { + // Group okay so update deep meters for it. + tempGroupsState[group.id].deepMeters = deepMeters; + // Go to next group/keep processing. + return false; + } + }); + + // Only do next step if update is still possible. + if (shouldUpdate) { + // Get all parent groups of this group. + const { data: parentGroupIDs = [] } = await store.dispatch(groupsApi.endpoints.getParentIDs.initiate(groupState.id, { subscribe: false })) + // Check for group changes and have admin agree or not. + shouldUpdate = await validateGroupPostAddChild(groupState.id, parentGroupIDs, tempGroupsState); + } + // If the admin wants to apply changes and allowed. + if (shouldUpdate) { + // Update the group. Now, the changes actually happen. + // Done by setting the edit state to the temp state so does not impact other groups + // and what is seen until the admin saves. + // Could limit to only ones changed but just do since local state and easy pull easy to see changed by Redux. + setEditGroupsState(tempGroupsState); + } + + // Tell if applied update. + return shouldUpdate; + } + + /** + * Determines if the change in compatible units of one group are okay with another group. + * Warns admin of changes and returns true if the changes should happen. + * @param gid The group that has a change in compatible units. + * @param parentGroupIds The parent groups' ids of that group. + * @param groupsState The local group state to use. + * @returns true if change fine or if admin agreed. false if admin does not or the change is an issue. + */ + function validateGroupPostAddChild(gid: number, parentGroupIds: number[], groupsState: any): boolean { + // This will hold the overall message for the admin alert. + let msg = ''; + // Tells if the change should be cancelled. + let cancel = false; + // We check the group being edited and all parent groups for changes in default graphic unit. + for (const groupId of [...parentGroupIds, gid]) { + // Use the edit group since want the current values for deepMeters for comparison. + const parentGroup = editGroupsState[groupId]; + // Get parent's compatible units + const parentCompatibleUnits = unitsCompatibleWithMeters(new Set(parentGroup.deepMeters)); + // Get compatibility change case when add this group to its parent. + const compatibilityChangeCase = getCompatibilityChangeCase(parentCompatibleUnits, gid, DataType.Group, + parentGroup.defaultGraphicUnit, groupsState[groupId].deepMeters); + switch (compatibilityChangeCase) { + case GroupCase.NoCompatibleUnits: + // The group has no compatible units so cannot do this. + msg += `${translate('group')} "${parentGroup.name}" ${translate('group.edit.nocompatible')}\n`; + cancel = true; + break; + + case GroupCase.LostDefaultGraphicUnit: + // The group has fewer compatible units and one of the lost ones is the default graphic unit. + msg += `${translate('group')} "${parentGroup.name}" ${translate('group.edit.nounit')}\n`; + // The current default graphic unit is no longer valid so make it no unit. + groupsState[groupId].defaultGraphicUnit = -99; + break; + + case GroupCase.LostCompatibleUnits: + // The group has fewer compatible units but the default graphic unit is still allowed. + msg += `${translate('group')} "${parentGroup.name}" ${translate('group.edit.changed')}\n`; + break; + + // Case NoChange requires no message. + } + } + if (msg !== '') { + // There is a message to display to the user. + if (cancel) { + // If cancel is true, doesn't allow the admin to apply changes. + msg += `\n${translate('group.edit.cancelled')}`; + notifyUser(msg); + } else { + // If msg is not empty, warns the admin and asks if they want to apply changes. + msg += `\n${translate('group.edit.verify')}`; + cancel = !window.confirm(msg); + } + } + return !cancel; + } + + /** + * Handles removing child from a group. + * @param childId The group/meter's id to add to the parent group. + * @param childType Can be group or meter. + * @returns true if change fine or if admin agreed. false if admin does not or the change is an issue. + */ + function removeChildFromGroup(childId: number, childType: DataType): boolean { + // Unlike adding, you do not change the default graphic unit by removing. Thus, you only need to recalculate the + // deep meters and remove this child from the group being edited. + + // Create a deep copy of the edit state before adding the child. We only need some of the state but this is easier. + // This copy is directly changed without using the Redux hooks since it is not used by React. + // This means that changes to the group do not happen put back into the edit state. + // For the record, it was tried to not create the copy and update the edit state for each change. This had + // two issues. First, the next step in this function does not see the change because Redux does not update + // until the next render. Second, and more importantly, the updated state was not showing during the render. + // Why that is the case was unclear because the set value were correct. Given all of this and to make the + // code more similar to add, it is done with a copy. + const tempGroupsState = _.cloneDeep(editGroupsState); + + // Add the child to the group being edited. + if (childType === DataType.Meter) { + // All the children without one being removed. + const newChildren = _.filter(tempGroupsState[groupState.id].childMeters, value => value != childId); + tempGroupsState[groupState.id].childMeters = newChildren; + } else { + // All the children without one being removed. + const newChildren = _.filter(tempGroupsState[groupState.id].childGroups, value => value != childId); + tempGroupsState[groupState.id].childGroups = newChildren; + } + + // The deep meters of any group can change for any group containing the group that just had a meter/group added. + // Since groups can be indirectly included in another group it is hard to know which ones where impacted so + // just redo them all for now. Also do this group since it likely changed. + const groupOk = !Object.values(tempGroupsState).some(group => { + const newDeepMeters = calculateMetersInGroup(group.id, tempGroupsState); + // If the array is empty then there are no child meters nor groups and this is not allowed. + // The change is rejected. + // This should only happen for the group being edited but check for all since easier. + if (newDeepMeters.length === 0) { + // Let the user know. + notifyUser(`${translate('group.edit.empty')}\n\n${translate('group.edit.cancelled')}`); + // Indicate issue and stop processing. + return true; + } else { + // Update the temp deep meters and continue. + tempGroupsState[group.id].deepMeters = newDeepMeters; + return false; + } + }); + + // Only update if the group is okay. + if (groupOk) { + // Update the group. Now, the changes actually happen. + // Done by setting the edit state to the temp state so does not impact other groups + // and what is seen until the admin saves. + // Could limit to only ones changed but just do since local state and easy. + setEditGroupsState(tempGroupsState); + } + // Tells if the edit was accepted. + return groupOk; + } + + /** + * Checks if this group is contained in another group. If so, no delete. + * If not, then continue delete process. + */ + async function validateDelete() { + // Get all parent groups of this group. + const { data: parentGroupIDs = [] } = await store.dispatch(groupsApi.endpoints.getParentIDs.initiate(groupState.id, { subscribe: false })) + + // If there are parents then you cannot delete this group. Notify admin. + if (parentGroupIDs.length !== 0) { + // This will hold the overall message for the admin alert. + let msg = `${translate('group')} "${groupState.name}" ${translate('group.delete.issue')}:\n`; + parentGroupIDs.forEach(groupId => { + msg += `${editGroupsState[groupId].name}\n`; + }) + msg += `\n${translate('group.edit.cancelled')}`; + notifyUser(msg); + } else { + // The group can be deleted. + handleDeleteConfirmationModalOpen(); + } + } + + /** + * Converts the child meters of this group to options for menu sorted by identifier + * @returns sorted SelectOption for child meters of group editing. + */ + function metersToSelectOptions(): SelectOption[] { + // In format for the display component for menu. + const selectedMetersUnsorted: SelectOption[] = []; + groupState.childMeters.forEach(groupId => { + selectedMetersUnsorted.push({ + value: groupId, + label: metersState[groupId].identifier + // isDisabled not needed since only used for selected and not display. + } as SelectOption + ); + }); + // Want chosen in sorted order. + return _.sortBy(selectedMetersUnsorted, item => item.label.toLowerCase(), 'asc'); + } + + /** + * Converts the child groups of this group to options for menu sorted by name + * @returns sorted SelectOption for child groups of group editing. + */ + function groupsToSelectOptions(): SelectOption[] { + // In format for the display component for menu. + const selectedGroupsUnsorted: SelectOption[] = []; + groupState.childGroups.forEach(groupId => { + selectedGroupsUnsorted.push({ + value: groupId, + // Use globalGroupsState so see edits in other groups. You would miss an update + // in this group but it cannot be on the menu so that is okay. + label: globalGroupsState[groupId].name + // isDisabled not needed since only used for selected and not display. + } as SelectOption + ); + }); + // Want chosen in sorted order. + return _.sortBy(selectedGroupsUnsorted, item => item.label.toLowerCase(), 'asc'); + } + + /** + * Converts the child meters of this group to list options sorted by name. + * This is needed for non-admins. Hidden items are not shown but noted in list. + * @returns names of all child meters in sorted order. + */ + function metersToList(): string[] { + // Hold the list for display. + const listedMeters: string[] = []; + // Tells if any meter is not visible to user. + let hasHidden = false; + groupState.childMeters.forEach(meterId => { + const meterIdentifier = metersState[meterId].identifier; + // The identifier is null if the meter is not visible to this user. If hidden then do + // not list and otherwise label. + if (meterIdentifier === null) { + hasHidden = true; + } else { + listedMeters.push(meterIdentifier); + } + }); + // Sort for display. Before were sorted by id so not okay here. + listedMeters.sort(); + if (hasHidden) { + // There are hidden meters so note at bottom of list. + listedMeters.push(translate('meter.hidden')); + } + return listedMeters; + } + + /** + * Converts the child meters of this group to list options sorted by name. + * This is needed for non-admins. Hidden items are not shown but noted in list. + * @returns names of all child meters in sorted order. + */ + function groupsToList(): string[] { + const listedGroups: string[] = []; + let hasHidden = false; + groupState.childGroups.forEach(groupId => { + // The name is null if the group is not visible to this user. + // TODO The following line should work but does not (it does for meters). + // The Redux state has the name of hidden groups but it should not. A quick + // attempt to fix did not work as login/out did not clear as expected when + // control what is returned. This needs to be addressed. + // if (groupName !== null) { + // For now, check if the group is displayable. + if (editGroupsState[groupId].displayable) { + listedGroups.push(editGroupsState[groupId].name); + } else { + hasHidden = true; + } + }); + // Sort for display. Before were sorted by id so not okay here. + listedGroups.sort(); + if (hasHidden) { + // There are hidden groups so note at bottom of list. + listedGroups.push(translate('group.hidden')); + } + return listedGroups; + } + + /** + * Converts the deep meters of this group to list options sorted by identifier. + * Hidden items are not shown but noted in list; admins should never see that. + * @returns names of all child meters in sorted order. + */ + function deepMetersToList() { + // Unlike child meter/group, these are lists for all users. + const listedDeepMeters: string[] = []; + let hasHidden = false; + groupState.deepMeters.forEach(meterId => { + const meterIdentifier = metersState[meterId].identifier; + if (meterIdentifier === null) { + // The identifier is null if the meter is not visible to this user. + hasHidden = true; + } else { + // If not null then either non-admin can see or you are an admin. + listedDeepMeters.push(meterIdentifier); + } + }); + // Sort for display. + listedDeepMeters.sort(); + if (hasHidden) { + // There are hidden meters so note at bottom of list. + // This should never happen to an admin. + listedDeepMeters.push(translate('meter.hidden')); + } + return listedDeepMeters; + } +} + +/** + * Returns the set of meters ids associated with the groupId. Does full calculation where + * only uses the direct meter and group children. It uses a store passed to it so it can + * be changed without changing the Redux group store. Thus, it directly and recursively gets + * the deep meters of a group. + * @param groupId The groupId. + * @param groupState The group state to use in the calculation. + * @param times The number of times the function has been recursively called. Not passed on first call and only used internally. + * @returns Array of deep children ids of this group or empty array if none/circular dependency. + */ +function calculateMetersInGroup(groupId: number, groupState: any, times: number = 0): number[] { + // The number of times should be set to zero on the first call. Each time add one and assume + // if depth of calls is greater than value then there is a circular dependency and stop to report issue. + // This assumes no site will ever have a group chain of this length which seems safe. + if (++times > 50) { + return []; + } + // Group to get the deep meters for. + const groupToCheck = groupState[groupId] as GroupData; + // Use a set to avoid duplicates. + // The deep meters are the direct child meters of this group plus the direct child meters + // of all included meters, recursively. + // This should reproduce some DB functionality but using local state. + const deepMeters = new Set(groupToCheck.childMeters); + // Loop over all included groups to get its meters. + groupToCheck.childGroups.some(group => { + // Get the deep meters of this group. + const meters = calculateMetersInGroup(group, groupState, times); + if (meters.length === 0) { + // Issue found so stop loop and return empty set. There must be meters if all is okay. + // Clear deep meters so calling function knows there is an issue. + deepMeters.clear(); + // Stops the processing. + return true; + } else { + // Add to set of deep meters for the group checking. + meters.forEach(meter => { deepMeters.add(meter); }); + // Continue loop to process more. + return false; + } + }); + // Create an array of the deep meters of this group and return it. + // It will be empty if there are none. + return Array.from(deepMeters); +} diff --git a/src/client/app/components/groups/GroupViewComponent.tsx b/src/client/app/components/groups/GroupViewComponent.tsx index 8f0a069dd..1346ac27a 100644 --- a/src/client/app/components/groups/GroupViewComponent.tsx +++ b/src/client/app/components/groups/GroupViewComponent.tsx @@ -4,21 +4,21 @@ import * as React from 'react'; // Realize that * is already imported from react -import { State } from 'types/redux/state'; import { useState } from 'react'; +import { FormattedMessage } from 'react-intl'; import { useSelector } from 'react-redux'; import { Button } from 'reactstrap'; -import { FormattedMessage } from 'react-intl'; -import EditGroupModalComponent from './EditGroupModalComponent'; +import { GroupData } from 'types/redux/groups'; +import { State } from 'types/redux/state'; import '../../styles/card-page.css'; -import { GroupDefinition } from 'types/redux/groups'; -import { isRoleAdmin } from '../../utils/hasPermissions'; -import translate from '../../utils/translate'; import { UnitData } from '../../types/redux/units'; +import { isRoleAdmin } from '../../utils/hasPermissions'; import { noUnitTranslated } from '../../utils/input'; +import translate from '../../utils/translate'; +import EditGroupModalComponent from './EditGroupModalComponent'; interface GroupViewComponentProps { - group: GroupDefinition; + group: GroupData; // This isn't used in this component but are passed to the edit component // This is done to avoid having to recalculate the possible units sets in each view component possibleGraphicUnits: Set; diff --git a/src/client/app/components/groups/GroupViewComponentWIP.tsx b/src/client/app/components/groups/GroupViewComponentWIP.tsx new file mode 100644 index 000000000..abec803d6 --- /dev/null +++ b/src/client/app/components/groups/GroupViewComponentWIP.tsx @@ -0,0 +1,88 @@ +/* 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'; +// Realize that * is already imported from react +import { useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Button } from 'reactstrap'; +import { GroupData } from 'types/redux/groups'; +import { selectUnitDataById } from '../../redux/api/unitsApi'; +import { useAppSelector } from '../../redux/hooks'; +import { selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; +import '../../styles/card-page.css'; +import { noUnitTranslated } from '../../utils/input'; +import translate from '../../utils/translate'; +import EditGroupModalComponentWIP from './EditGroupModalComponentWIP'; + +interface GroupViewComponentProps { + group: GroupData; +} + +/** + * Defines the group info card + * @param props variables passed in to define + * @returns Group info card element + */ +export default function GroupViewComponentWIP(props: GroupViewComponentProps) { + // Don't check if admin since only an admin is allowed to route to this page. + + + // Edit Modal Show + const [showEditModal, setShowEditModal] = useState(false); + + const handleShow = () => { + setShowEditModal(true); + } + + const handleClose = () => { + setShowEditModal(false); + } + + // Check for admin status + const loggedInAsAdmin = useAppSelector(selectIsLoggedInAsAdmin); + + // Set up to display the units associated with the group as the unit identifier. + // unit state + const { data: unitDataById = {} } = useAppSelector(selectUnitDataById); + + + return ( +
+ {/* Use identifier-container since similar and groups only have name */} +
+ {props.group.name} +
+
+ {/* Use meter translation id string since same one wanted. */} + + {/* This is the default graphic unit associated with the group or no unit if none. */} + {props.group.defaultGraphicUnit === -99 ? ' ' + noUnitTranslated().identifier : ' ' + unitDataById[props.group.defaultGraphicUnit].identifier} +
+ {loggedInAsAdmin && +
+ {translate(`TrueFalseType.${props.group.displayable.toString()}`)} +
+ } + {/* Only show first 30 characters so card does not get too big. Should limit to one line */} + {loggedInAsAdmin && +
+ {props.group.note?.slice(0, 29)} +
+ } +
+ + {/* Creates a child GroupModalEditComponent */} + +
+
+ ); +} diff --git a/src/client/app/components/groups/GroupsDetailComponent.tsx b/src/client/app/components/groups/GroupsDetailComponent.tsx index f03cbc35c..5c90c5ef3 100644 --- a/src/client/app/components/groups/GroupsDetailComponent.tsx +++ b/src/client/app/components/groups/GroupsDetailComponent.tsx @@ -7,15 +7,16 @@ import { FormattedMessage } from 'react-intl'; import HeaderComponent from '../../components/HeaderComponent'; import FooterContainer from '../../containers/FooterContainer'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; + import { useAppSelector } from '../../redux/hooks'; import { selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; import { selectVisibleMetersGroupsDataByID } from '../../redux/selectors/dataSelectors'; -import { GroupDefinition } from '../../types/redux/groups'; import { potentialGraphicUnits } from '../../utils/input'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateGroupModalComponent from './CreateGroupModalComponent'; import GroupViewComponent from './GroupViewComponent'; -import { selectUnitDataById } from '../../reducers/units'; +import { GroupData } from 'types/redux/groups'; +import { selectUnitDataById } from '../../redux/api/unitsApi'; /** * Defines the groups page card view @@ -30,7 +31,8 @@ export default function GroupsDetailComponent() { const { visibleGroups } = useAppSelector(state => selectVisibleMetersGroupsDataByID(state)); // Units state - const unitDataById = useAppSelector(state => selectUnitDataById(state)); + const { data: unitDataById = {} } = useAppSelector(selectUnitDataById); + // Possible graphic units to use const possibleGraphicUnits = potentialGraphicUnits(unitDataById); @@ -71,11 +73,11 @@ export default function GroupsDetailComponent() {
{/* Create a GroupViewComponent for each groupData in Groups State after sorting by name */} {Object.values(visibleGroups) - .sort((groupA: GroupDefinition, groupB: GroupDefinition) => (groupA.name.toLowerCase() > groupB.name.toLowerCase()) ? 1 : + .sort((groupA: GroupData, groupB: GroupData) => (groupA.name.toLowerCase() > groupB.name.toLowerCase()) ? 1 : ((groupB.name.toLowerCase() > groupA.name.toLowerCase()) ? -1 : 0)) .map(groupData => ())} diff --git a/src/client/app/components/groups/GroupsDetailComponentWIP.tsx b/src/client/app/components/groups/GroupsDetailComponentWIP.tsx new file mode 100644 index 000000000..b34b3089a --- /dev/null +++ b/src/client/app/components/groups/GroupsDetailComponentWIP.tsx @@ -0,0 +1,80 @@ +/* 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 { FormattedMessage } from 'react-intl'; +import HeaderComponent from '../../components/HeaderComponent'; +import FooterContainer from '../../containers/FooterContainer'; +import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import { useAppSelector } from '../../redux/hooks'; +import { selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; +import { selectVisibleMetersGroupsDataByID } from '../../redux/selectors/dataSelectors'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; +import CreateGroupModalComponentWIP from './CreateGroupModalComponentWIP'; +import GroupViewComponentWIP from './GroupViewComponentWIP'; + +/** + * Defines the groups page card view + * @returns Groups page element + */ +export default function GroupsDetailComponentWIP() { + + // Check for admin status + const isAdmin = useAppSelector(state => selectIsLoggedInAsAdmin(state)); + + // We only want displayable groups if non-admins because they still have non-displayable in state. + const { visibleGroups } = useAppSelector(state => selectVisibleMetersGroupsDataByID(state)); + + + + const titleStyle: React.CSSProperties = { + textAlign: 'center' + }; + + const tooltipStyle = { + display: 'inline-block', + fontSize: '50%', + // Switch help depending if admin or not. + tooltipGroupView: isAdmin ? 'help.admin.groupview' : 'help.groups.groupview' + }; + + return ( +
+
+ + + +
+

+ +
+ +
+

+ {isAdmin && +
+ {/* The actual button for create is inside this component. */} + < CreateGroupModalComponentWIP + /> +
+ } + { +
+ {/* Create a GroupViewComponent for each groupData in Groups State after sorting by name */} + {Object.values(visibleGroups) + .sort((groupA, groupB) => (groupA.name.toLowerCase() > groupB.name.toLowerCase()) ? 1 : + ((groupB.name.toLowerCase() > groupA.name.toLowerCase()) ? -1 : 0)) + .map(groupData => ())} +
+ } +
+ +
+
+ ); +} diff --git a/src/client/app/components/meters/CreateMeterModalComponent.tsx b/src/client/app/components/meters/CreateMeterModalComponent.tsx index 221474dcb..dd04ae3e3 100644 --- a/src/client/app/components/meters/CreateMeterModalComponent.tsx +++ b/src/client/app/components/meters/CreateMeterModalComponent.tsx @@ -2,29 +2,29 @@ * 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 { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; +import { useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import translate from '../../utils/translate'; import { useDispatch, useSelector } from 'react-redux'; -import { useState, useEffect } from 'react'; +import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; +import { Dispatch } from 'types/redux/actions'; import { State } from 'types/redux/state'; -import '../../styles/modal.css'; -import { MeterTimeSortType, MeterType } from '../../types/redux/meters'; import { addMeter } from '../../actions/meters'; -import TooltipMarkerComponent from '../TooltipMarkerComponent'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import '../../styles/modal.css'; +import { tooltipBaseStyle } from '../../styles/modalStyle'; +import { ConversionArray } from '../../types/conversionArray'; import { TrueFalseType } from '../../types/items'; -import TimeZoneSelect from '../TimeZoneSelect'; -import { GPSPoint, isValidGPSInput } from '../../utils/calibration'; +import { MeterTimeSortType, MeterType } from '../../types/redux/meters'; import { UnitData } from '../../types/redux/units'; +import { GPSPoint, isValidGPSInput } from '../../utils/calibration'; import { unitsCompatibleWithUnit } from '../../utils/determineCompatibleUnits'; -import { ConversionArray } from '../../types/conversionArray'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; -import { notifyUser } from '../../utils/input' -import { tooltipBaseStyle } from '../../styles/modalStyle'; -import { Dispatch } from 'types/redux/actions'; -import * as moment from 'moment'; +import { notifyUser } from '../../utils/input'; +import translate from '../../utils/translate'; +import TimeZoneSelect from '../TimeZoneSelect'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; // TODO Moved the possible meters/graphic units calculations up to the details component diff --git a/src/client/app/components/meters/CreateMeterModalComponentWIP.tsx b/src/client/app/components/meters/CreateMeterModalComponentWIP.tsx new file mode 100644 index 000000000..2b4e1e6f6 --- /dev/null +++ b/src/client/app/components/meters/CreateMeterModalComponentWIP.tsx @@ -0,0 +1,849 @@ +/* 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 moment from 'moment'; +import * as React from 'react'; +import { useEffect, useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; +import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import { metersApi } from '../../redux/api/metersApi'; +import { useAppSelector } from '../../redux/hooks'; +import { makeSelectGraphicUnitCompatibility } from '../../redux/selectors/adminSelectors'; +import '../../styles/modal.css'; +import { tooltipBaseStyle } from '../../styles/modalStyle'; +import { TrueFalseType } from '../../types/items'; +import { MeterTimeSortType, MeterType } from '../../types/redux/meters'; +import { UnitData } from '../../types/redux/units'; +import { GPSPoint, isValidGPSInput } from '../../utils/calibration'; +import { AreaUnitType } from '../../utils/getAreaUnitConversion'; +import { notifyUser } from '../../utils/input'; +import translate from '../../utils/translate'; +import TimeZoneSelect from '../TimeZoneSelect'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; +import { showSuccessNotification } from '../../utils/notifications'; + + +// TODO Moved the possible meters/graphic units calculations up to the details component +// This was to prevent the calculations from being done on every load, but now requires them to be passed as props +export interface CreateMeterModalComponentProps { + possibleMeterUnits: Set; + possibleGraphicUnits: Set; +} + +/** + * Defines the create meter modal form + * @returns Meter create element + */ +export default function CreateMeterModalComponent() { + + const [addMeter] = metersApi.endpoints.addMeter.useMutation() + // Admin state so can get the default reading frequency. + const adminState = useAppSelector(state => state.admin) + // Memo'd memoized selector + const selectGraphicUnitCompatibility = React.useMemo(makeSelectGraphicUnitCompatibility, []) + + // TODO MAKE A SELECTOR? + const defaultValues = { + id: -99, + identifier: '', + name: '', + area: 0, + enabled: false, + displayable: false, + meterType: '', + url: '', + timeZone: '', + gps: '', + // Defaults of -999 (not to be confused with -99 which is no unit) + // Purely for allowing the default select to be "select a ..." + unitId: -99, + defaultGraphicUnit: -99, + note: '', + cumulative: false, + cumulativeReset: false, + cumulativeResetStart: '', + cumulativeResetEnd: '', + endOnlyTime: false, + readingGap: adminState.defaultMeterReadingGap, + readingVariation: 0, + readingDuplication: 1, + timeSort: MeterTimeSortType.increasing, + reading: 0.0, + startTimestamp: '', + endTimestamp: '', + previousEnd: '', + areaUnit: AreaUnitType.none, + readingFrequency: adminState.defaultMeterReadingFrequency, + minVal: adminState.defaultMeterMinimumValue, + maxVal: adminState.defaultMeterMaximumValue, + minDate: adminState.defaultMeterMinimumDate, + maxDate: adminState.defaultMeterMaximumDate, + maxError: adminState.defaultMeterMaximumErrors, + disableChecks: adminState.defaultMeterDisableChecks + } + + /* State */ + // To make this consistent with EditUnitModalComponent, we don't pass show and close via props + // even this one does have other props. + // Modal show + const [showModal, setShowModal] = useState(false); + + + // Handlers for each type of input change + const [meterDetails, setMeterDetails] = useState(defaultValues); + const { + incompatibleGraphicUnits, + compatibleGraphicUnits, + compatibleUnits, + incompatibleUnits + } = useAppSelector(state => selectGraphicUnitCompatibility(state, meterDetails.unitId, meterDetails.defaultGraphicUnit)) + const handleShow = () => setShowModal(true); + + const handleStringChange = (e: React.ChangeEvent) => { + setMeterDetails({ ...meterDetails, [e.target.name]: e.target.value }); + } + + const handleBooleanChange = (e: React.ChangeEvent) => { + setMeterDetails({ ...meterDetails, [e.target.name]: JSON.parse(e.target.value) }); + } + + const handleNumberChange = (e: React.ChangeEvent) => { + setMeterDetails({ ...meterDetails, [e.target.name]: Number(e.target.value) }); + } + + const handleTimeZoneChange = (timeZone: string) => { + setMeterDetails({ ...meterDetails, ['timeZone']: timeZone }); + } + + // Dropdowns + const [selectedUnitId, setSelectedUnitId] = useState(false) + const [selectedGraphicId, setSelectedGraphicId] = useState(false) + /* Create Meter Validation: + Name cannot be blank + Area must be positive or zero + If area is nonzero, area unit must be set + Reading Gap must be greater than zero + Reading Variation must be greater than zero + Reading Duplication must be between 1 and 9 + Reading frequency cannot be blank + Unit and Default Graphic Unit must be set (can be to no unit) + Meter type must be set + If displayable is true and unitId is set to -99, warn admin + Mininum Value cannot bigger than Maximum Value + Minimum Value and Maximum Value must be between valid input + Minimum Date and Maximum cannot be blank + Minimum Date cannot be after Maximum Date + Minimum Date and Maximum Value must be between valid input + Maximum No of Error must be between 0 and valid input + */ + const [validMeter, setValidMeter] = useState(false); + + useEffect(() => { + setValidMeter( + meterDetails.name !== '' && + (meterDetails.area === 0 || (meterDetails.area > 0 && meterDetails.areaUnit !== AreaUnitType.none)) && + meterDetails.readingGap >= 0 && + meterDetails.readingVariation >= 0 && + (meterDetails.readingDuplication >= 1 && meterDetails.readingDuplication <= 9) && + meterDetails.readingFrequency !== '' && + meterDetails.unitId !== -99 && + meterDetails.defaultGraphicUnit !== -99 && + meterDetails.meterType !== '' && + meterDetails.minVal >= MIN_VAL && + meterDetails.minVal <= meterDetails.maxVal && + meterDetails.maxVal <= MAX_VAL && + moment(meterDetails.minDate).isValid() && + moment(meterDetails.maxDate).isValid() && + moment(meterDetails.minDate).isSameOrAfter(MIN_DATE_MOMENT) && + moment(meterDetails.minDate).isSameOrBefore(moment(meterDetails.maxDate)) && + moment(meterDetails.maxDate).isSameOrBefore(MAX_DATE_MOMENT) && + (meterDetails.maxError >= 0 && meterDetails.maxError <= MAX_ERRORS) + ); + }, [ + meterDetails.area, + meterDetails.name, + meterDetails.readingGap, + meterDetails.readingVariation, + meterDetails.readingDuplication, + meterDetails.areaUnit, + meterDetails.readingFrequency, + meterDetails.unitId, + meterDetails.defaultGraphicUnit, + meterDetails.meterType, + meterDetails.minVal, + meterDetails.maxVal, + meterDetails.minDate, + meterDetails.maxDate, + meterDetails.maxError + ]); + /* End State */ + + // Reset the state to default values + // This would also benefit from a single state changing function for all state + const resetState = () => { + setMeterDetails(defaultValues); + setSelectedGraphicId(false) + setSelectedUnitId(false) + } + + const handleClose = () => { + setShowModal(false); + resetState(); + }; + + // Unlike edit, we decided to discard and inputs when you choose to leave the page. The reasoning is + // that create starts from an empty template. + + // Submit + const handleSubmit = async () => { + // Close modal first to avoid repeat clicks + setShowModal(false); + + // true if inputted values are okay. Then can submit. + let inputOk = true; + + // TODO Maybe should do as a single popup? + + // Set default identifier as name if left blank + meterDetails.identifier = (!meterDetails.identifier || meterDetails.identifier.length === 0) ? meterDetails.name : meterDetails.identifier; + + // Check GPS entered. + // Validate GPS is okay and take from string to GPSPoint to submit. + const gpsInput = meterDetails.gps; + let gps: GPSPoint | null = null; + const latitudeIndex = 0; + const longitudeIndex = 1; + // If the user input a value then gpsInput should be a string. + // null came from the DB and it is okay to just leave it - Not a string. + if (typeof gpsInput === 'string') { + if (isValidGPSInput(gpsInput)) { + // Clearly gpsInput is a string but TS complains about the split so cast. + const gpsValues = (gpsInput as string).split(',').map((value: string) => parseFloat(value)); + // It is valid and needs to be in this format for routing. + gps = { + longitude: gpsValues[longitudeIndex], + latitude: gpsValues[latitudeIndex] + }; + // gpsInput must be of type string but TS does not think so so cast. + } else if ((gpsInput as string).length !== 0) { + // GPS not okay. Only true if some input. + // TODO isValidGPSInput currently pops up an alert so not doing it here, may change + // so leaving code commented out. + // notifyUser(translate('input.gps.range') + state.gps + '.'); + inputOk = false; + } + } + + if (inputOk) { + // The input passed validation. + // The default value for timeZone is an empty string but that should be null for DB. + // See below for usage of timeZoneValue. + const timeZoneValue = (meterDetails.timeZone == '' ? null : meterDetails.timeZone); + // GPS may have been updated so create updated state to submit. + const submitState = { ...meterDetails, gps: gps, timeZone: timeZoneValue }; + // Submit new meter if checks where ok. + // Attempt to add meter to database + addMeter(submitState) + .unwrap() + .then(() => { + // if successful, the mutation will invalidate existing cache causing all meter details to be retrieved + showSuccessNotification(translate('meter.successfully.create.meter')); + resetState(); + }) + .catch(err => { + // TODO Better way than popup with React but want to stay so user can read/copy. + + window.alert(translate('meter.failed.to.create.meter') + '"' + err.response.data + '"'); + }) + } else { + // Tell user that not going to update due to input issues. + notifyUser(translate('meter.input.error')); + } + }; + + + const tooltipStyle = { + ...tooltipBaseStyle, + // Only an admin can create a meter. + tooltipCreateMeterView: 'help.admin.metercreate' + }; + + // This is a bit of a hack. The defaultValues set the time zone to the empty string. + // This makes the type a string and no easy way was found to allow null too. + // The DB stores null for no choice and TimeZoneSelect expects null for no choice. + // To get around this, a new variable is used for the menu options so it can have + // both values where the empty string is converted to null. + const timeZoneValue: string | null = (meterDetails.timeZone === '' ? null : meterDetails.timeZone); + + return ( + <> + {/* Show modal button */} + + + + + +
+ +
+
+ {/* when any of the Meter values are changed call one of the functions. */} + + + {/* Identifier input */} + + + handleStringChange(e)} + value={meterDetails.identifier} /> + + {/* Name input */} + + + handleStringChange(e)} + required value={meterDetails.name} + invalid={meterDetails.name === ''} /> + + + + + + + {/* meter unit input */} + + + { + handleNumberChange(e) + setSelectedUnitId(true) + }} + invalid={!selectedUnitId}> + {} + {Array.from(compatibleUnits).map(unit => { + return () + })} + {Array.from(incompatibleUnits).map(unit => { + return () + })} + + + + {/* default graphic unit input */} + + + { + handleNumberChange(e) + setSelectedGraphicId(true) + }} + > + {} + {Array.from(compatibleGraphicUnits).map(unit => { + return () + })} + {Array.from(incompatibleGraphicUnits).map(unit => { + return () + })} + + + + + + {/* Enabled input */} + + + handleBooleanChange(e)}> + {Object.keys(TrueFalseType).map(key => { + return () + })} + + + {/* Displayable input */} + + + handleBooleanChange(e)} + invalid={meterDetails.displayable && meterDetails.unitId === -99}> + {Object.keys(TrueFalseType).map(key => { + return () + })} + + + + + + + + {/* Meter type input */} + + + handleStringChange(e)} + invalid={meterDetails.meterType === ''}> + {/* The default value is a blank string so then tell user to select one. */} + {} + {/* The dB expects lowercase. */} + {Object.keys(MeterType).map(key => { + return () + })} + + + + {/* Meter reading frequency */} + + + handleStringChange(e)} + value={meterDetails.readingFrequency} + invalid={meterDetails.readingFrequency === ''} /> + + + + + + + {/* URL input */} + + + handleStringChange(e)} + value={meterDetails.url} /> + + {/* GPS input */} + + + handleStringChange(e)} + value={meterDetails.gps} /> + + + + {/* Area input */} + + + handleNumberChange(e)} + invalid={meterDetails.area < 0} /> + + + + + {/* meter area unit input */} + + + handleStringChange(e)} + invalid={meterDetails.area > 0 && meterDetails.areaUnit === AreaUnitType.none}> + {Object.keys(AreaUnitType).map(key => { + return () + })} + + + + + + + {/* note input */} + + + handleStringChange(e)} + value={meterDetails.note} + placeholder='Note' /> + + + {/* cumulative input */} + + + handleBooleanChange(e)}> + {Object.keys(TrueFalseType).map(key => { + return () + })} + + + {/* cumulativeReset input */} + + + handleBooleanChange(e)}> + {Object.keys(TrueFalseType).map(key => { + return () + })} + + + + + {/* cumulativeResetStart input */} + + + handleStringChange(e)} + value={meterDetails.cumulativeResetStart} + placeholder='HH:MM:SS' /> + + {/* cumulativeResetEnd input */} + + + handleStringChange(e)} + value={meterDetails.cumulativeResetEnd} + placeholder='HH:MM:SS' /> + + + + {/* endOnlyTime input */} + + + handleBooleanChange(e)}> + {Object.keys(TrueFalseType).map(key => { + return () + })} + + + {/* readingGap input */} + + + handleNumberChange(e)} + min='0' + defaultValue={meterDetails.readingGap} + invalid={meterDetails?.readingGap < 0} /> + + + + + + + {/* readingVariation input */} + + + handleNumberChange(e)} + min='0' + defaultValue={meterDetails.readingVariation} + invalid={meterDetails?.readingVariation < 0} /> + + + + + {/* readingDuplication input */} + + + handleNumberChange(e)} + step='1' + min='1' + max='9' + defaultValue={meterDetails.readingDuplication} + invalid={meterDetails?.readingDuplication < 1 || meterDetails?.readingDuplication > 9} /> + + + + + + + {/* timeSort input */} + + + handleStringChange(e)}> + {Object.keys(MeterTimeSortType).map(key => { + // This is a bit of a hack but it should work fine. The TypeSortTypes and MeterTimeSortType should be in sync. + // The translation is on the former so we use that enum name there but loop on the other to get the value desired. + return () + })} + + + {/* Timezone input */} + + + handleTimeZoneChange(timeZone)} /> + + + + {/* minVal input */} + + + handleNumberChange(e)} + min={MIN_VAL} + max={meterDetails.maxVal} + defaultValue={meterDetails.minVal} + invalid={meterDetails?.minVal < MIN_VAL || meterDetails?.minVal > meterDetails?.maxVal} /> + + + + + {/* maxVal input */} + + + handleNumberChange(e)} + min={meterDetails.minVal} + max={MAX_VAL} + defaultValue={meterDetails.maxVal} + invalid={meterDetails?.maxVal > MAX_VAL || meterDetails?.minVal > meterDetails?.maxVal} /> + + + + + + + {/* minDate input */} + + + handleStringChange(e)} + placeholder='YYYY-MM-DD HH:MM:SS' + required value={meterDetails.minDate} + invalid={!moment(meterDetails.minDate).isValid() + || !moment(meterDetails.minDate).isSameOrAfter(MIN_DATE_MOMENT) + || !moment(meterDetails.minDate).isSameOrBefore(moment(meterDetails.maxDate))} /> + + + + + {/* maxDate input */} + + + handleStringChange(e)} + required value={meterDetails.maxDate} + invalid={!moment(meterDetails.maxDate).isValid() + || !moment(meterDetails.maxDate).isSameOrBefore(MAX_DATE_MOMENT) + || !moment(meterDetails.maxDate).isSameOrAfter(moment(meterDetails.minDate))} /> + + + + + + + {/* DisableChecks input */} + {/* maxError input */} + + + handleNumberChange(e)} + min='0' + max={MAX_ERRORS} + defaultValue={meterDetails.maxError} + invalid={meterDetails?.maxError > MAX_ERRORS || meterDetails?.maxError < 0} /> + + + + + + + handleBooleanChange(e)}> + {Object.keys(TrueFalseType).map(key => { + return () + })} + + + + + {/* reading input */} + + + handleNumberChange(e)} + defaultValue={meterDetails.reading} /> + + {/* startTimestamp input */} + + + handleStringChange(e)} + placeholder='YYYY-MM-DD HH:MM:SS' + value={meterDetails.startTimestamp} /> + + + + {/* endTimestamp input */} + + + handleStringChange(e)} + placeholder='YYYY-MM-DD HH:MM:SS' + value={meterDetails.endTimestamp} /> + + {/* previousEnd input */} + + + handleStringChange(e)} + placeholder='YYYY-MM-DD HH:MM:SS' + value={meterDetails.previousEnd} /> + + + + + {/* Hides the modal */} + + {/* On click calls the function handleSaveChanges in this component */} + + +
+ + ); +} +const MIN_VAL = Number.MIN_SAFE_INTEGER; +const MAX_VAL = Number.MAX_SAFE_INTEGER; +const MIN_DATE_MOMENT = moment(0).utc(); +const MAX_DATE_MOMENT = moment(0).utc().add(5000, 'years'); +const MIN_DATE = MIN_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); +const MAX_DATE = MAX_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); +const MAX_ERRORS = 75; \ No newline at end of file diff --git a/src/client/app/components/meters/EditMeterModalComponentWIP.tsx b/src/client/app/components/meters/EditMeterModalComponentWIP.tsx new file mode 100644 index 000000000..2b5c86e8f --- /dev/null +++ b/src/client/app/components/meters/EditMeterModalComponentWIP.tsx @@ -0,0 +1,748 @@ +/* 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 _ from 'lodash'; +import * as moment from 'moment'; +import * as React from 'react'; +import { useEffect, useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; +import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; +import { metersApi } from '../../redux/api/metersApi'; +import { useAppDispatch, useAppSelector } from '../../redux/hooks'; +import { makeSelectGraphicUnitCompatibility, selectMeterDataWithID } from '../../redux/selectors/adminSelectors'; +import '../../styles/modal.css'; +import { tooltipBaseStyle } from '../../styles/modalStyle'; +import { TrueFalseType } from '../../types/items'; +import { MeterData, MeterTimeSortType, MeterType } from '../../types/redux/meters'; +import { UnitRepresentType } from '../../types/redux/units'; +import { GPSPoint, isValidGPSInput } from '../../utils/calibration'; +import { AreaUnitType } from '../../utils/getAreaUnitConversion'; +import { getGPSString, notifyUser, nullToEmptyString } from '../../utils/input'; +import translate from '../../utils/translate'; +import TimeZoneSelect from '../TimeZoneSelect'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; +import { selectUnitDataById } from '../../redux/api/unitsApi'; + +interface EditMeterModalComponentProps { + show: boolean; + meter: MeterData; + // passed in to handle closing the modal + handleClose: () => void; +} +/** + * Defines the edit meter modal form + * @param props for the edit component + * @returns Meter edit element + */ +export default function EditMeterModalComponent(props: EditMeterModalComponentProps) { + const dispatch = useAppDispatch(); + const [editMeter] = metersApi.useEditMeterMutation() + // since this selector is shared amongst many other modals, we must use a selector factory in order + // to have a single selector per modal instance. Memo ensures that this is a stable reference + const selectGraphicUnitCompatibility = React.useMemo(makeSelectGraphicUnitCompatibility, []) + // The current meter's state of meter being edited. It should always be valid. + const meterState = useAppSelector(state => selectMeterDataWithID(state, props.meter.id)); + const [localMeterEdits, setLocalMeterEdits] = useState(_.cloneDeep(meterState)); + const { + compatibleGraphicUnits, + incompatibleGraphicUnits, + compatibleUnits, + incompatibleUnits + } = useAppSelector(state => selectGraphicUnitCompatibility(state, localMeterEdits.unitId, localMeterEdits.defaultGraphicUnit)) + + useEffect(() => { setLocalMeterEdits(_.cloneDeep(meterState)) }, [meterState]) + /* State */ + // unit state + const { data: unitDataById = {} } = useAppSelector(selectUnitDataById); + + + const [validMeter, setValidMeter] = useState(isValidMeter(localMeterEdits)); + + useEffect(() => { setValidMeter(isValidMeter(localMeterEdits)) }, [localMeterEdits]); + /* End State */ + + + // Save changes + // Currently using the old functionality which is to compare inherited prop values to state values + // If there is a difference between props and state, then a change was made + // Side note, we could probably just set a boolean when any input but this would not detect if edited but no change made. + const handleSaveChanges = () => { + // Close the modal first to avoid repeat clicks + props.handleClose(); + + // true if inputted values are okay. Then can submit. + let inputOk = true; + + // Check for changes by comparing state to props + const meterHasChanges = !_.isEqual(meterState, localMeterEdits) + + // Only validate and store if any changes. + if (meterHasChanges) { + // Set default identifier as name if left blank + localMeterEdits.identifier = (!localMeterEdits.identifier || localMeterEdits.identifier.length === 0) ? + localMeterEdits.name : localMeterEdits.identifier; + + // Check GPS entered. + // Validate GPS is okay and take from string to GPSPoint to submit. + const gpsInput = localMeterEdits.gps; + let gps: GPSPoint | null = null; + const latitudeIndex = 0; + const longitudeIndex = 1; + // If the user input a value then gpsInput should be a string. + // null came from the DB and it is okay to just leave it - Not a string. + if (typeof gpsInput === 'string') { + if (isValidGPSInput(gpsInput)) { + // Clearly gpsInput is a string but TS complains about the split so cast. + const gpsValues = (gpsInput as string).split(',').map((value: string) => parseFloat(value)); + // It is valid and needs to be in this format for routing. + gps = { + longitude: gpsValues[longitudeIndex], + latitude: gpsValues[latitudeIndex] + }; + // gpsInput must be of type string but TS does not think so so cast. + } else if ((gpsInput as string).length !== 0) { + // GPS not okay. + // TODO isValidGPSInput currently tops up an alert so not doing it here, may change + // so leaving code commented out. + // notifyUser(translate('input.gps.range') + state.gps + '.'); + inputOk = false; + } + } + + if (inputOk) { + // The input passed validation. + // GPS may have been updated so create updated state to submit. + const submitState = { ...localMeterEdits, gps }; + // The reading views need to be refreshed if going to/from no unit or + // to/from type quantity. + // The check does it by first seeing if the unit changed and, if so, it + // sees if either were non unit meaning it crossed since both cannot be no unit + // or the unit change to/from quantity. + const shouldRefreshReadingViews = (props.meter.unitId != localMeterEdits.unitId) && + ((props.meter.unitId == -99 || localMeterEdits.unitId == -99) || + (unitDataById[props.meter.unitId].unitRepresent == UnitRepresentType.quantity + && unitDataById[localMeterEdits.unitId].unitRepresent != UnitRepresentType.quantity) || + (unitDataById[props.meter.unitId].unitRepresent != UnitRepresentType.quantity + && unitDataById[localMeterEdits.unitId].unitRepresent == UnitRepresentType.quantity)); + // Submit new meter if checks where ok. + // dispatch(submitEditedMeter(submitState, shouldRefreshReadingViews) as ThunkAction); + editMeter({ meterData: submitState, shouldRefreshViews: shouldRefreshReadingViews }) + dispatch(unsavedWarningSlice.actions.removeUnsavedChanges()); + } else { + // Tell user that not going to update due to input issues. + notifyUser(translate('meter.input.error')); + } + } + }; + + const handleStringChange = (e: React.ChangeEvent) => { + setLocalMeterEdits({ ...localMeterEdits, [e.target.name]: e.target.value.trim() }); + } + + const handleBooleanChange = (e: React.ChangeEvent) => { + setLocalMeterEdits({ ...localMeterEdits, [e.target.name]: JSON.parse(e.target.value) }); + } + + const handleNumberChange = (e: React.ChangeEvent) => { + setLocalMeterEdits({ ...localMeterEdits, [e.target.name]: Number(e.target.value) }); + } + + const handleTimeZoneChange = (timeZone: string) => { + setLocalMeterEdits({ ...localMeterEdits, ['timeZone']: timeZone }); + } + // Reset the state to default values + // To be used for the discard changes button + // Different use case from CreateMeterModalComponent's resetState + // This allows us to reset our state to match the store in the event of an edit failure + // Failure to edit meters will not trigger a re-render, as no state has changed. Therefore, we must manually reset the values + const resetState = () => { + setLocalMeterEdits(meterState); + } + + const handleClose = () => { + props.handleClose(); + resetState(); + } + return ( + <> + + + + +
+ +
+
+ {/* when any of the Meter values are changed call one of the functions. */} + + + {/* Identifier input */} + + + handleStringChange(e)} + value={localMeterEdits.identifier} /> + + {/* Name input */} + + + handleStringChange(e)} + value={localMeterEdits.name} + invalid={localMeterEdits.name === ''} /> + + + + + + + {/* meter unit input */} + + + handleNumberChange(e)}> + {Array.from(compatibleUnits).map(unit => { + return () + })} + {Array.from(incompatibleUnits).map(unit => { + return () + })} + + + {/* default graphic unit input */} + + + handleNumberChange(e)}> + {Array.from(compatibleGraphicUnits).map(unit => { + return () + })} + {Array.from(incompatibleGraphicUnits).map(unit => { + return () + })} + + + + + {/* Enabled input */} + + + handleBooleanChange(e)}> + {Object.keys(TrueFalseType).map(key => { + return () + })} + + + {/* Displayable input */} + + + handleBooleanChange(e)} + invalid={localMeterEdits.displayable && localMeterEdits.unitId === -99}> + {Object.keys(TrueFalseType).map(key => { + return () + })} + + + + + + + + {/* Meter type input */} + + + handleStringChange(e)}> + {/* The dB expects lowercase. */} + {Object.keys(MeterType).map(key => { + return () + })} + + + {/* Meter reading frequency */} + + + handleStringChange(e)} + value={localMeterEdits.readingFrequency} + invalid={localMeterEdits.readingFrequency === ''} /> + + + + + + + {/* URL input */} + + + handleStringChange(e)} + value={nullToEmptyString(localMeterEdits.url)} /> + + {/* GPS input */} + + + handleStringChange(e)} + value={getGPSString(localMeterEdits.gps)} /> + + + + {/* Area input */} + + + handleNumberChange(e)} + invalid={localMeterEdits.area < 0} /> + + + + + {/* meter area unit input */} + + + handleStringChange(e)} + invalid={localMeterEdits.area > 0 && localMeterEdits.areaUnit === AreaUnitType.none}> + {Object.keys(AreaUnitType).map(key => { + return () + })} + + + + + + + {/* note input */} + + + handleStringChange(e)} + value={nullToEmptyString(localMeterEdits.note)} + placeholder='Note' /> + + + {/* cumulative input */} + + + handleBooleanChange(e)}> + {Object.keys(TrueFalseType).map(key => { + return () + })} + + + {/* cumulativeReset input */} + + + handleBooleanChange(e)}> + {Object.keys(TrueFalseType).map(key => { + return () + })} + + + + + {/* cumulativeResetStart input */} + + + handleStringChange(e)} + value={localMeterEdits.cumulativeResetStart} + placeholder='HH:MM:SS' /> + + {/* cumulativeResetEnd input */} + + + handleStringChange(e)} + value={localMeterEdits?.cumulativeResetEnd} + placeholder='HH:MM:SS' /> + + + + {/* endOnlyTime input */} + + + handleBooleanChange(e)}> + {Object.keys(TrueFalseType).map(key => { + return () + })} + + + {/* readingGap input */} + + + handleNumberChange(e)} + min='0' + defaultValue={localMeterEdits?.readingGap} + invalid={localMeterEdits?.readingGap < 0} /> + + + + + + + {/* readingVariation input */} + + + handleNumberChange(e)} + min='0' + defaultValue={localMeterEdits?.readingVariation} + invalid={localMeterEdits?.readingVariation < 0} /> + + + + + {/* readingDuplication input */} + + + handleNumberChange(e)} + step='1' + min='1' + max='9' + defaultValue={localMeterEdits?.readingDuplication} + invalid={localMeterEdits?.readingDuplication < 1 || localMeterEdits?.readingDuplication > 9} /> + + + + + + + {/* timeSort input */} + + + handleStringChange(e)}> + {Object.keys(MeterTimeSortType).map(key => { + // This is a bit of a hack but it should work fine. The TypeSortTypes and MeterTimeSortType should be in sync. + // The translation is on the former so we use that enum name there but loop on the other to get the value desired. + return () + })} + + + {/* Timezone input */} + + + handleTimeZoneChange(timeZone)} /> + + + + {/* minVal input */} + + + handleNumberChange(e)} + min={MIN_VAL} + max={localMeterEdits.maxVal} + required value={localMeterEdits.minVal} + invalid={localMeterEdits?.minVal < MIN_VAL || localMeterEdits?.minVal > localMeterEdits?.maxVal} /> + + + + + {/* maxVal input */} + + + handleNumberChange(e)} + min={localMeterEdits.minVal} + max={MAX_VAL} + required value={localMeterEdits.maxVal} + invalid={localMeterEdits?.maxVal > MAX_VAL || localMeterEdits?.minVal > localMeterEdits?.maxVal} /> + + + + + + + {/* minDate input */} + + + handleStringChange(e)} + placeholder='YYYY-MM-DD HH:MM:SS' + required value={localMeterEdits.minDate} + invalid={!moment(localMeterEdits.minDate).isValid() + || !moment(localMeterEdits.minDate).isSameOrAfter(MIN_DATE_MOMENT) + || !moment(localMeterEdits.minDate).isSameOrBefore(moment(localMeterEdits.maxDate))} /> + + + + + {/* maxDate input */} + + + handleStringChange(e)} + placeholder='YYYY-MM-DD HH:MM:SS' + required value={localMeterEdits.maxDate} + invalid={!moment(localMeterEdits.maxDate).isValid() + || !moment(localMeterEdits.maxDate).isSameOrBefore(MAX_DATE_MOMENT) + || !moment(localMeterEdits.maxDate).isSameOrAfter(moment(localMeterEdits.minDate))} /> + + + + + + + {/* maxError input */} + + + handleNumberChange(e)} + min='0' + max={MAX_ERRORS} + required value={localMeterEdits.maxError} + invalid={localMeterEdits?.maxError > MAX_ERRORS || localMeterEdits?.maxError < 0} /> + + + + + {/* DisableChecks input */} + + + handleBooleanChange(e)} + invalid={localMeterEdits?.disableChecks && localMeterEdits.unitId === -99}> + {Object.keys(TrueFalseType).map(key => { + return () + })} + + + + + {/* reading input */} + + + handleNumberChange(e)} + defaultValue={localMeterEdits?.reading} /> + + {/* startTimestamp input */} + + + handleStringChange(e)} + placeholder='YYYY-MM-DD HH:MM:SS' + value={localMeterEdits?.startTimestamp} /> + + + + {/* endTimestamp input */} + + + handleStringChange(e)} + placeholder='YYYY-MM-DD HH:MM:SS' + value={localMeterEdits?.endTimestamp} /> + + {/* previousEnd input */} + + + handleStringChange(e)} + placeholder='YYYY-MM-DD HH:MM:SS' + value={localMeterEdits?.previousEnd} /> + + + + + {/* Hides the modal */} + + {/* On click calls the function handleSaveChanges in this component */} + + +
+ + ); +} + + +const MIN_VAL = Number.MIN_SAFE_INTEGER; +const MAX_VAL = Number.MAX_SAFE_INTEGER; +const MIN_DATE_MOMENT = moment(0).utc(); +const MAX_DATE_MOMENT = moment(0).utc().add(5000, 'years'); +const MIN_DATE = MIN_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); +const MAX_DATE = MAX_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); +const MAX_ERRORS = 75; +const tooltipStyle = { + ...tooltipBaseStyle, + // Only and admin can edit a meter. + tooltipEditMeterView: 'help.admin.meteredit' +}; + +const isValidMeter = (localMeterEdits: MeterData) => { + /* Edit Meter Validation: + Name cannot be blank + Area must be positive or zero + If area is nonzero, area unit must be set + Reading Gap must be greater than zero + Reading Variation must be greater than zero + Reading Duplication must be between 1 and 9 + Reading frequency cannot be blank + If displayable is true and unitId is set to -99, warn admin + Minimum Value cannot bigger than Maximum Value + Minimum Value and Maximum Value must be between valid input + Minimum Date and Maximum cannot be blank + Minimum Date cannot be after Maximum Date + Minimum Date and Maximum Value must be between valid input + Maximum No of Error must be between 0 and valid input + */ + return localMeterEdits.name !== '' && + (localMeterEdits.area === 0 || (localMeterEdits.area > 0 && localMeterEdits.areaUnit !== AreaUnitType.none)) && + localMeterEdits.readingGap >= 0 && + localMeterEdits.readingVariation >= 0 && + (localMeterEdits.readingDuplication >= 1 && localMeterEdits.readingDuplication <= 9) && + localMeterEdits.readingFrequency !== '' && + localMeterEdits.minVal >= MIN_VAL && + localMeterEdits.minVal <= localMeterEdits.maxVal && + localMeterEdits.maxVal <= MAX_VAL && + moment(localMeterEdits.minDate).isValid() && + moment(localMeterEdits.maxDate).isValid() && + moment(localMeterEdits.minDate).isSameOrAfter(MIN_DATE_MOMENT) && + moment(localMeterEdits.minDate).isSameOrBefore(moment(localMeterEdits.maxDate)) && + moment(localMeterEdits.maxDate).isSameOrBefore(MAX_DATE_MOMENT) && + (localMeterEdits.maxError >= 0 && localMeterEdits.maxError <= MAX_ERRORS) +} \ No newline at end of file diff --git a/src/client/app/components/meters/MeterViewComponentWIP.tsx b/src/client/app/components/meters/MeterViewComponentWIP.tsx new file mode 100644 index 000000000..0cbf56664 --- /dev/null +++ b/src/client/app/components/meters/MeterViewComponentWIP.tsx @@ -0,0 +1,92 @@ +/* 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'; +import { Button } from 'reactstrap'; +import { MeterData } from 'types/redux/meters'; +import { useAppSelector } from '../../redux/hooks'; +import { selectGraphicName, selectUnitName } from '../../redux/selectors/adminSelectors'; +import { selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; +import '../../styles/card-page.css'; +import translate from '../../utils/translate'; +import EditMeterModalComponentWIP from './EditMeterModalComponentWIP'; + +interface MeterViewComponentProps { + meter: MeterData; +} + +/** + * Defines the meter info card + * @param props component props + * @returns Meter info card element + */ +export default function MeterViewComponent(props: MeterViewComponentProps) { + // Edit Modal Show + const [showEditModal, setShowEditModal] = useState(false); + // Check for admin status + const loggedInAsAdmin = useAppSelector(selectIsLoggedInAsAdmin); + + + // Set up to display the units associated with the meter as the unit identifier. + // This is the unit associated with the meter. + const unitName = useAppSelector(state => selectUnitName(state, props.meter.id)) + // This is the default graphic unit associated with the meter. See above for how code works. + const graphicName = useAppSelector(state => selectGraphicName(state, props.meter.id)) + const handleShow = () => { + setShowEditModal(true); + } + const handleClose = () => { + setShowEditModal(false); + } + // Only display limited data if not an admin. + return ( +
+
+ {props.meter.identifier} +
+ {loggedInAsAdmin && +
+ {props.meter.name} +
+ } +
+ {unitName} +
+
+ {graphicName} +
+ {loggedInAsAdmin && +
+ {translate(`TrueFalseType.${props.meter.enabled.toString()}`)} +
+ } + {loggedInAsAdmin && +
+ {translate(`TrueFalseType.${props.meter.displayable.toString()}`)} +
+ } + {loggedInAsAdmin && +
+ {/* Only show first 30 characters so card does not get too big. Should limit to one line. Check in case null. */} + {props.meter.note?.slice(0, 29)} +
+ } + {loggedInAsAdmin && +
+ + {/* Creates a child MeterModalEditComponent */} + +
+ } +
+ ); +} diff --git a/src/client/app/components/meters/MetersDetailComponent.tsx b/src/client/app/components/meters/MetersDetailComponent.tsx index f86cfd3c5..fdaecb25b 100644 --- a/src/client/app/components/meters/MetersDetailComponent.tsx +++ b/src/client/app/components/meters/MetersDetailComponent.tsx @@ -18,8 +18,9 @@ import { noUnitTranslated, potentialGraphicUnits } from '../../utils/input'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateMeterModalComponent from './CreateMeterModalComponent'; import MeterViewComponent from './MeterViewComponent'; -import { selectUnitDataById } from '../../reducers/units'; + import { selectCurrentUser } from '../../reducers/currentUser'; +import { selectUnitDataById } from '../../redux/api/unitsApi'; /** * Defines the meters page card view @@ -37,7 +38,7 @@ export default function MetersDetailComponent() { const { visibleMeters } = useAppSelector(state => selectVisibleMetersGroupsDataByID(state)); // Units state - const unitDataById = useAppSelector(state => selectUnitDataById(state)); + const { data: unitDataById = {} } = useAppSelector(selectUnitDataById); // TODO? Convert into Selector? // Possible Meter Units to use diff --git a/src/client/app/components/meters/MetersDetailComponentWIP.tsx b/src/client/app/components/meters/MetersDetailComponentWIP.tsx new file mode 100644 index 000000000..b6c91f32d --- /dev/null +++ b/src/client/app/components/meters/MetersDetailComponentWIP.tsx @@ -0,0 +1,82 @@ +/* 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 { FormattedMessage } from 'react-intl'; +import HeaderComponent from '../../components/HeaderComponent'; +import FooterContainer from '../../containers/FooterContainer'; +import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import { metersApi } from '../../redux/api/metersApi'; +import { useAppSelector } from '../../redux/hooks'; +import { selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; +import { selectVisibleMetersGroupsDataByID } from '../../redux/selectors/dataSelectors'; +import '../../styles/card-page.css'; +import { MeterData } from '../../types/redux/meters'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; +import CreateMeterModalComponentWIP from './CreateMeterModalComponentWIP'; +import MeterViewComponentWIP from './MeterViewComponentWIP'; + +/** + * Defines the meters page card view + * @returns Meters page element + */ +export default function MetersDetailComponent() { + + // Check for admin status + const isAdmin = useAppSelector(state => selectIsLoggedInAsAdmin(state)); + // We only want displayable meters if non-admins because they still have + // non-displayable in state. + const { visibleMeters } = useAppSelector(selectVisibleMetersGroupsDataByID); + const { isFetching } = metersApi.useGetMetersQuery() + + return ( +
+ + + +
+

+ +
+ +
+

+ {isAdmin && +
+ +
+ } + { +
+ {/* Create a MeterViewComponent for each MeterData in Meters State after sorting by identifier */} + {!isFetching && Object.values(visibleMeters) + .sort((MeterA: MeterData, MeterB: MeterData) => (MeterA.identifier.toLowerCase() > MeterB.identifier.toLowerCase()) ? 1 : + ((MeterB.identifier.toLowerCase() > MeterA.identifier.toLowerCase()) ? -1 : 0)) + .map(MeterData => ( + + ))} +
+ } +
+ +
+ ); +} + +const titleStyle: React.CSSProperties = { + textAlign: 'center' +}; + + + +const tooltipStyle = { + display: 'inline-block', + fontSize: '50%' +}; + +// Switch help depending if admin or not. +const getToolTipMessage = (isAdmin: boolean) => isAdmin ? 'help.admin.meterview' : 'help.meters.meterview' \ No newline at end of file diff --git a/src/client/app/components/unit/EditUnitModalComponent.tsx b/src/client/app/components/unit/EditUnitModalComponent.tsx index f0c844998..4c84c223a 100644 --- a/src/client/app/components/unit/EditUnitModalComponent.tsx +++ b/src/client/app/components/unit/EditUnitModalComponent.tsx @@ -2,25 +2,25 @@ * 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 {store} from '../../store'; +import { store } from '../../store'; //Realize that * is already imported from react import { useEffect, useState } from 'react'; +import { FormattedMessage } from 'react-intl'; import { useDispatch } from 'react-redux'; import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; -import { FormattedMessage } from 'react-intl'; -import translate from '../../utils/translate'; -import TooltipMarkerComponent from '../TooltipMarkerComponent'; +import { Dispatch } from 'types/redux/actions'; +import { submitEditedUnit } from '../../actions/units'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; +import { selectMeterDataById } from '../../redux/api/metersApi'; +import { useAppSelector } from '../../redux/hooks'; import '../../styles/modal.css'; -import { submitEditedUnit } from '../../actions/units'; -import { UnitData, DisplayableType, UnitRepresentType, UnitType } from '../../types/redux/units'; -import { TrueFalseType } from '../../types/items'; -import { notifyUser } from '../../utils/input' import { tooltipBaseStyle } from '../../styles/modalStyle'; -import { Dispatch } from 'types/redux/actions'; -import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; -import { useSelector } from 'react-redux'; -import { State } from 'types/redux/state'; +import { TrueFalseType } from '../../types/items'; +import { DisplayableType, UnitData, UnitRepresentType, UnitType } from '../../types/redux/units'; +import { notifyUser } from '../../utils/input'; +import translate from '../../utils/translate'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; interface EditUnitModalComponentProps { show: boolean; @@ -55,7 +55,7 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp /* State */ // Handlers for each type of input change const [state, setState] = useState(values); - const globalConversionsState = useSelector((state: State) => state.conversions.conversions); + const globalConversionsState = useAppSelector(state => state.conversions.conversions); const handleStringChange = (e: React.ChangeEvent) => { setState({ ...state, [e.target.name]: e.target.value }); @@ -101,11 +101,12 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp const shouldUpdateUnit = (): boolean => { // true if inputted values are okay and there are changes. let inputOk = true; + const { data: meterDataByID = {} } = selectMeterDataById(store.getState()) // Check for case 1 if (props.unit.typeOfUnit === UnitType.meter && state.typeOfUnit !== UnitType.meter) { // Get an array of all meters - const meters = Object.values(store.getState().meters.byMeterID); + const meters = Object.values(meterDataByID); const meter = meters.find(m => m.unitId === props.unit.id); if (meter) { // There exists a meter that is still linked with this unit diff --git a/src/client/app/components/unit/UnitViewComponent.tsx b/src/client/app/components/unit/UnitViewComponent.tsx index 6753a0835..3d45340aa 100644 --- a/src/client/app/components/unit/UnitViewComponent.tsx +++ b/src/client/app/components/unit/UnitViewComponent.tsx @@ -11,6 +11,7 @@ import EditUnitModalComponent from './EditUnitModalComponent'; import '../../styles/card-page.css'; import { UnitData } from 'types/redux/units'; import translate from '../../utils/translate'; +import { LocaleDataKey } from 'translations/data'; interface UnitViewComponentProps { unit: UnitData; @@ -53,7 +54,7 @@ export default function UnitViewComponent(props: UnitViewComponentProps) { {props.unit.displayable}
- {translate(`TrueFalseType.${props.unit.preferredDisplay.toString()}`)} + {translate(`TrueFalseType.${props.unit.preferredDisplay.toString()}` as LocaleDataKey)}
{props.unit.secInRate} diff --git a/src/client/app/components/unit/UnitsDetailComponent.tsx b/src/client/app/components/unit/UnitsDetailComponent.tsx index b734f22f4..6256a0ffb 100644 --- a/src/client/app/components/unit/UnitsDetailComponent.tsx +++ b/src/client/app/components/unit/UnitsDetailComponent.tsx @@ -14,7 +14,7 @@ import { State } from '../../types/redux/state'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateUnitModalComponent from './CreateUnitModalComponent'; import UnitViewComponent from './UnitViewComponent'; -import { selectUnitDataById } from '../../reducers/units'; +import { selectUnitDataById } from '../../redux/api/unitsApi'; /** * Defines the units page card view @@ -25,7 +25,7 @@ export default function UnitsDetailComponent() { const isUpdatingCikAndDBViews = useSelector((state: State) => state.admin.isUpdatingCikAndDBViews); //Units state - const unitDataById = useAppSelector(state => selectUnitDataById(state)); + const { data: unitDataById = {} } = useAppSelector(selectUnitDataById); const titleStyle: React.CSSProperties = { diff --git a/src/client/app/reducers/admin.ts b/src/client/app/reducers/admin.ts index fbce0f0ce..67c5b87d7 100644 --- a/src/client/app/reducers/admin.ts +++ b/src/client/app/reducers/admin.ts @@ -131,5 +131,30 @@ export const adminSlice = createSlice({ state.defaultMeterDisableChecks = action.payload; state.submitted = false; } + }, + selectors: { + selectAdminState: state => state } -}); \ No newline at end of file +}); + +export const { + updateDisplayTitle, + updateDefaultChartToRender, + updateDefaultLanguage, + updateDefaultTimeZone, + updateDefaultWarningFileSize, + updateDefaultFileSizeLimit, + updateDefaultAreaUnit, + updateDefaultMeterReadingFrequency, + updateDefaultMeterMinimumValue, + updateDefaultMeterMaximumValue, + updateDefaultMeterMinimumDate, + updateDefaultMeterMaximumDate, + updateDefaultMeterReadingGap, + updateDefaultMeterMaximumErrors, + updateDefaultMeterDisableChecks +} = adminSlice.actions + +export const { + selectAdminState +} = adminSlice.selectors \ No newline at end of file diff --git a/src/client/app/reducers/groups.ts b/src/client/app/reducers/groups.ts index eff777277..d894d11fd 100644 --- a/src/client/app/reducers/groups.ts +++ b/src/client/app/reducers/groups.ts @@ -2,43 +2,25 @@ * 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 * as _ from 'lodash'; -import { GroupsState, DisplayMode } from '../types/redux/groups'; -import * as t from '../types/redux/groups'; -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { groupsApi } from '../redux/api/groupsApi'; +import * as t from '../types/redux/groups'; +import { GroupsState } from '../types/redux/groups'; const defaultState: GroupsState = { - hasBeenFetchedOnce: false, - // Has the child meters and groups of all groups already been put into state. - hasChildrenBeenFetchedOnce: false, - isFetching: false, - // Are we currently getting the child meters/groups for all groups. - isFetchingAllChildren: false, byGroupID: {}, - selectedGroups: [], + selectedGroups: [] // TODO groupInEditing: { // dirty: false // }, - displayMode: DisplayMode.View }; export const groupsSlice = createSlice({ name: 'groups', initialState: defaultState, reducers: { - confirmGroupsFetchedOnce: state => { - state.hasBeenFetchedOnce = true; - }, - confirmAllGroupsChildrenFetchedOnce: state => { - // Records if all group meter/group children have been fetched at least once. - // Normally just once but can reset to get it to fetch again. - state.hasChildrenBeenFetchedOnce = true; - }, - requestGroupsDetails: state => { - state.isFetching = true; - }, - receiveGroupsDetails: (state, action: PayloadAction) => { + receiveGroupsDetails: (state, action: PayloadAction) => { const newGroups = action.payload.map(group => ({ ...group, isFetching: false, @@ -48,32 +30,18 @@ export const groupsSlice = createSlice({ childGroups: (state.byGroupID[group.id] && state.byGroupID[group.id].childGroups) ? state.byGroupID[group.id].childGroups : [], childMeters: (state.byGroupID[group.id] && state.byGroupID[group.id].childMeters) ? state.byGroupID[group.id].childMeters : [], selectedGroups: [], - selectedMeters: [], - deepMeters: group.deepMeters ? group.deepMeters : [] + selectedMeters: [] })); // newGroups is an array: this converts it into a nested object where the key to each group is its ID. // Without this, byGroupID will not be keyed by group ID. - state.isFetching = false; // TODO FIX TYPES HERE Weird interaction here state.byGroupID = _.keyBy(newGroups, 'id'); }, - requestGroupChildren: (state, action: PayloadAction) => { - // Make no changes except setting isFetching = true for the group whose children we are fetching. - state.byGroupID[action.payload].isFetching = true; - }, receiveGroupChildren: (state, action: PayloadAction<{ groupID: number, data: { meters: number[], groups: number[], deepMeters: number[] } }>) => { - state.byGroupID[action.payload.groupID].isFetching = false; state.byGroupID[action.payload.groupID].childGroups = action.payload.data.groups; state.byGroupID[action.payload.groupID].childMeters = action.payload.data.meters; state.byGroupID[action.payload.groupID].deepMeters = action.payload.data.deepMeters; }, - requestAllGroupsChildren: state => { - state.isFetchingAllChildren = true; - // When the group children are forced to be re-fetched on creating a new group, we need to indicate - // here that the children are not yet gotten. This causes the group detail page to redraw when this - // is finished so the new group has the latest info. - state.hasChildrenBeenFetchedOnce = false; - }, receiveAllGroupsChildren: (state, action: PayloadAction) => { // For each group that received data, set the children meters and groups. for (const groupInfo of action.payload) { @@ -83,46 +51,13 @@ export const groupsSlice = createSlice({ state.byGroupID[groupId].childMeters = groupInfo.childMeters; state.byGroupID[groupId].childGroups = groupInfo.childGroups; } - // Note that not fetching children - state.isFetchingAllChildren = false - }, - changeDisplayedGroups: (state, action: PayloadAction) => { - state.selectedGroups = action.payload; - }, confirmEditedGroup: (state, action: PayloadAction) => { - // Return new state object with updated edited group info. - state.byGroupID[action.payload.id] = { - // There is state that is in each group that is not part of the edit information state. - ...state.byGroupID[action.payload.id], - ...action.payload - }; } }, // TODO Much of this logic is duplicated due to migration trying not to change too much at once. // When no longer needed remove base reducers if applicable, or delete slice entirely and rely solely on api cache extraReducers: builder => { builder.addMatcher(groupsApi.endpoints.getGroups.matchFulfilled, - (state, { payload }) => { - const newGroups = payload.map(group => ({ - ...group, - isFetching: false, - // Sometimes OED fetches both the details and the child meters/groups as separate actions. Since the order they will happen is - // uncertain, we need to preserve the child meters/groups if they exist. If not, put empty so no issues when accessing in other - // places. Note this may be the wrong values but they should refresh quickly once all actions are done. - childGroups: (state.byGroupID[group.id] && state.byGroupID[group.id].childGroups) ? state.byGroupID[group.id].childGroups : [], - childMeters: (state.byGroupID[group.id] && state.byGroupID[group.id].childMeters) ? state.byGroupID[group.id].childMeters : [], - selectedGroups: [], - selectedMeters: [], - - // TODO Verify this reducer. - // line added due to conflicting typing. TS Warns about potential undefined deepMeters - deepMeters: group.deepMeters ? group.deepMeters : [] - })); - // newGroups is an array: this converts it into a nested object where the key to each group is its ID. - // Without this, byGroupID will not be keyed by group ID. - state.isFetching = false; - // TODO FIX TYPES HERE Weird interaction here - state.byGroupID = _.keyBy(newGroups, 'id'); - }) + (state, { payload }) => { state.byGroupID = payload }) .addMatcher(groupsApi.endpoints.getAllGroupsChildren.matchFulfilled, (state, action) => { // For each group that received data, set the children meters and groups. @@ -137,9 +72,6 @@ export const groupsSlice = createSlice({ }, selectors: { - selectGroupState: state => state, - selectGroupDataByID: state => state.byGroupID + selectGroupState: state => state } }) - -export const { selectGroupDataByID, selectGroupState } = groupsSlice.selectors \ No newline at end of file diff --git a/src/client/app/reducers/index.ts b/src/client/app/reducers/index.ts index 8d83aa603..56de29b22 100644 --- a/src/client/app/reducers/index.ts +++ b/src/client/app/reducers/index.ts @@ -3,25 +3,25 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { combineReducers } from 'redux'; -import { metersSlice } from './meters'; import lineReadings from './lineReadings'; import barReadings from './barReadings'; import compareReadings from './compareReadings'; -import { groupsSlice } from './groups'; import maps from './maps'; import { adminSlice } from './admin'; import { versionSlice } from './version'; import { currentUserSlice } from './currentUser'; import { unsavedWarningSlice } from './unsavedWarning'; -import { unitsSlice } from './units'; import { conversionsSlice } from './conversions'; import { optionsSlice } from './options'; import { baseApi } from '../redux/api/baseApi'; import { graphSlice } from './graph'; +// removing these in favor of api reducers +// import { metersSlice } from './meters'; +// import { groupsSlice } from './groups'; +// import { unitsSlice } from './units'; export const rootReducer = combineReducers({ - meters: metersSlice.reducer, readings: combineReducers({ line: lineReadings, bar: barReadings, @@ -29,14 +29,15 @@ export const rootReducer = combineReducers({ }), graph: graphSlice.reducer, maps, - groups: groupsSlice.reducer, + // meters: metersSlice.reducer, + // groups: groupsSlice.reducer, admin: adminSlice.reducer, version: versionSlice.reducer, currentUser: currentUserSlice.reducer, unsavedWarning: unsavedWarningSlice.reducer, - units: unitsSlice.reducer, + // units: unitsSlice.reducer, conversions: conversionsSlice.reducer, options: optionsSlice.reducer, // RTK Query's Derived Reducers [baseApi.reducerPath]: baseApi.reducer -}); +}); \ No newline at end of file diff --git a/src/client/app/reducers/meters.ts b/src/client/app/reducers/meters.ts index a1548ba92..d5c44ad8a 100644 --- a/src/client/app/reducers/meters.ts +++ b/src/client/app/reducers/meters.ts @@ -1,18 +1,17 @@ /* 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 { createSlice, PayloadAction } from '@reduxjs/toolkit'; import * as _ from 'lodash'; +import { metersApi } from '../redux/api/metersApi'; +import * as t from '../types/redux/meters'; import { MetersState } from '../types/redux/meters'; import { durationFormat } from '../utils/durationFormat'; -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { metersApi } from '../redux/api/metersApi'; -import * as t from '../types/redux/meters' const defaultState: MetersState = { hasBeenFetchedOnce: false, isFetching: false, byMeterID: {}, - selectedMeters: [], submitting: [] }; @@ -31,9 +30,6 @@ export const metersSlice = createSlice({ state.isFetching = false; state.byMeterID = _.keyBy(action.payload, meter => meter.id); }, - changeDisplayedMeters: (state, action: PayloadAction) => { - state.selectedMeters = action.payload; - }, submitEditedMeter: (state, action: PayloadAction) => { state.submitting.push(action.payload); }, @@ -58,10 +54,7 @@ export const metersSlice = createSlice({ ) }, selectors: { - selectMeterState: state => state, - selectMeterDataByID: state => state.byMeterID + selectMeterState: state => state } }); - -export const { selectMeterState, selectMeterDataByID } = metersSlice.selectors \ No newline at end of file diff --git a/src/client/app/reducers/options.ts b/src/client/app/reducers/options.ts index 4527c336f..a49a3a47d 100644 --- a/src/client/app/reducers/options.ts +++ b/src/client/app/reducers/options.ts @@ -7,6 +7,7 @@ import { LanguageTypes } from '../types/redux/i18n'; import { OptionsState } from '../types/redux/options'; import { createSlice } from '@reduxjs/toolkit' import type { PayloadAction } from '@reduxjs/toolkit' +import * as moment from 'moment' const defaultState: OptionsState = { selectedLanguage: LanguageTypes.en @@ -23,6 +24,12 @@ export const optionsSlice = createSlice({ extraReducers: builder => { builder.addMatcher(preferencesApi.endpoints.getPreferences.matchFulfilled, (state, action) => { state.selectedLanguage = action.payload.defaultLanguage + moment.locale(action.payload.defaultLanguage); }) + }, + selectors: { + selectSelectedLanguage: state => state.selectedLanguage } }); + +export const { selectSelectedLanguage } = optionsSlice.selectors \ No newline at end of file diff --git a/src/client/app/reducers/units.ts b/src/client/app/reducers/units.ts index 22df566e1..4b90e4c25 100644 --- a/src/client/app/reducers/units.ts +++ b/src/client/app/reducers/units.ts @@ -44,7 +44,7 @@ export const unitsSlice = createSlice({ }, extraReducers: builder => { builder.addMatcher(unitsApi.endpoints.getUnitsDetails.matchFulfilled, - (state, action) => { state.units = _.keyBy(action.payload, unit => unit.id) } + (state, action) => { state.units = action.payload } ) }, selectors: { @@ -52,5 +52,3 @@ export const unitsSlice = createSlice({ selectUnitDataById: state => state.units } }); - -export const { selectUnitsState, selectUnitDataById } = unitsSlice.selectors \ No newline at end of file diff --git a/src/client/app/redux/api/conversionsApi.ts b/src/client/app/redux/api/conversionsApi.ts index f0a6a0ddc..23f7813f8 100644 --- a/src/client/app/redux/api/conversionsApi.ts +++ b/src/client/app/redux/api/conversionsApi.ts @@ -11,7 +11,20 @@ export const conversionsApi = baseApi.injectEndpoints({ url: 'api/conversions/addConversion', method: 'POST', body: { conversion } - }) + }), + onQueryStarted: async (arg, api) => { + await api.queryFulfilled. + then(() => { + { + api.dispatch( + conversionsApi.endpoints.refresh.initiate({ + redoCik: false, + refreshReadingViews: false + })) + } + }) + + } }), deleteConversion: builder.query({ query: conversion => ({ @@ -34,8 +47,12 @@ export const conversionsApi = baseApi.injectEndpoints({ query: args => ({ url: 'api/conversion-array/refresh', method: 'POST', - body: { redoCik: args.redoCik, refreshReadingViews: args.refreshReadingViews } + body: { redoCik: args.redoCik, refreshReadingViews: args.refreshReadingViews }, + responseHandler: 'text' }) }) }) -}) \ No newline at end of file +}) + +export const selectPIK = conversionsApi.endpoints.getConversionArray.select() +export const selectConversionsDetails = conversionsApi.endpoints.getConversionsDetails.select() \ No newline at end of file diff --git a/src/client/app/redux/api/groupsApi.ts b/src/client/app/redux/api/groupsApi.ts index 34ef9e955..0d0260c73 100644 --- a/src/client/app/redux/api/groupsApi.ts +++ b/src/client/app/redux/api/groupsApi.ts @@ -1,17 +1,100 @@ -import { baseApi } from './baseApi' -import { GroupChildren, GroupDetailsData } from '../../types/redux/groups' +import * as _ from 'lodash'; +import { GroupChildren, GroupData, GroupDataByID } from '../../types/redux/groups'; +import { baseApi } from './baseApi'; +import { selectIsLoggedInAsAdmin } from '../selectors/authSelectors'; +import { RootState } from '../../store'; +import { CompareReadings } from 'types/readings'; +import { TimeInterval } from '../../../../common/TimeInterval'; export const groupsApi = baseApi.injectEndpoints({ endpoints: builder => ({ - getGroups: builder.query({ + getGroups: builder.query({ query: () => 'api/groups', + transformResponse: (response: GroupData[]) => { + const groupsData = response.map(groupData => ({ + ...groupData, + // endpoint doesn't return these so define them here or else undefined may cause issues on admin pages + childMeters: [], + childGroups: [] + })) + return _.keyBy(groupsData, 'id') + }, + onQueryStarted: async (_, api) => { + try { + await api.queryFulfilled + const state = api.getState() as RootState + const isAdmin = selectIsLoggedInAsAdmin(state) + // if user is an admin, automatically fetch allGroupChildren and update the + if (isAdmin) { + const { data = [] } = await api.dispatch(groupsApi.endpoints.getAllGroupsChildren.initiate(undefined)) + api.dispatch(groupsApi.util.updateQueryData('getGroups', undefined, groupDataById => { + data.forEach(groupInfo => { + const groupId = groupInfo.groupId; + // Group id of the current item + // Reset the newState for this group to have child meters/groups. + groupDataById[groupId].childMeters = groupInfo.childMeters; + groupDataById[groupId].childGroups = groupInfo.childGroups; + }) + })) + } + } catch (e) { + console.log(e) + } + }, providesTags: ['GroupData'] }), getAllGroupsChildren: builder.query({ query: () => 'api/groups/allChildren', providesTags: ['GroupChildrenData'] - }) + }), + createGroup: builder.mutation({ + query: groupData => ({ + url: 'api/groups/create', + method: 'POST', + // omit the 'id' property of the groupData or api errors/fails + body: _.omit(groupData, 'id') + }), + invalidatesTags: ['GroupData', 'GroupChildrenData'] + }), + editGroup: builder.mutation>({ + query: group => ({ + url: 'api/groups/edit', + method: 'PUT', + body: group + }) + }), + deleteGroup: builder.mutation({ + query: groupId => ({ + url: 'api/groups/delete', + method: 'POST', + body: { id: groupId } + }), + invalidatesTags: ['GroupData', 'GroupChildrenData'] + }), + getParentIDs: builder.query({ + query: groupId => `api/groups/parents/${groupId}` + }), + /** + * Gets compare readings for groups for the given current time range and a shift for previous time range + * @param groupIDs The group IDs to get readings for + * @param timeInterval start and end of current/this compare period + * @param shift how far to shift back in time from current period to previous period + * @param unitID The unit id that the reading should be returned in, i.e., the graphic unit + * @returns CompareReadings in sorted order + */ + getCompareReadingsForGroups: + builder.query({ + query: ({ groupIDs, timeInterval, shift, unitID }) => ({ + url: `/api/compareReadings/groups/${groupIDs.join(',')}`, + params: { + curr_start: timeInterval.getStartTimestamp().toISOString(), + curr_end: timeInterval.getEndTimestamp().toISOString(), + shift: shift.toISOString(), + graphicUnitId: unitID.toString() + } + }) + }) }) }) -export const selectGroupInfo = groupsApi.endpoints.getGroups.select(); \ No newline at end of file +export const selectGroupDataById = groupsApi.endpoints.getGroups.select(); \ No newline at end of file diff --git a/src/client/app/redux/api/metersApi.ts b/src/client/app/redux/api/metersApi.ts index 1d47a593e..213bc2ec5 100644 --- a/src/client/app/redux/api/metersApi.ts +++ b/src/client/app/redux/api/metersApi.ts @@ -1,7 +1,11 @@ -import { baseApi } from './baseApi' import * as _ from 'lodash'; -import { MeterData, MeterDataByID } from '../../types/redux/meters' +import { TimeInterval } from '../../../../common/TimeInterval'; +import { MeterData, MeterDataByID } from '../../types/redux/meters'; import { durationFormat } from '../../utils/durationFormat'; +import { baseApi } from './baseApi'; +import { NamedIDItem } from 'types/items'; +import { CompareReadings, RawReadings } from 'types/readings'; +import { conversionsApi } from './conversionsApi'; export const metersApi = baseApi.injectEndpoints({ @@ -15,9 +19,60 @@ export const metersApi = baseApi.injectEndpoints({ }, // Tags used for invalidation by mutation requests. providesTags: ['MeterData'] + }), + editMeter: builder.mutation({ + query: ({ meterData }) => ({ + url: 'api/meters/edit', + method: 'POST', + body: { ...meterData } + }), + onQueryStarted: async ({ shouldRefreshViews }, api) => { + await api.queryFulfilled.then(() => { + // Update reading views if needed. Never redoCik so false. + api.dispatch(conversionsApi.endpoints.refresh.initiate({ redoCik: false, refreshReadingViews: shouldRefreshViews })) + }) + }, + invalidatesTags: ['MeterData'] + }), + addMeter: builder.mutation({ + query: meter => ({ + url: 'api/meters/addMeter', + method: 'POST', + body: { ...meter } + }), + + invalidatesTags: ['MeterData'] + }), + + lineReadingsCount: builder.query({ + query: ({ meterIDs, timeInterval }) => `api/readings/line/count/meters/${meterIDs.join(',')}?timeInterval=${timeInterval.toString()}` + }), + details: builder.query({ + query: () => 'api/meters' + }), + rawLineReadings: builder.query({ + query: ({ meterID, timeInterval }) => `api/readings/line/raw/meter/${meterID}?timeInterval=${timeInterval.toString()}` + }), + /** + * Gets compare readings for meters for the given current time range and a shift for previous time range + * @param meterIDs The meter IDs to get readings for + * @param timeInterval start and end of current/this compare period + * @param shift how far to shift back in time from current period to previous period + * @param unitID The unit id that the reading should be returned in, i.e., the graphic unit + * @returns CompareReadings in sorted order + */ + meterCompareReadings: builder.query({ + query: ({ meterIDs, timeInterval, shift, unitID }) => { + const stringifiedIDs = meterIDs.join(','); + const currStart = timeInterval.getStartTimestamp().toISOString(); + const currEnd = timeInterval.getEndTimestamp().toISOString(); + const apiURL = `/api/compareReadings/meters/${stringifiedIDs}?` + const params = `curr_start=${currStart}&curr_end=${currEnd}&shift=${shift.toISOString()}&graphicUnitId=${unitID.toString()}` + return `${apiURL}${params}` + } }) + }) }) -export const { useGetMetersQuery } = metersApi; -export const selectMeterInfo = metersApi.endpoints.getMeters.select() +export const selectMeterDataById = metersApi.endpoints.getMeters.select() diff --git a/src/client/app/redux/api/preferencesApi.ts b/src/client/app/redux/api/preferencesApi.ts index 88478876b..ca67f721e 100644 --- a/src/client/app/redux/api/preferencesApi.ts +++ b/src/client/app/redux/api/preferencesApi.ts @@ -5,7 +5,8 @@ import { baseApi } from './baseApi'; export const preferencesApi = baseApi.injectEndpoints({ endpoints: builder => ({ getPreferences: builder.query({ - query: () => 'api/preferences' + query: () => 'api/preferences', + providesTags: ['Preferences'] }), submitPreferences: builder.mutation({ query: preferences => ({ diff --git a/src/client/app/redux/api/unitsApi.ts b/src/client/app/redux/api/unitsApi.ts index 5e1774036..51aa537a3 100644 --- a/src/client/app/redux/api/unitsApi.ts +++ b/src/client/app/redux/api/unitsApi.ts @@ -1,16 +1,20 @@ -import { UnitData } from '../../types/redux/units'; +import * as _ from 'lodash' +import { UnitData, UnitDataById } from '../../types/redux/units'; import { baseApi } from './baseApi'; export const unitsApi = baseApi.injectEndpoints({ endpoints: builder => ({ - getUnitsDetails: builder.query({ - query: () => 'api/units' + getUnitsDetails: builder.query({ + query: () => 'api/units', + transformResponse: (response: UnitData[]) => { + return _.keyBy(response, unit => unit.id) + } }), addUnit: builder.mutation({ query: unitDataArgs => ({ url: 'api/units/addUnit', method: 'POST', - body: { unitDataArgs } + body: { ...unitDataArgs } }) }), editUnit: builder.mutation({ @@ -22,4 +26,6 @@ export const unitsApi = baseApi.injectEndpoints({ }) }) }) -}) \ No newline at end of file +}) + +export const selectUnitDataById = unitsApi.endpoints.getUnitsDetails.select() \ No newline at end of file diff --git a/src/client/app/redux/selectors/adminSelectors.ts b/src/client/app/redux/selectors/adminSelectors.ts new file mode 100644 index 000000000..57421b790 --- /dev/null +++ b/src/client/app/redux/selectors/adminSelectors.ts @@ -0,0 +1,283 @@ +import { createSelector } from '@reduxjs/toolkit' +import * as _ from 'lodash' +import { selectAdminState } from '../../reducers/admin' +import { selectConversionsDetails } from '../../redux/api/conversionsApi' +import { selectMeterDataById } from '../../redux/api/metersApi' +import { RootState } from '../../store' +import { PreferenceRequestItem } from '../../types/items' +import { UnitData, UnitType } from '../../types/redux/units' +import { unitsCompatibleWithUnit } from '../../utils/determineCompatibleUnits' +import { noUnitTranslated, potentialGraphicUnits } from '../../utils/input' +import { selectUnitDataById } from '../api/unitsApi' + +export const selectAdminPreferences = createSelector( + selectAdminState, + adminState => ({ + displayTitle: adminState.displayTitle, + defaultChartToRender: adminState.defaultChartToRender, + defaultBarStacking: adminState.defaultBarStacking, + defaultLanguage: adminState.defaultLanguage, + defaultTimezone: adminState.defaultTimeZone, + defaultWarningFileSize: adminState.defaultWarningFileSize, + defaultFileSizeLimit: adminState.defaultFileSizeLimit, + defaultAreaNormalization: adminState.defaultAreaNormalization, + defaultAreaUnit: adminState.defaultAreaUnit, + defaultMeterReadingFrequency: adminState.defaultMeterReadingFrequency, + defaultMeterMinimumValue: adminState.defaultMeterMinimumValue, + defaultMeterMaximumValue: adminState.defaultMeterMaximumValue, + defaultMeterMinimumDate: adminState.defaultMeterMinimumDate, + defaultMeterMaximumDate: adminState.defaultMeterMaximumDate, + defaultMeterReadingGap: adminState.defaultMeterReadingGap, + defaultMeterMaximumErrors: adminState.defaultMeterMaximumErrors, + defaultMeterDisableChecks: adminState.defaultMeterDisableChecks + } as PreferenceRequestItem) +) + + +/** + * Calculates the set of all possible graphic units for a meter/group. + * This is any unit that is of type unit or suffix. + * @returns The set of all possible graphic units for a meter/group + */ +export const selectPossibleGraphicUnits = createSelector( + selectUnitDataById, + ({ data: unitDataById = {} }) => { + return potentialGraphicUnits(unitDataById) + } +) +/** + * Calculates the set of all possible meter units for a meter. + * This is any unit that is of type unit or suffix. + * @returns The set of all possible graphic units for a meter + */ +export const selectPossibleMeterUnits = createSelector( + selectUnitDataById, + ({ data: unitDataById = {} }) => { + let possibleMeterUnits = new Set(); + // The meter unit can be any unit of type meter. + Object.values(unitDataById).forEach(unit => { + if (unit.typeOfUnit == UnitType.meter) { + possibleMeterUnits.add(unit); + } + }); + // Put in alphabetical order. + possibleMeterUnits = new Set(_.sortBy(Array.from(possibleMeterUnits), unit => unit.identifier.toLowerCase(), 'asc')); + // The default graphic unit can also be no unit/-99 but that is not desired so put last in list. + return possibleMeterUnits.add(noUnitTranslated()); + } +) + + +export const selectMeterDataWithID = (state: RootState, meterID: number) => { + const { data: meterDataByID = {} } = selectMeterDataById(state) + return meterDataByID[meterID] +} +export const selectUnitWithID = (state: RootState, unitID: number) => { + const { data: unitDataById = {} } = selectMeterDataById(state) + return unitDataById[unitID] + +} + +/** + * Selector that returns a unit associated with a meter given an meterID + * @param {RootState} state redux global state + * @param {number} id redux global state + * @returns {string} Unit Name. + * @example + * useAppSelector(state => selectUnitName(state, 42)) + */ +export const selectUnitName = createSelector( + // This is the unit associated with the meter. + // The first test of length is because the state may not yet be set when loading. This should not be seen + // since the state should be set and the page redrawn so just use 'no unit'. + // The second test of -99 is for meters without units. + // ThisSelector takes an argument, due to one or more of the selectors accepts an argument (selectUnitWithID selectMeterDataWithID) + selectUnitDataById, + selectMeterDataWithID, + ({ data: unitDataById = {} }, meterData) => { + const unitName = (Object.keys(unitDataById).length === 0 || meterData.unitId === -99) ? + noUnitTranslated().identifier : unitDataById[meterData.defaultGraphicUnit].identifier + return unitName + } +) + + +/** + * Selector to retrieve the graphic name based on unit and meter data. + * @param {RootState} state - The global application state. + * @param {number} id - The ID used to look up unit and meter data. + * @returns {string} The identifier for the graphic name, or a default identifier + * @example + * useAppSelector(state => selectGraphicName(state, 42)) + */ +export const selectGraphicName = createSelector( + // This is the default graphic unit associated with the meter. See above for how code works. + // notice that this selector is written with inline selectors for demonstration purposes + selectUnitDataById, + selectMeterDataWithID, + ({ data: unitDataById = {} }, meterData) => { + const graphicName = (Object.keys(unitDataById).length === 0 || meterData.defaultGraphicUnit === -99) ? + noUnitTranslated().identifier : unitDataById[meterData.defaultGraphicUnit].identifier + return graphicName + } +) + + +/** + * Selects the graphic unit compatibility data based on the possible graphic and meter units and local edits. + * @returns - Memoized selector instance The compatible and incompatible graphic and meter units. + * @example + * const selectGraphicUnitCompatibility = useMemo(makeSelectGraphicUnitCompatibility, []) + * useAppSelector(state => selectGraphicUnitCompatibility(state, localMeterEdits.unitId, localMeterEdits.defaultGraphicUnit)) + */ +export const makeSelectGraphicUnitCompatibility = () => { + const selectGraphicUnitCompatibilityInstance = createSelector( + selectPossibleGraphicUnits, + selectPossibleMeterUnits, + // 3rd/4th callback used to pass in non-state value in this case the local edits. + // two separate call backs so their return values will pass a === equality check for memoized behavior + (state: RootState, unitId: number) => unitId, + (state: RootState, unitId: number, defaultGraphicUnit: number) => defaultGraphicUnit, + (possibleGraphicUnits, possibleMeterUnits, unitId, defaultGraphicUnit) => { + // Graphic units compatible with currently selected unit + const compatibleGraphicUnits = new Set(); + // Graphic units incompatible with currently selected unit + const incompatibleGraphicUnits = new Set(); + // If unit is not 'no unit' + if (unitId != -99) { + // Find all units compatible with the selected unit + const unitsCompatibleWithSelectedUnit = unitsCompatibleWithUnit(unitId); + possibleGraphicUnits.forEach(unit => { + // If current graphic unit exists in the set of compatible graphic units OR if the current graphic unit is 'no unit' + if (unitsCompatibleWithSelectedUnit.has(unit.id) || unit.id === -99) { + compatibleGraphicUnits.add(unit); + } else { + incompatibleGraphicUnits.add(unit); + } + }); + } else { + // No unit is selected + // OED does not allow a default graphic unit if there is no unit so it must be -99. + defaultGraphicUnit = -99; + possibleGraphicUnits.forEach(unit => { + // Only -99 is allowed. + if (unit.id === -99) { + compatibleGraphicUnits.add(unit); + } else { + incompatibleGraphicUnits.add(unit); + } + }); + } + + // Units compatible with currently selected graphic unit + let compatibleUnits = new Set(); + // Units incompatible with currently selected graphic unit + const incompatibleUnits = new Set(); + // If a default graphic unit is not 'no unit' + if (defaultGraphicUnit !== -99) { + // Find all units compatible with the selected graphic unit + possibleMeterUnits.forEach(unit => { + // Graphic units compatible with the current meter unit + const compatibleGraphicUnits = unitsCompatibleWithUnit(unit.id); + // If the currently selected default graphic unit exists in the set of graphic units compatible with the current meter unit + // Also add the 'no unit' unit + if (compatibleGraphicUnits.has(defaultGraphicUnit) || unit.id === -99) { + // add the current meter unit to the list of compatible units + compatibleUnits.add(unit.id === -99 ? noUnitTranslated() : unit); + } else { + // add the current meter unit to the list of incompatible units + incompatibleUnits.add(unit); + } + }); + } else { + // No default graphic unit is selected + // All units are compatible + compatibleUnits = new Set(possibleMeterUnits); + } + // return compatibility for current selected unit(s) + return { compatibleGraphicUnits, incompatibleGraphicUnits, compatibleUnits, incompatibleUnits } + } + ) + return selectGraphicUnitCompatibilityInstance +} + + +/** + * Checks if conversion is valid + * @param sourceId New conversion sourceId + * @param destinationId New conversion destinationId + * @param bidirectional New conversion bidirectional status + * @returns boolean representing if new conversion is valid or not + */ +export const selectIsValidConversion = createSelector( + selectUnitDataById, + selectConversionsDetails, + (_state: RootState, sourceId: number) => sourceId, + (_state: RootState, _sourceId: number, destinationId: number) => destinationId, + (_state: RootState, _sourceId: number, _destinationId: number, bidirectional: boolean) => bidirectional, + ({ data: unitDataById = {} }, { data: conversionData = [] }, sourceId, destinationId, bidirectional) => { + /* Create Conversion Validation: + Source equals destination: invalid conversion + Conversion exists: invalid conversion + Conversion does not exist: + Inverse exists: + Conversion is bidirectional: invalid conversion + Destination cannot be a meter + Cannot mix unit represent + TODO Some of these can go away when we make the menus dynamic. + */ + + // The destination cannot be a meter unit. + if (destinationId !== -999 && unitDataById[destinationId].typeOfUnit === UnitType.meter) { + // notifyUser(translate('conversion.create.destination.meter')); + return false; + } + + // Source or destination not set + if (sourceId === -999 || destinationId === -999) { + return false + } + + // Conversion already exists + if ((conversionData.findIndex(conversionData => (( + conversionData.sourceId === sourceId) && + conversionData.destinationId === destinationId))) !== -1) { + // notifyUser(translate('conversion.create.exists')); + return false; + } + + // You cannot have a conversion between units that differ in unit_represent. + // This means you cannot mix quantity, flow & raw. + if (unitDataById[sourceId].unitRepresent !== unitDataById[destinationId].unitRepresent) { + // notifyUser(translate('conversion.create.mixed.represent')); + return false; + } + + + let isValid = true; + // Loop over conversions and check for existence of inverse of conversion passed in + // If there exists an inverse that is bidirectional, then there is no point in making a conversion since it is essentially a duplicate. + // If there is a non bidirectional inverse, then it is a valid conversion + Object.values(conversionData).forEach(conversion => { + // Inverse exists + if ((conversion.sourceId === destinationId) && (conversion.destinationId === sourceId)) { + // Inverse is bidirectional + if (conversion.bidirectional) { + isValid = false; + } + // Inverse is not bidirectional + else { + // Do not allow for a bidirectional conversion with an inverse that is not bidirectional + if (bidirectional) { + // The new conversion is bidirectional + isValid = false; + } + } + } + }); + if (!isValid) { + // notifyUser(translate('conversion.create.exists.inverse')); + } + return isValid; + } +) \ No newline at end of file diff --git a/src/client/app/redux/selectors/dataSelectors.ts b/src/client/app/redux/selectors/dataSelectors.ts index f190d70bb..a50386427 100644 --- a/src/client/app/redux/selectors/dataSelectors.ts +++ b/src/client/app/redux/selectors/dataSelectors.ts @@ -1,9 +1,9 @@ import { createSelector } from '@reduxjs/toolkit'; import * as _ from 'lodash'; import { selectGraphState } from '../../reducers/graph'; -import { selectGroupDataByID } from '../../reducers/groups'; -import { selectMeterDataByID } from '../../reducers/meters'; -import { readingsApi } from '../../redux/api/readingsApi'; +import { selectGroupDataById } from '../api/groupsApi'; +import { selectMeterDataById } from '../api/metersApi'; +import { readingsApi } from '../api/readingsApi'; import { MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; import { selectIsLoggedInAsAdmin } from './authSelectors'; @@ -115,10 +115,10 @@ export const selectChartQueryArgs = createSelector( ) export const selectVisibleMetersGroupsDataByID = createSelector( - selectMeterDataByID, - selectGroupDataByID, + selectMeterDataById, + selectGroupDataById, selectIsLoggedInAsAdmin, - (meterDataByID, groupDataByID, isAdmin) => { + ({ data: meterDataByID = {} }, { data: groupDataByID = {} }, isAdmin) => { let visibleMeters; let visibleGroups; if (isAdmin) { diff --git a/src/client/app/redux/selectors/threeDSelectors.ts b/src/client/app/redux/selectors/threeDSelectors.ts index b4104c9e6..f46ef2b3e 100644 --- a/src/client/app/redux/selectors/threeDSelectors.ts +++ b/src/client/app/redux/selectors/threeDSelectors.ts @@ -1,12 +1,12 @@ import { createSelector } from '@reduxjs/toolkit'; +import { selectMeterDataById } from '../../redux/api/metersApi'; import { selectGraphUnitID, selectQueryTimeInterval, selectThreeDMeterOrGroup, selectThreeDMeterOrGroupID, selectThreeDReadingInterval } from '../../reducers/graph'; -import { selectGroupState } from '../../reducers/groups'; -import { selectMeterState } from '../../reducers/meters'; +import { selectGroupDataById } from '../../redux/api/groupsApi'; import { MeterOrGroup } from '../../types/redux/graph'; import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; @@ -15,20 +15,23 @@ import { ThreeDReadingApiArgs } from './dataSelectors'; // Memoized Selectors export const selectThreeDComponentInfo = createSelector( - [selectThreeDMeterOrGroupID, selectThreeDMeterOrGroup, selectMeterState, selectGroupState], - (id, meterOrGroup, meterData, groupData) => { + selectThreeDMeterOrGroupID, + selectThreeDMeterOrGroup, + selectMeterDataById, + selectGroupDataById, + (id, meterOrGroup, { data: meterDataById = {} }, { data: groupDataById = {} }) => { //Default Values let meterOrGroupName = 'Unselected Meter or Group' let isAreaCompatible = true; if (id) { // Get Meter or Group's info - if (meterOrGroup === MeterOrGroup.meters && meterData) { - const meterInfo = meterData.byMeterID[id] + if (meterOrGroup === MeterOrGroup.meters && meterDataById) { + const meterInfo = meterDataById[id] meterOrGroupName = meterInfo.identifier; isAreaCompatible = meterInfo.area !== 0 && meterInfo.areaUnit !== AreaUnitType.none; - } else if (meterOrGroup === MeterOrGroup.groups && groupData) { - const groupInfo = groupData.byGroupID[id]; + } else if (meterOrGroup === MeterOrGroup.groups && groupDataById) { + const groupInfo = groupDataById[id]; meterOrGroupName = groupInfo.name; isAreaCompatible = groupInfo.area !== 0 && groupInfo.areaUnit !== AreaUnitType.none; } diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts index 8313a3715..5603a54e4 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -4,14 +4,11 @@ import { createSelector } from '@reduxjs/toolkit'; import * as _ from 'lodash'; -import { instanceOfGroupsState, instanceOfMetersState, instanceOfUnitsState } from '../../components/ChartDataSelectComponent'; import { selectMapState } from '../../reducers/maps'; import { DataType } from '../../types/Datasources'; import { GroupedOption, SelectOption } from '../../types/items'; import { ChartTypes, MeterOrGroup } from '../../types/redux/graph'; -import { GroupsState } from '../../types/redux/groups'; -import { MetersState } from '../../types/redux/meters'; -import { DisplayableType, UnitRepresentType, UnitType, UnitsState } from '../../types/redux/units'; +import { DisplayableType, UnitRepresentType, UnitType } from '../../types/redux/units'; import { CartesianPoint, Dimensions, calculateScaleFromEndpoints, gpsToUserGrid, itemDisplayableOnMap, itemMapInfoOk, normalizeImageDimensions @@ -24,16 +21,17 @@ import { selectChartToRender, selectGraphAreaNormalization, selectGraphUnitID, selectQueryTimeInterval, selectSelectedGroups, selectSelectedMeters } from '../../reducers/graph'; -import { selectGroupState } from '../../reducers/groups'; -import { selectMeterState } from '../../reducers/meters'; -import { selectUnitsState } from '../../reducers/units'; + +import { selectGroupDataById } from '../../redux/api/groupsApi'; +import { selectMeterDataById } from '../api/metersApi'; +import { selectUnitDataById } from '../../redux/api/unitsApi'; export const selectVisibleMetersAndGroups = createSelector( - selectMeterState, - selectGroupState, + selectMeterDataById, + selectGroupDataById, selectCurrentUser, - (meterState, groupState, currentUser) => { + ({ data: meterDataByID = {} }, { data: groupDataById = {} }, currentUser) => { // Holds all meters visible to the user const visibleMeters = new Set(); const visibleGroups = new Set(); @@ -41,21 +39,21 @@ export const selectVisibleMetersAndGroups = createSelector( // Get all the meters that this user can see. if (currentUser.profile?.role === 'admin') { // Can see all meters - Object.values(meterState.byMeterID).forEach(meter => { + Object.values(meterDataByID).forEach(meter => { visibleMeters.add(meter.id); }); - Object.values(groupState.byGroupID).forEach(group => { + Object.values(groupDataById).forEach(group => { visibleGroups.add(group.id); }); } else { // Regular user or not logged in so only add displayable meters - Object.values(meterState.byMeterID).forEach(meter => { + Object.values(meterDataByID).forEach(meter => { if (meter.displayable) { visibleMeters.add(meter.id); } }); - Object.values(groupState.byGroupID).forEach(group => { + Object.values(groupDataById).forEach(group => { if (group.displayable) { visibleGroups.add(group.id); } @@ -67,10 +65,10 @@ export const selectVisibleMetersAndGroups = createSelector( export const selectCurrentUnitCompatibility = createSelector( selectVisibleMetersAndGroups, - selectMeterState, - selectGroupState, + selectMeterDataById, + selectGroupDataById, selectGraphUnitID, - (visible, meterState, groupState, graphUnitID) => { + (visible, { data: meterDataById = {} }, { data: groupDataById = {} }, graphUnitID) => { // meters and groups that can graph const compatibleMeters = new Set(); const compatibleGroups = new Set(); @@ -84,7 +82,7 @@ export const selectCurrentUnitCompatibility = createSelector( // In this case, every meter is valid (provided it has a default graphic unit) // If the meter/group has a default graphic unit set then it can graph, otherwise it cannot. visible.meters.forEach(meterId => { - const meterGraphingUnit = meterState.byMeterID[meterId].defaultGraphicUnit; + const meterGraphingUnit = meterDataById[meterId].defaultGraphicUnit; if (meterGraphingUnit === -99) { //Default graphic unit is not set incompatibleMeters.add(meterId); @@ -95,7 +93,7 @@ export const selectCurrentUnitCompatibility = createSelector( } }); visible.groups.forEach(groupId => { - const groupGraphingUnit = groupState.byGroupID[groupId].defaultGraphicUnit; + const groupGraphingUnit = groupDataById[groupId].defaultGraphicUnit; if (groupGraphingUnit === -99) { //Default graphic unit is not set incompatibleGroups.add(groupId); @@ -144,10 +142,10 @@ export const selectCurrentAreaCompatibility = createSelector( selectCurrentUnitCompatibility, selectGraphAreaNormalization, selectGraphUnitID, - selectMeterState, - selectGroupState, - selectUnitsState, - (currentUnitCompatibility, areaNormalization, unitID, meterState, groupState, unitState) => { + selectMeterDataById, + selectGroupDataById, + selectUnitDataById, + (currentUnitCompatibility, areaNormalization, unitID, { data: meterDataById = {} }, { data: groupDataById = {} }, { data: unitDataById = {} }) => { // Deep Copy previous selector's values, and update as needed based on current Area Normalization setting const compatibleMeters = new Set(currentUnitCompatibility.compatibleMeters); const compatibleGroups = new Set(currentUnitCompatibility.compatibleGroups); @@ -159,22 +157,22 @@ export const selectCurrentAreaCompatibility = createSelector( // only run this check if area normalization is on if (areaNormalization) { compatibleMeters.forEach(meterID => { - const meterGraphingUnit = meterState.byMeterID[meterID].defaultGraphicUnit; + const meterGraphingUnit = meterDataById[meterID].defaultGraphicUnit; // No unit is selected then no meter/group should be selected if area normalization is enabled and meter type is raw - if ((unitID === -99 && unitState.units[meterGraphingUnit] && unitState.units[meterGraphingUnit].unitRepresent === UnitRepresentType.raw) || + if ((unitID === -99 && unitDataById[meterGraphingUnit] && unitDataById[meterGraphingUnit].unitRepresent === UnitRepresentType.raw) || // do not allow meter to be selected if it has zero area or no area unit - meterState.byMeterID[meterID].area === 0 || meterState.byMeterID[meterID].areaUnit === AreaUnitType.none + meterDataById[meterID].area === 0 || meterDataById[meterID].areaUnit === AreaUnitType.none ) { incompatibleMeters.add(meterID); } }); compatibleGroups.forEach(groupID => { - const groupGraphingUnit = groupState.byGroupID[groupID].defaultGraphicUnit; + const groupGraphingUnit = groupDataById[groupID].defaultGraphicUnit; // No unit is selected then no meter/group should be selected if area normalization is enabled and meter type is raw - if ((unitID === -99 && unitState.units[groupGraphingUnit] && unitState.units[groupGraphingUnit].unitRepresent === UnitRepresentType.raw) || + if ((unitID === -99 && unitDataById[groupGraphingUnit] && unitDataById[groupGraphingUnit].unitRepresent === UnitRepresentType.raw) || // do not allow group to be selected if it has zero area or no area unit - groupState.byGroupID[groupID].area === 0 || groupState.byGroupID[groupID].areaUnit === AreaUnitType.none) { + groupDataById[groupID].area === 0 || groupDataById[groupID].areaUnit === AreaUnitType.none) { incompatibleGroups.add(groupID); } }); @@ -191,10 +189,10 @@ export const selectCurrentAreaCompatibility = createSelector( export const selectChartTypeCompatibility = createSelector( selectCurrentAreaCompatibility, selectChartToRender, - selectMeterState, - selectGroupState, + selectMeterDataById, + selectGroupDataById, selectMapState, - (areaCompat, chartToRender, meterState, groupState, mapState) => { + (areaCompat, chartToRender, { data: meterDataById = {} }, { data: groupDataById = {} }, mapState) => { // Deep Copy previous selector's values, and update as needed based on current ChartType(s) const compatibleMeters = new Set(Array.from(areaCompat.compatibleMeters)); const incompatibleMeters = new Set(Array.from(areaCompat.incompatibleMeters)); @@ -233,7 +231,7 @@ export const selectChartTypeCompatibility = createSelector( const opposite = mp.opposite; compatibleMeters.forEach(meterID => { // This meter's GPS value. - const gps = meterState.byMeterID[meterID].gps; + const gps = meterDataById[meterID].gps; if (origin !== undefined && opposite !== undefined && gps !== undefined && gps !== null) { // 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 @@ -253,7 +251,7 @@ export const selectChartTypeCompatibility = createSelector( // The below code follows the logic for meters shown above. See comments above for clarification on the below code. compatibleGroups.forEach(groupID => { - const gps = groupState.byGroupID[groupID].gps; + 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); @@ -278,11 +276,11 @@ export const selectChartTypeCompatibility = createSelector( export const selectMeterGroupSelectData = createSelector( selectChartTypeCompatibility, - selectMeterState, - selectGroupState, + selectMeterDataById, + selectGroupDataById, selectSelectedMeters, selectSelectedGroups, - (chartTypeCompatibility, meterState, groupState, selectedMeters, selectedGroups) => { + (chartTypeCompatibility, { data: meterDataById = {} }, { data: groupDataById = {} }, selectedMeters, selectedGroups) => { // Destructure Previous Selectors's values const { compatibleMeters, incompatibleMeters, compatibleGroups, incompatibleGroups } = chartTypeCompatibility; @@ -303,12 +301,12 @@ export const selectMeterGroupSelectData = createSelector( }); // The Multiselect's current selected value(s) - const selectedMeterOptions = getSelectOptionsByItem(compatibleSelectedMeters, incompatibleSelectedMeters, meterState) - const selectedGroupOptions = getSelectOptionsByItem(compatibleSelectedGroups, incompatibleSelectedGroups, groupState) + const selectedMeterOptions = getSelectOptionsByItem(compatibleSelectedMeters, incompatibleSelectedMeters, meterDataById, 'meter') + const selectedGroupOptions = getSelectOptionsByItem(compatibleSelectedGroups, incompatibleSelectedGroups, groupDataById, 'group') // List of options with metadata for react-select - const meterSelectOptions = getSelectOptionsByItem(compatibleMeters, incompatibleMeters, meterState) - const groupSelectOptions = getSelectOptionsByItem(compatibleGroups, incompatibleGroups, groupState) + const meterSelectOptions = getSelectOptionsByItem(compatibleMeters, incompatibleMeters, meterDataById, 'meter') + const groupSelectOptions = getSelectOptionsByItem(compatibleGroups, incompatibleGroups, groupDataById, 'group') // currently when selected values are found to be incompatible (by area for example) get removed from selected options. // in the near future they should instead remain selected but visually appear disabled. @@ -337,19 +335,19 @@ export const selectMeterGroupSelectData = createSelector( * @returns an array of UnitData */ export const selectVisibleUnitOrSuffixState = createSelector( - selectUnitsState, + selectUnitDataById, selectCurrentUser, - (unitState, currentUser) => { + ({ data: unitDataById }, currentUser) => { let visibleUnitsOrSuffixes; if (currentUser.profile?.role === 'admin') { // User is an admin, allow all units to be seen - visibleUnitsOrSuffixes = _.filter(unitState.units, o => { + visibleUnitsOrSuffixes = _.filter(unitDataById, o => { return (o.typeOfUnit == UnitType.unit || o.typeOfUnit == UnitType.suffix) && o.displayable != DisplayableType.none; }); } else { // User is not an admin, do not allow for admin units to be seen - visibleUnitsOrSuffixes = _.filter(unitState.units, o => { + visibleUnitsOrSuffixes = _.filter(unitDataById, o => { return (o.typeOfUnit == UnitType.unit || o.typeOfUnit == UnitType.suffix) && o.displayable == DisplayableType.all; }); } @@ -358,12 +356,12 @@ export const selectVisibleUnitOrSuffixState = createSelector( ) export const selectUnitSelectData = createSelector( - selectUnitsState, + selectMeterDataById, selectVisibleUnitOrSuffixState, selectSelectedMeters, selectSelectedGroups, selectGraphAreaNormalization, - (unitState, visibleUnitsOrSuffixes, selectedMeters, selectedGroups, areaNormalization) => { + ({ data: unitDataById = {} }, visibleUnitsOrSuffixes, selectedMeters, selectedGroups, areaNormalization) => { // Holds all units that are compatible with selected meters/groups const compatibleUnits = new Set(); // Holds all units that are not compatible with selected meters/groups @@ -413,7 +411,7 @@ export const selectUnitSelectData = createSelector( }); } // Ready to display unit. Put selectable ones before non-selectable ones. - const unitOptions = getSelectOptionsByItem(compatibleUnits, incompatibleUnits, unitState); + const unitOptions = getSelectOptionsByItem(compatibleUnits, incompatibleUnits, unitDataById, 'unit'); const unitsGroupedOptions: GroupedOption[] = [ { label: 'Units', @@ -433,10 +431,16 @@ export const selectUnitSelectData = createSelector( * Visibility is determined by which set the items are contained in. * @param compatibleItems - compatible items to make select options for * @param incompatibleItems - incompatible items to make select options for - * @param state - current redux state, must be one of UnitsState, MetersState, or GroupsState + * @param dataById - current redux state, must be one of UnitsState, MetersState, or GroupsState + * @param type - one of the current variables * @returns Two Lists: Compatible, and Incompatible selectOptions for use as grouped React-Select options */ -export function getSelectOptionsByItem(compatibleItems: Set, incompatibleItems: Set, state: UnitsState | MetersState | GroupsState) { +export function getSelectOptionsByItem( + compatibleItems: Set, + incompatibleItems: Set, + // using any due to ts errs + dataById: any, + type: 'meter' | 'group' | 'unit') { // TODO Refactor original // redefined here for testing. // Holds the label of the select item, set dynamically according to the type of item passed in @@ -457,20 +461,19 @@ export function getSelectOptionsByItem(compatibleItems: Set, incompatibl // If this is converted to a switch statement the instanceOf function needs to be called twice // Once for the initial state type check, again because the interpreter (for some reason) needs to be sure that the property exists in the object // If else statements do not suffer from this - if (instanceOfUnitsState(state)) { - label = state.units[itemId].identifier; + if (type === 'unit') { + label = dataById[itemId].identifier; } - else if (instanceOfMetersState(state)) { - label = state.byMeterID[itemId].identifier; + else if (type === 'meter') { + label = dataById[itemId].identifier; meterOrGroup = MeterOrGroup.meters - defaultGraphicUnit = state.byMeterID[itemId].defaultGraphicUnit; + defaultGraphicUnit = dataById[itemId].defaultGraphicUnit; } - else if (instanceOfGroupsState(state)) { - label = state.byGroupID[itemId].name; + else if (type === 'group') { + label = dataById[itemId].name; meterOrGroup = MeterOrGroup.groups - defaultGraphicUnit = state.byGroupID[itemId].defaultGraphicUnit; + defaultGraphicUnit = dataById[itemId].defaultGraphicUnit; } - else { label = ''; } // TODO This is a bit of a hack. When an admin logs in they may not have the new state so label is null. // This should clear once the state is loaded. label = label === null ? '' : label; @@ -489,21 +492,19 @@ export function getSelectOptionsByItem(compatibleItems: Set, incompatibl let label = ''; let meterOrGroup: MeterOrGroup | undefined; let defaultGraphicUnit: number | undefined; - if (instanceOfUnitsState(state)) { - label = state.units[itemId].identifier; + if (type === 'unit') { + label = dataById[itemId].identifier; } - else if (instanceOfMetersState(state)) { - label = state.byMeterID[itemId].identifier; + else if (type === 'meter') { + label = dataById[itemId].identifier; meterOrGroup = MeterOrGroup.meters - defaultGraphicUnit = state.byMeterID[itemId].defaultGraphicUnit; - + defaultGraphicUnit = dataById[itemId].defaultGraphicUnit; } - else if (instanceOfGroupsState(state)) { - label = state.byGroupID[itemId].name; + else if (type === 'group') { + label = dataById[itemId].name; meterOrGroup = MeterOrGroup.groups - defaultGraphicUnit = state.byGroupID[itemId].defaultGraphicUnit; + defaultGraphicUnit = dataById[itemId].defaultGraphicUnit; } - else { label = ''; } // TODO This is a bit of a hack. When an admin logs in they may not have the new state so label is null. // This should clear once the state is loaded. label = label === null ? '' : label; diff --git a/src/client/app/types/redux/groups.ts b/src/client/app/types/redux/groups.ts index 5070e98e0..344c4f73c 100644 --- a/src/client/app/types/redux/groups.ts +++ b/src/client/app/types/redux/groups.ts @@ -13,9 +13,11 @@ export interface GroupMetadata { } export interface GroupData { + id: number; name: string; childMeters: number[]; childGroups: number[]; + deepMeters: number[]; gps: GPSPoint | null; displayable: boolean; note?: string; @@ -25,45 +27,6 @@ export interface GroupData { areaUnit: AreaUnitType; } -export interface GroupEditData { - id: number, - name: string; - childMeters: number[]; - childGroups: number[]; - // This is optional since it is in Redux state and used during group editing but not sent in route. - deepMeters?: number[]; - gps: GPSPoint | null; - displayable: boolean; - note?: string; - // TODO with area? you get a TS error but without it lets null through (see web console). - area: number; - defaultGraphicUnit: number; - areaUnit: AreaUnitType; -} - -// TODO This is similar to GroupEditData but without the children. Should be able to -// fuse and clean up. -export interface GroupDetailsData { - id: number, - name: string; - // This is optional since it is in Redux state and used during group editing but not sent in route. - deepMeters?: number[]; - gps: GPSPoint | null; - displayable: boolean; - note?: string; - // TODO with area? you get a TS error but without it lets null through (see web console). - area: number; - defaultGraphicUnit: number; - areaUnit: AreaUnitType; -} - -export interface GroupID { - id: number; -} - -export interface GroupDeepMeters { - deepMeters: number[]; -} // TODO this duplicates two fields in ones above so decide if should somehow merge. export interface GroupChildren { @@ -75,25 +38,16 @@ export interface GroupChildren { childGroups: number[]; } -export type GroupDefinition = GroupData & GroupMetadata & GroupID & GroupDeepMeters; export interface StatefulEditable { dirty: boolean; submitted?: boolean; } export interface GroupDataByID { - [groupID: number]: GroupDefinition; + [groupID: number]: GroupData; } export interface GroupsState { - hasBeenFetchedOnce: boolean; - // If all groups child meters/groups are in state. - hasChildrenBeenFetchedOnce: boolean; - isFetching: boolean; - // If fetching all groups child meters/groups. - isFetchingAllChildren: boolean; byGroupID: GroupDataByID selectedGroups: number[]; - // TODO groupInEditing: GroupDefinition & StatefulEditable | StatefulEditable; - displayMode: DisplayMode; } diff --git a/src/client/app/types/redux/meters.ts b/src/client/app/types/redux/meters.ts index 33651b5bc..46b8ae93f 100644 --- a/src/client/app/types/redux/meters.ts +++ b/src/client/app/types/redux/meters.ts @@ -113,7 +113,6 @@ export interface MeterDataByID { export interface MetersState { hasBeenFetchedOnce: boolean; isFetching: boolean; - selectedMeters: number[]; submitting: number[]; byMeterID: MeterDataByID; } \ No newline at end of file diff --git a/src/client/app/utils/api/GroupsApi.ts b/src/client/app/utils/api/GroupsApi.ts index 033c3b733..111c51ed7 100644 --- a/src/client/app/utils/api/GroupsApi.ts +++ b/src/client/app/utils/api/GroupsApi.ts @@ -8,7 +8,7 @@ import ApiBackend from './ApiBackend'; import * as moment from 'moment'; import { CompareReadings } from '../../types/readings'; import { TimeInterval } from '../../../../common/TimeInterval'; -import { GroupChildren, GroupData, GroupDetailsData, GroupEditData } from '../../types/redux/groups'; +import { GroupChildren, GroupData } from '../../types/redux/groups'; export default class GroupsApi { private readonly backend: ApiBackend; @@ -17,8 +17,8 @@ export default class GroupsApi { this.backend = backend; } - public async details(): Promise { - return await this.backend.doGetRequest('/api/groups'); + public async details(): Promise { + return await this.backend.doGetRequest('/api/groups'); } public async children(groupID: number): Promise<{ meters: number[], groups: number[], deepMeters: number[] }> { @@ -34,7 +34,7 @@ export default class GroupsApi { return await this.backend.doPostRequest('api/groups/create', groupData); } - public async edit(group: GroupEditData): Promise { + public async edit(group: GroupData): Promise { return await this.backend.doPutRequest('api/groups/edit', group); } diff --git a/src/client/app/utils/determineCompatibleUnits.ts b/src/client/app/utils/determineCompatibleUnits.ts index 091d080aa..88abeef3f 100644 --- a/src/client/app/utils/determineCompatibleUnits.ts +++ b/src/client/app/utils/determineCompatibleUnits.ts @@ -2,16 +2,18 @@ * 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 {store} from '../store'; import * as _ from 'lodash'; -import { MeterData } from '../types/redux/meters'; -import { ConversionArray } from '../types/conversionArray'; -import { UnitData, UnitType } from '../types/redux/units'; -import { GroupDefinition, GroupEditData } from '../types/redux/groups'; +import React from 'react'; +import { selectPIK } from '../redux/api/conversionsApi'; +import { selectGroupDataById } from '../redux/api/groupsApi'; +import { selectMeterDataById } from '../redux/api/metersApi'; +import { selectUnitDataById } from '../redux/api/unitsApi'; +import { store } from '../store'; import { DataType } from '../types/Datasources'; -import { State } from '../types/redux/state'; import { SelectOption } from '../types/items'; -import React from 'react'; +import { GroupData } from '../types/redux/groups'; +import { UnitData, UnitType } from '../types/redux/units'; + /** * The intersect operation of two sets. @@ -29,7 +31,8 @@ export function setIntersect(setA: Set, setB: Set): Set * @returns Set of compatible unit ids. */ export function unitsCompatibleWithMeters(meters: Set): Set { - const state = store.getState(); + const { data: meterDataByID = {} } = selectMeterDataById(store.getState()) + // The first meter processed is different since intersection with empty set is empty. let first = true; // Holds current set of compatible units. @@ -37,7 +40,7 @@ export function unitsCompatibleWithMeters(meters: Set): Set { // Loops over all meters. meters.forEach(function (meterId: number) { // Gets the meter associated with the meterId. - const meter = _.get(state.meters.byMeterID, meterId) as MeterData; + const meter = _.get(meterDataByID, meterId); let meterUnits = new Set(); // If meter had no unit then nothing compatible with it. // This probably won't happen but be safe. Note once you have one of these then @@ -71,9 +74,8 @@ export function unitsCompatibleWithUnit(unitId: number): Set { // If unit was null in the database then -99. This means there is no unit // so nothing is compatible with it. Skip processing and return empty set at end. // Do same if pik is not yet available. - if (unitId != -99 && ConversionArray.pikAvailable()) { - // The Pik array. - const pik = ConversionArray.pik; + const { data: pik } = selectPIK(store.getState()) + if (unitId != -99 && pik) { // Get the row index in Pik of this unit. const row = pRowFromUnit(unitId); // The compatible units are all columns with true for Pik where i = row. @@ -95,8 +97,9 @@ export function unitsCompatibleWithUnit(unitId: number): Set { * @returns The row index in Pik for given meter unit. */ export function pRowFromUnit(unitId: number): number { - const state = store.getState(); - const unit = _.find(state.units.units, function (o: UnitData) { + const { data: unitDataById = {} } = selectUnitDataById(store.getState()) + + const unit = _.find(unitDataById, function (o: UnitData) { // Since this is the row index, type of unit must be meter. return o.id == unitId && o.typeOfUnit == UnitType.meter; }) as UnitData; @@ -109,8 +112,9 @@ export function pRowFromUnit(unitId: number): number { * @returns The unit id given the row in Pik units. */ export function unitFromPRow(row: number): number { - const state = store.getState(); - const unit = _.find(state.units.units, function (o: UnitData) { + const { data: unitDataById = {} } = selectUnitDataById(store.getState()) + + const unit = _.find(unitDataById, function (o: UnitData) { // Since the given unitIndex is a row index, the unit type must be meter. return o.unitIndex == row && o.typeOfUnit == UnitType.meter; }) as UnitData; @@ -123,8 +127,9 @@ export function unitFromPRow(row: number): number { * @returns The unit id given the column in Pik. */ export function unitFromPColumn(column: number): number { - const state = store.getState(); - const unit = _.find(state.units.units, function (o: UnitData) { + const { data: unitDataById = {} } = selectUnitDataById(store.getState()) + + const unit = _.find(unitDataById, function (o: UnitData) { // Since the given unitIndex is a column index, the unit type must be different from meter. return o.unitIndex == column && o.typeOfUnit != UnitType.meter; }) as UnitData; @@ -141,7 +146,8 @@ export function metersInGroup(groupId: number): Set { const state = store.getState(); // Gets the group associated with groupId. // The deep children are automatically fetched with group state so should exist. - const group = _.get(state.groups.byGroupID, groupId) as GroupDefinition; + const { data: groupDataById = {} } = selectGroupDataById(state) + const group = _.get(groupDataById, groupId); // Create a set of the deep meters of this group and return it. return new Set(group.deepMeters); } @@ -152,14 +158,16 @@ export function metersInGroup(groupId: number): Set { * @param changedGroupState The state for the changed group * @returns array of deep meter ids of the changed group considering possible changes */ -export function metersInChangedGroup(changedGroupState: GroupEditData): number[] { +export function metersInChangedGroup(changedGroupState: GroupData): number[] { const state = store.getState(); + const { data: groupDataById = {} } = selectGroupDataById(state) + // deep meters starts with all the direct child meters of the group being changed. const deepMeters = new Set(changedGroupState.childMeters); // These groups cannot contain the group being changed so the redux state is okay. changedGroupState.childGroups.forEach((group: number) => { // The group state for the current child group. - const groupState = _.get(state.groups.byGroupID, group) as GroupDefinition; + const groupState = _.get(groupDataById, group); // The group state might not be defined, e.g., a group delete happened and the state is refreshing. // In this case the deepMeters returned will be off but they should quickly refresh. if (groupState) { @@ -182,19 +190,19 @@ export function metersInChangedGroup(changedGroupState: GroupEditData): number[] */ export function getMeterMenuOptionsForGroup(defaultGraphicUnit: number, deepMeters: number[] = []): SelectOption[] { // deepMeters has a default value since it is optional for the type of state but it should always be set in the code. - const state = store.getState() as State; + const state = store.getState(); // Get the currentGroup's compatible units. We need to use the current deep meters to get it right. // First must get a set from the array of meter numbers. const deepMetersSet = new Set(deepMeters); // Get the units that are compatible with this set of meters. const currentUnits = unitsCompatibleWithMeters(deepMetersSet); // Get all meters' state. - const meters = Object.values(state.meters.byMeterID) as MeterData[]; + const { data: meters = {} } = selectMeterDataById(state) // Options for the meter menu. const options: SelectOption[] = []; // For each meter, decide its compatibility for the menu - meters.forEach((meter: MeterData) => { + Object.values(meters).forEach(meter => { const option = { label: meter.identifier, value: meter.id, @@ -214,7 +222,8 @@ export function getMeterMenuOptionsForGroup(defaultGraphicUnit: number, deepMete }); // We want the options sorted by meter identifier. - return _.sortBy(options, item => item.label.toLowerCase(), 'asc'); + // Had to make item.label? potentially undefined due to start up race conditions + return _.sortBy(options, item => item.label?.toLowerCase(), 'asc') } /** @@ -225,20 +234,18 @@ export function getMeterMenuOptionsForGroup(defaultGraphicUnit: number, deepMete * @returns The current group options for this group. */ export function getGroupMenuOptionsForGroup(groupId: number, defaultGraphicUnit: number, deepMeters: number[] = []): SelectOption[] { - // deepMeters has a default value since it is optional for the type of state but it should always be set in the code. - const state = store.getState() as State; // Get the currentGroup's compatible units. We need to use the current deep meters to get it right. // First must get a set from the array of meter numbers. const deepMetersSet = new Set(deepMeters); // Get the currentGroup's compatible units. const currentUnits = unitsCompatibleWithMeters(deepMetersSet); // Get all groups' state. - const groups = Object.values(state.groups.byGroupID) as GroupDefinition[]; + const { data: groups = {} } = selectGroupDataById(store.getState()); // Options for the group menu. const options: SelectOption[] = []; - groups.forEach((group: GroupDefinition) => { + Object.values(groups).forEach(group => { // You cannot have yourself in the group so not an option. if (group.id !== groupId) { const option = { @@ -260,7 +267,8 @@ export function getGroupMenuOptionsForGroup(groupId: number, defaultGraphicUnit: }); // We want the options sorted by group name. - return _.sortBy(options, item => item.label.toLowerCase(), 'asc'); + // Had to make item.label? potentially undefined due to start up race conditions + return _.sortBy(options, item => item.label?.toLowerCase(), 'asc') } /** @@ -304,8 +312,9 @@ export function getCompatibilityChangeCase(currentUnits: Set, idToAdd: n function getCompatibleUnits(id: number, type: DataType, deepMeters: number[]): Set { if (type == DataType.Meter) { const state = store.getState(); + const { data: meterDataByID = {} } = selectMeterDataById(state) // Get the unit id of meter. - const unitId = state.meters.byMeterID[id].unitId; + const unitId = meterDataByID[id].unitId; // Returns all compatible units with this unit id. return unitsCompatibleWithUnit(unitId); } else { diff --git a/src/client/app/utils/exportData.ts b/src/client/app/utils/exportData.ts index 808130bfd..3e0e405c7 100644 --- a/src/client/app/utils/exportData.ts +++ b/src/client/app/utils/exportData.ts @@ -1,6 +1,8 @@ /* 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/. */ +* 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 @typescript-eslint/ban-ts-comment */ +// @ts-nocheck import { LineReading, BarReading, RawReadings } from '../types/readings'; import * as moment from 'moment'; diff --git a/src/client/app/utils/notifications.ts b/src/client/app/utils/notifications.ts index 3d4a407e4..7f55ae8ff 100644 --- a/src/client/app/utils/notifications.ts +++ b/src/client/app/utils/notifications.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 { TranslatedString } from './translate'; import { ToastPosition, toast } from 'react-toastify'; /** @@ -11,7 +10,7 @@ import { ToastPosition, toast } from 'react-toastify'; * @param position screen position for notification where top, right is the default * @param autoDismiss milliseconds until notification goes away with default of 3 seconds */ -export function showSuccessNotification(message: TranslatedString, position: ToastPosition = toast.POSITION.TOP_RIGHT, autoDismiss = 3000) { +export function showSuccessNotification(message: string, position: ToastPosition = toast.POSITION.TOP_RIGHT, autoDismiss = 3000) { toast.success(message, { position: position, autoClose: autoDismiss, @@ -28,7 +27,7 @@ export function showSuccessNotification(message: TranslatedString, position: Toa * @param position screen position for notification where top, right is the default * @param autoDismiss milliseconds until notification goes away with default of 15 seconds */ -export function showErrorNotification(message: TranslatedString, position: ToastPosition = toast.POSITION.TOP_RIGHT, autoDismiss = 15000) { +export function showErrorNotification(message: string, position: ToastPosition = toast.POSITION.TOP_RIGHT, autoDismiss = 15000) { toast.error(message, { position: position, autoClose: autoDismiss, From bf0136e50d3e2067ce2f271cc6cd07bda7875f39 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Sun, 5 Nov 2023 21:56:05 +0000 Subject: [PATCH 034/131] RTK Beta Version Bump --- package-lock.json | 18 +- package.json | 2 +- src/client/app/components/AppLayout.tsx | 20 ++ .../app/components/DashboardComponent.tsx | 2 + .../app/components/DateRangeComponent.tsx | 4 + src/client/app/components/ExportComponent.tsx | 3 +- src/client/app/components/HeaderComponent.tsx | 8 +- src/client/app/components/HomeComponent.tsx | 4 - .../components/InitializationComponent.tsx | 80 ------ src/client/app/components/LoginComponent.tsx | 6 +- .../MeterAndGroupSelectComponent.tsx | 6 +- src/client/app/components/RouteComponent.tsx | 2 - .../app/components/RouteComponentWIP.tsx | 54 ++-- src/client/app/components/ThreeDComponent.tsx | 46 ++-- .../app/components/ThreeDPillComponent.tsx | 37 +-- src/client/app/components/TimeZoneSelect.tsx | 1 + .../app/components/admin/AdminComponent.tsx | 6 +- .../admin/UsersDetailComponentWIP.tsx | 5 - .../conversion/ConversionViewComponentWIP.tsx | 91 ++++++ .../conversion/ConversionsDetailComponent.tsx | 7 +- .../ConversionsDetailComponentWIP.tsx | 35 ++- .../CreateConversionModalComponentWIP.tsx | 136 +++------ .../EditConversionModalComponentWIP.tsx | 258 ++++++++++++++++++ .../groups/GroupsDetailComponentWIP.tsx | 4 - .../maps/MapCalibrationComponent.tsx | 1 - .../components/maps/MapsDetailComponent.tsx | 14 +- .../meters/MetersDetailComponentWIP.tsx | 4 - .../unit/EditUnitModalComponent.tsx | 4 +- .../components/unit/UnitsDetailComponent.tsx | 52 ++-- .../containers/admin/CreateUserContainer.tsx | 6 +- src/client/app/index.tsx | 24 +- src/client/app/initScript.ts | 41 ++- src/client/app/redux/api/authApi.ts | 12 +- src/client/app/redux/api/baseApi.ts | 12 +- src/client/app/redux/api/conversionsApi.ts | 86 ++++-- .../app/redux/selectors/adminSelectors.ts | 20 +- src/client/app/redux/selectors/uiSelectors.ts | 22 +- src/client/app/styles/DateRangeCustom.css | 12 + 38 files changed, 717 insertions(+), 428 deletions(-) create mode 100644 src/client/app/components/AppLayout.tsx delete mode 100644 src/client/app/components/InitializationComponent.tsx create mode 100644 src/client/app/components/conversion/ConversionViewComponentWIP.tsx create mode 100644 src/client/app/components/conversion/EditConversionModalComponentWIP.tsx create mode 100644 src/client/app/styles/DateRangeCustom.css diff --git a/package-lock.json b/package-lock.json index b586a4df7..5c8a00cc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MPL-2.0", "dependencies": { - "@reduxjs/toolkit": "~2.0.0-beta.3", + "@reduxjs/toolkit": "~2.0.0-beta.4", "@wojtekmaj/react-daterange-picker": "~5.2.0", "axios": "~0.24.0", "bcryptjs": "~2.4.3", @@ -2542,18 +2542,18 @@ } }, "node_modules/@reduxjs/toolkit": { - "version": "2.0.0-beta.3", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.0.0-beta.3.tgz", - "integrity": "sha512-JUINQUveScGU29Z2rCdfh6REVZDJksMn7/L5bGlhjQ442/Yq9JuMc3n5rjjnwcpSYnwgCireKC0dEpS1i+5Q6w==", + "version": "2.0.0-beta.4", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.0.0-beta.4.tgz", + "integrity": "sha512-Bh53FuSfh0eeXIBzoWkB6XdBt61Pxk3xHsUmzcz4aMr5l3Tu1YcPx0KxRJqKJUnTU/I9k92536cv7d4CGU/oOg==", "dependencies": { "immer": "^10.0.2", "redux": "^5.0.0-beta.0", "redux-thunk": "^3.0.0-beta.0", - "reselect": "^5.0.0-alpha.2" + "reselect": "^5.0.0-beta.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18", - "react-redux": "^7.2.1 || ^8.0.2 || ^9.0.0-alpha.1" + "react-redux": "^7.2.1 || ^8.0.2 || ^9.0.0-beta.0" }, "peerDependenciesMeta": { "react": { @@ -10874,9 +10874,9 @@ "peer": true }, "node_modules/reselect": { - "version": "5.0.0-alpha.2", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.0.0-alpha.2.tgz", - "integrity": "sha512-wachIH1FWB/ceIgBP418PXtjJyhvgjtjqi0Go5nCqe/2xrwwAyCn1/4krfBurNfxxo7dWpiLGb1yYjCrWi40PA==" + "version": "5.0.0-beta.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.0.0-beta.0.tgz", + "integrity": "sha512-q9cwinGYNn3xNtyknjmNZQvH6FYdxS0tqUin1MIrtMLt1QB17FFz/C2WhlPgYBYuKJe9K/+bTKALuYXn+Elp1g==" }, "node_modules/resolve": { "version": "1.22.6", diff --git a/package.json b/package.json index 911f2a7fc..4c4ba55ef 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "babel-plugin-lodash": "~3.3.4" }, "dependencies": { - "@reduxjs/toolkit": "~2.0.0-beta.3", + "@reduxjs/toolkit": "~2.0.0-beta.4", "@wojtekmaj/react-daterange-picker": "~5.2.0", "axios": "~0.24.0", "bcryptjs": "~2.4.3", diff --git a/src/client/app/components/AppLayout.tsx b/src/client/app/components/AppLayout.tsx new file mode 100644 index 000000000..51fb62fa9 --- /dev/null +++ b/src/client/app/components/AppLayout.tsx @@ -0,0 +1,20 @@ +import * as React from 'react' +import { Outlet } from 'react-router-dom-v5-compat' +import FooterContainer from '../containers/FooterContainer' +import HeaderComponent from './HeaderComponent' +import { Slide, ToastContainer } from 'react-toastify' +import 'react-toastify/dist/ReactToastify.css'; +/** + * @returns The OED Application, with the current route as an outlet + */ +export default function AppLayout() { + return ( + <> + + + + + + + ) +} \ No newline at end of file diff --git a/src/client/app/components/DashboardComponent.tsx b/src/client/app/components/DashboardComponent.tsx index 749704bed..43bc3d8fd 100644 --- a/src/client/app/components/DashboardComponent.tsx +++ b/src/client/app/components/DashboardComponent.tsx @@ -25,6 +25,8 @@ export default function DashboardComponent() { const optionsClassName = optionsVisibility ? 'col-2 d-none d-lg-block' : 'd-none'; const chartClassName = optionsVisibility ? 'col-12 col-lg-10' : 'col-12'; + // const optionsClassName = optionsVisibility ? 'col-3 d-none d-lg-block' : 'd-none'; + // const chartClassName = optionsVisibility ? 'col-12 col-lg-9' : 'col-12'; return (
diff --git a/src/client/app/components/DateRangeComponent.tsx b/src/client/app/components/DateRangeComponent.tsx index 2cf96bfbc..b43595a45 100644 --- a/src/client/app/components/DateRangeComponent.tsx +++ b/src/client/app/components/DateRangeComponent.tsx @@ -14,6 +14,10 @@ import { Dispatch } from '../types/redux/actions'; import { dateRangeToTimeInterval, timeIntervalToDateRange } from '../utils/dateRangeCompatibility'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; + +// Potential Fixes, for now omitted +// import '../styles/DateRangeCustom.css' + /** * A component which allows users to select date ranges in lieu of a slider (line graphic) * @returns Date Range Calendar Picker diff --git a/src/client/app/components/ExportComponent.tsx b/src/client/app/components/ExportComponent.tsx index be9f9983a..c21048733 100644 --- a/src/client/app/components/ExportComponent.tsx +++ b/src/client/app/components/ExportComponent.tsx @@ -20,6 +20,7 @@ import { barUnitLabel, lineUnitLabel } from '../utils/graphics'; import { hasToken } from '../utils/token'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; +import { selectConversionsDetails } from '../redux/api/conversionsApi'; /** * Creates export buttons and does code for handling export to CSV files. @@ -33,7 +34,7 @@ export default function ExportComponent() { // Units state const { data: unitsDataById = {} } = useAppSelector(selectUnitDataById); // Conversion state - const conversionState = useAppSelector(state => state.conversions.conversions); + const { data: conversionState = [] } = useAppSelector(selectConversionsDetails); // graph state const graphState = useAppSelector(state => state.graph); // admin state diff --git a/src/client/app/components/HeaderComponent.tsx b/src/client/app/components/HeaderComponent.tsx index fb4665050..43989f40c 100644 --- a/src/client/app/components/HeaderComponent.tsx +++ b/src/client/app/components/HeaderComponent.tsx @@ -3,13 +3,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { Link } from 'react-router-dom-v5-compat'; -import LogoComponent from './LogoComponent'; -import MenuModalComponent from './MenuModalComponent'; -import HeaderButtonsComponent from './HeaderButtonsComponent'; import { useSelector } from 'react-redux'; +import { Link } from 'react-router-dom-v5-compat'; import { State } from '../types/redux/state'; import getPage from '../utils/getPage'; +import HeaderButtonsComponent from './HeaderButtonsComponent'; +import LogoComponent from './LogoComponent'; +import MenuModalComponent from './MenuModalComponent'; /** * React component that controls the header strip at the top of all pages diff --git a/src/client/app/components/HomeComponent.tsx b/src/client/app/components/HomeComponent.tsx index 76cda1055..4a2afd83b 100644 --- a/src/client/app/components/HomeComponent.tsx +++ b/src/client/app/components/HomeComponent.tsx @@ -3,9 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import FooterContainer from '../containers/FooterContainer'; import TooltipHelpContainer from '../containers/TooltipHelpContainer'; -import HeaderComponent from './HeaderComponent'; import DashboardComponent from './DashboardComponent'; /** @@ -16,10 +14,8 @@ export default function HomeComponent() { return (
- -
); } diff --git a/src/client/app/components/InitializationComponent.tsx b/src/client/app/components/InitializationComponent.tsx deleted file mode 100644 index 07b524c96..000000000 --- a/src/client/app/components/InitializationComponent.tsx +++ /dev/null @@ -1,80 +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 * as React from 'react'; -import { useEffect } from 'react'; -import { useDispatch } from 'react-redux'; -import { Slide, ToastContainer } from 'react-toastify'; -import 'react-toastify/dist/ReactToastify.css'; -import { Dispatch } from 'types/redux/actions'; -import { fetchMapsDetails } from '../actions/map'; -import { authApi } from '../redux/api/authApi'; -import { conversionsApi } from '../redux/api/conversionsApi'; -import { metersApi } from '../redux/api/metersApi'; -import { preferencesApi } from '../redux/api/preferencesApi'; -import { unitsApi } from '../redux/api/unitsApi'; -import { ConversionArray } from '../types/conversionArray'; -import { getToken, hasToken } from '../utils/token'; -import { groupsApi } from '../redux/api/groupsApi'; - -/** - * Initializes the app by fetching and subscribing to the store with various queries - * @returns Initialization JSX element - */ -export default function InitializationComponent() { - const dispatch: Dispatch = useDispatch(); - - // With RTKQuery, Mutations are used for POST, PUT, PATCH, etc. - // The useMutation() hooks returns a tuple containing triggerFunction that can be called to initiate the request - // and an optional results object containing derived data related the the executed query. - const [verifyTokenTrigger] = authApi.useVerifyTokenMutation() - - // useQueryHooks derived by api endpoint definitions fetch and cache data to the store as soon the component mounts. - // They maintain an active subscription to the store so long as the component remains mounted. - // Since this component lives up near the root of the DOM, these queries will remain subscribed indefinitely by default - preferencesApi.useGetPreferencesQuery(); - unitsApi.useGetUnitsDetailsQuery(); - conversionsApi.useGetConversionsDetailsQuery(); - conversionsApi.useGetConversionArrayQuery(); - metersApi.useGetMetersQuery(); - - // Use Query hooks return an object with various derived values related to the query's status which can be destructured as flows - groupsApi.useGetGroupsQuery(); - - // Queries can be conditionally fetched based if optional parameter skip is true; - // Skip this query if user is not admin - // When user is an admin, ensure that the initial Group data exists and is not currently fetching - // groupsApi.useGetAllGroupsChildrenQuery(undefined, { skip: (!isAdmin || !groupData || groupDataIsFetching) }); - - - - // There are many derived hooks each with different use cases - // Read More @ https://redux-toolkit.js.org/rtk-query/api/created-api/hooks#hooks-overview - - - // Only run once by making it depend on an empty array. - useEffect(() => { - // If user has token from prior logins verify, and fetch user details if valid. - if (hasToken()) { - // use the verify token mutation, - verifyTokenTrigger(getToken()) - } - dispatch(fetchMapsDetails()); - ConversionArray.fetchPik(); - - - // Converted to useHooks() - // dispatch(fetchMetersDetailsIfNeeded()); - // dispatch(fetchGroupsDetailsIfNeeded()); - // dispatch(fetchPreferencesIfNeeded()); - // dispatch(fetchUnitsDetailsIfNeeded()); - // dispatch(fetchConversionsDetailsIfNeeded()); - }, [dispatch, verifyTokenTrigger]); - - return ( -
- -
- ); -} diff --git a/src/client/app/components/LoginComponent.tsx b/src/client/app/components/LoginComponent.tsx index 6fedc6c7b..27f78989d 100644 --- a/src/client/app/components/LoginComponent.tsx +++ b/src/client/app/components/LoginComponent.tsx @@ -5,13 +5,11 @@ import * as React from 'react'; import { useRef, useState } from 'react'; import { FormattedMessage } from 'react-intl'; +import { useNavigate } from 'react-router-dom-v5-compat'; import { Button, Form, FormGroup, Input, Label } from 'reactstrap'; -import FooterContainer from '../containers/FooterContainer'; import { authApi } from '../redux/api/authApi'; import { showErrorNotification, showSuccessNotification } from '../utils/notifications'; import translate from '../utils/translate'; -import HeaderComponent from './HeaderComponent'; -import { useNavigate } from 'react-router-dom-v5-compat'; /** @@ -50,7 +48,6 @@ export default function LoginComponent() { return (
-
@@ -82,7 +79,6 @@ export default function LoginComponent() { -
) } diff --git a/src/client/app/components/MeterAndGroupSelectComponent.tsx b/src/client/app/components/MeterAndGroupSelectComponent.tsx index d88236ca4..6e6cdb0fe 100644 --- a/src/client/app/components/MeterAndGroupSelectComponent.tsx +++ b/src/client/app/components/MeterAndGroupSelectComponent.tsx @@ -99,13 +99,17 @@ const animatedComponents = makeAnimated(); const customStyles: StylesConfig = { valueContainer: base => ({ ...base, - maxHeight: 150, + maxHeight: 175, overflowY: 'scroll', '&::-webkit-scrollbar': { display: 'none' }, 'msOverflowStyle': 'none', 'scrollbarWidth': 'none' + }), + multiValue: base => ({ + ...base }) + }; diff --git a/src/client/app/components/RouteComponent.tsx b/src/client/app/components/RouteComponent.tsx index b044f0050..8d5ebc504 100644 --- a/src/client/app/components/RouteComponent.tsx +++ b/src/client/app/components/RouteComponent.tsx @@ -33,7 +33,6 @@ import MetersDetailComponent from './meters/MetersDetailComponent'; import GroupsDetailComponent from './groups/GroupsDetailComponent'; import ConversionsDetailComponent from './conversion/ConversionsDetailComponent'; import * as queryString from 'query-string'; -import InitializationComponent from './InitializationComponent'; interface RouteProps { barStacking: boolean; @@ -286,7 +285,6 @@ export default class RouteComponent extends React.Component { const messages = (LocaleTranslationData as any)[lang]; return (
- <> diff --git a/src/client/app/components/RouteComponentWIP.tsx b/src/client/app/components/RouteComponentWIP.tsx index 6470c575c..3e4a74cf0 100644 --- a/src/client/app/components/RouteComponentWIP.tsx +++ b/src/client/app/components/RouteComponentWIP.tsx @@ -25,6 +25,7 @@ import { AreaUnitType } from '../utils/getAreaUnitConversion'; import { showErrorNotification } from '../utils/notifications'; import translate from '../utils/translate'; import HomeComponent from './HomeComponent'; +import AppLayout from './AppLayout'; import LoginComponent from './LoginComponent'; import SpinnerComponent from './SpinnerComponent'; import AdminComponent from './admin/AdminComponent'; @@ -36,6 +37,7 @@ import UnitsDetailComponent from './unit/UnitsDetailComponent'; + const useWaitForInit = () => { const dispatch = useAppDispatch(); const isAdmin = useAppSelector(state => selectIsLoggedInAsAdmin(state)); @@ -102,6 +104,7 @@ export const RoleOutlet = ({ UserRole }: { UserRole: UserRole }) => { } export const NotFound = () => { + // redirect to home page if non-existent route is requested. return } @@ -210,32 +213,37 @@ export const GraphLink = () => { /// Router const router = createBrowserRouter([ - { path: '/', element: }, - { path: '/login', element: }, { - path: '/', - element: , + path: '/', element: , children: [ - { path: 'admin', element: }, - { path: 'calibration', element: }, - { path: 'maps', element: }, - { path: 'users/new', element: }, - { path: 'units', element: }, - { path: 'conversions', element: }, - { path: 'groups', element: }, - { path: 'meters', element: }, - { path: 'users', element: } - ] - }, - { - path: '/', - element: , - children: [ - { path: 'csv', element: } + { index: true, element: }, + { path: '/login', element: }, + { + path: '/', + element: , + children: [ + { path: 'admin', element: }, + { path: 'calibration', element: }, + { path: 'maps', element: }, + { path: 'users/new', element: }, + { path: 'units', element: }, + { path: 'conversions', element: }, + { path: 'groups', element: }, + { path: 'meters', element: }, + { path: 'users', element: }, + { + path: '/', + element: , + children: [ + { path: 'csv', element: } + ] + }, + { + path: '*', element: + } + ] + } ] - }, - { - path: '*', element: } ]) diff --git a/src/client/app/components/ThreeDComponent.tsx b/src/client/app/components/ThreeDComponent.tsx index 2cd091472..9f3100961 100644 --- a/src/client/app/components/ThreeDComponent.tsx +++ b/src/client/app/components/ThreeDComponent.tsx @@ -5,23 +5,25 @@ import * as moment from 'moment'; import * as React from 'react'; import Plot from 'react-plotly.js'; -import { useSelector } from 'react-redux'; +import { selectGraphState } from '../reducers/graph'; +import { selectGroupDataById } from '../redux/api/groupsApi'; +import { selectMeterDataById } from '../redux/api/metersApi'; import { readingsApi } from '../redux/api/readingsApi'; +import { selectUnitDataById } from '../redux/api/unitsApi'; import { useAppSelector } from '../redux/hooks'; +import { ChartSingleQueryProps, ThreeDReadingApiArgs } from '../redux/selectors/dataSelectors'; import { selectThreeDComponentInfo } from '../redux/selectors/threeDSelectors'; import { ThreeDReading } from '../types/readings'; import { GraphState, MeterOrGroup } from '../types/redux/graph'; -import { GroupsState } from '../types/redux/groups'; -import { MetersState } from '../types/redux/meters'; -import { State } from '../types/redux/state'; -import { UnitsState } from '../types/redux/units'; +import { GroupDataByID } from '../types/redux/groups'; +import { MeterDataByID } from '../types/redux/meters'; +import { UnitDataById } from '../types/redux/units'; import { isValidThreeDInterval, roundTimeIntervalForFetch } from '../utils/dateRangeCompatibility'; import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; import { lineUnitLabel } from '../utils/graphics'; import translate from '../utils/translate'; import SpinnerComponent from './SpinnerComponent'; import ThreeDPillComponent from './ThreeDPillComponent'; -import { ChartSingleQueryProps, ThreeDReadingApiArgs } from '../redux/selectors/dataSelectors'; /** * Component used to render 3D graphics @@ -31,10 +33,10 @@ import { ChartSingleQueryProps, ThreeDReadingApiArgs } from '../redux/selectors/ export default function ThreeDComponent(props: ChartSingleQueryProps) { const { args, skipQuery } = props.queryArgs; const { data, isFetching } = readingsApi.endpoints.threeD.useQuery(args, { skip: skipQuery }); - const metersState = useSelector((state: State) => state.meters); - const groupsState = useSelector((state: State) => state.groups); - const graphState = useSelector((state: State) => state.graph); - const unitState = useSelector((state: State) => state.units); + const { data: meterDataById = {} } = useAppSelector(selectMeterDataById); + const { data: groupDataById = {} } = useAppSelector(selectGroupDataById); + const { data: unitDataById = {} } = useAppSelector(selectUnitDataById); + const graphState = useAppSelector(selectGraphState); const { meterOrGroupID, meterOrGroupName, isAreaCompatible } = useAppSelector(selectThreeDComponentInfo); @@ -62,7 +64,7 @@ export default function ThreeDComponent(props: ChartSingleQueryProps 0 && areaUnit != AreaUnitType.none)) { diff --git a/src/client/app/components/ThreeDPillComponent.tsx b/src/client/app/components/ThreeDPillComponent.tsx index 623787499..f5eb9ad72 100644 --- a/src/client/app/components/ThreeDPillComponent.tsx +++ b/src/client/app/components/ThreeDPillComponent.tsx @@ -3,11 +3,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; import { Badge } from 'reactstrap'; -import { State } from '../types/redux/state'; -import { Dispatch } from '../types/redux/actions'; -import { changeMeterOrGroupInfo } from '../actions/graph'; +import { updateThreeDMeterOrGroupInfo } from '../reducers/graph'; +import { selectGroupDataById } from '../redux/api/groupsApi'; +import { selectMeterDataById } from '../redux/api/metersApi'; +import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { MeterOrGroup, MeterOrGroupPill } from '../types/redux/graph'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; @@ -16,15 +16,15 @@ import { AreaUnitType } from '../utils/getAreaUnitConversion'; * @returns List of selected groups and meters as reactstrap Pills Badges */ export default function ThreeDPillComponent() { - const dispatch: Dispatch = useDispatch(); - const metersState = useSelector((state: State) => state.meters); - const groupsState = useSelector((state: State) => state.groups); - const threeDState = useSelector((state: State) => state.graph.threeD); - const graphState = useSelector((state: State) => state.graph); + const dispatch = useAppDispatch(); + const { data: meterDataById = {} } = useAppSelector(selectMeterDataById); + const { data: groupDataById = {} } = useAppSelector(selectGroupDataById); + const threeDState = useAppSelector(state => state.graph.threeD); + const graphState = useAppSelector(state => state.graph); const meterPillData = graphState.selectedMeters.map(meterID => { - const area = metersState.byMeterID[meterID].area; - const areaUnit = metersState.byMeterID[meterID].areaUnit; + const area = meterDataById[meterID].area; + const areaUnit = meterDataById[meterID].areaUnit; const isAreaCompatible = area !== 0 && areaUnit !== AreaUnitType.none; const isDisabled = !isAreaCompatible && graphState.areaNormalization @@ -32,24 +32,29 @@ export default function ThreeDPillComponent() { }) const groupPillData = graphState.selectedGroups.map(groupID => { - const area = groupsState.byGroupID[groupID].area; - const areaUnit = groupsState.byGroupID[groupID].areaUnit; + const area = groupDataById[groupID].area; + const areaUnit = groupDataById[groupID].areaUnit; const isAreaCompatible = area !== 0 && areaUnit !== AreaUnitType.none; const isDisabled = !isAreaCompatible && graphState.areaNormalization return { meterOrGroupID: groupID, isDisabled: isDisabled, meterOrGroup: MeterOrGroup.groups } as MeterOrGroupPill }) // When a Pill Badge is clicked update threeD state to indicate new meter or group to render. - const handlePillClick = (pillData: MeterOrGroupPill) => dispatch(changeMeterOrGroupInfo(pillData.meterOrGroupID, pillData.meterOrGroup)); + const handlePillClick = (pillData: MeterOrGroupPill) => dispatch(updateThreeDMeterOrGroupInfo( + { + meterOrGroupID: pillData.meterOrGroupID, + meterOrGroup: pillData.meterOrGroup + } + )); // Method Generates Reactstrap Pill Badges for selected meters or groups const populatePills = (meterOrGroupPillData: MeterOrGroupPill[]) => { return meterOrGroupPillData.map(pillData => { //retrieve data from appropriate state slice .meters or .group const meterOrGroupName = pillData.meterOrGroup === MeterOrGroup.meters ? - metersState.byMeterID[pillData.meterOrGroupID].identifier + meterDataById[pillData.meterOrGroupID].identifier : - groupsState.byGroupID[pillData.meterOrGroupID].name; + groupDataById[pillData.meterOrGroupID].name; // Get Selected ID from state const selectedMeterOrGroupID = threeDState.meterOrGroupID; diff --git a/src/client/app/components/TimeZoneSelect.tsx b/src/client/app/components/TimeZoneSelect.tsx index ef124836b..c4b66865c 100644 --- a/src/client/app/components/TimeZoneSelect.tsx +++ b/src/client/app/components/TimeZoneSelect.tsx @@ -22,6 +22,7 @@ const TimeZoneSelect: React.FC = ({ current, handleClick }) React.useEffect(() => { if (!optionsLoaded) { + // TODO REWRITE AS rtkQUERY? axios.get('/api/timezones').then(res => { const timeZones = res.data; const resetTimeZone = [{ value: null, label: translate('timezone.no') }]; diff --git a/src/client/app/components/admin/AdminComponent.tsx b/src/client/app/components/admin/AdminComponent.tsx index e9bd83a60..8bb30511f 100644 --- a/src/client/app/components/admin/AdminComponent.tsx +++ b/src/client/app/components/admin/AdminComponent.tsx @@ -4,13 +4,11 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import HeaderComponent from '../../components/HeaderComponent'; -import FooterContainer from '../../containers/FooterContainer'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; // import PreferencesContainer from '../../containers/admin/PreferencesContainer'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; +import PreferencesComponentWIP from './PreferencesComponentWIP'; import ManageUsersLinkButtonComponent from './users/ManageUsersLinkButtonComponent'; -import PreferencesComponentWIP from './PreferencesComponentWIP' /** * React component that defines the admin page @@ -36,7 +34,6 @@ export default function AdminComponent() { }; return (
-

@@ -58,7 +55,6 @@ export default function AdminComponent() {

-
); } diff --git a/src/client/app/components/admin/UsersDetailComponentWIP.tsx b/src/client/app/components/admin/UsersDetailComponentWIP.tsx index 926411cfb..61dbeeda0 100644 --- a/src/client/app/components/admin/UsersDetailComponentWIP.tsx +++ b/src/client/app/components/admin/UsersDetailComponentWIP.tsx @@ -6,13 +6,11 @@ import * as _ from 'lodash'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { Button, Input, Table } from 'reactstrap'; -import FooterContainer from '../../containers/FooterContainer'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; import { userApi } from '../../redux/api/userApi'; import { User, UserRole } from '../../types/items'; import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; import translate from '../../utils/translate'; -import HeaderComponent from '../HeaderComponent'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import { UnsavedWarningComponentWIP } from '../UnsavedWarningComponentWIP'; import CreateUserLinkButtonComponent from './users/CreateUserLinkButtonComponent'; @@ -58,7 +56,6 @@ export default function UserDetailComponentWIP() { return (
-
- -
) } diff --git a/src/client/app/components/conversion/ConversionViewComponentWIP.tsx b/src/client/app/components/conversion/ConversionViewComponentWIP.tsx new file mode 100644 index 000000000..def042f61 --- /dev/null +++ b/src/client/app/components/conversion/ConversionViewComponentWIP.tsx @@ -0,0 +1,91 @@ +/* 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'; +// Realize that * is already imported from react +import { useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Button } from 'reactstrap'; +import { ConversionData } from 'types/redux/conversions'; +import '../../styles/card-page.css'; +import translate from '../../utils/translate'; +import EditConversionModalComponentWIP from './EditConversionModalComponentWIP'; +import { useAppSelector } from '../../redux/hooks'; +import { selectUnitDataById } from '../../redux/api/unitsApi'; + +interface ConversionViewComponentProps { + conversion: ConversionData; +} + +/** + * Defines the conversion info card + * @param props defined above + * @returns Single conversion element + */ +export default function ConversionViewComponent(props: ConversionViewComponentProps) { + // Don't check if admin since only an admin is allow to route to this page. + + // Edit Modal Show + const [showEditModal, setShowEditModal] = useState(false); + const { data: unitDataById = {} } = useAppSelector(selectUnitDataById) + + const handleShow = () => { + setShowEditModal(true); + } + + const handleClose = () => { + setShowEditModal(false); + } + React.useEffect(() => undefined, [props.conversion]) + // Create header from sourceId, destinationId identifiers + // Arrow is bidirectional if conversion is bidirectional and one way if not. + let arrowShown: string; + if (props.conversion.bidirectional) { + arrowShown = ' ↔ '; + } else { + arrowShown = ' → '; + } + const header = String(unitDataById[props.conversion.sourceId].identifier + arrowShown + unitDataById[props.conversion.destinationId].identifier); + + // Unlike the details component, we don't check if units are loaded since must come through that page. + + return ( +
+
+ {header} +
+
+ {unitDataById[props.conversion.sourceId].identifier} +
+
+ {unitDataById[props.conversion.destinationId].identifier} +
+
+ {translate(`TrueFalseType.${props.conversion.bidirectional.toString()}`)} +
+
+ {props.conversion.slope} +
+
+ {props.conversion.intercept} +
+
+ {/* Only show first 30 characters so card does not get too big. Should limit to one line */} + {props.conversion.note.slice(0, 29)} +
+
+ + {/* Creates a child ConversionModalEditComponent */} + +
+
+ ); +} diff --git a/src/client/app/components/conversion/ConversionsDetailComponent.tsx b/src/client/app/components/conversion/ConversionsDetailComponent.tsx index 2e4fddf8e..7ae4f43cc 100644 --- a/src/client/app/components/conversion/ConversionsDetailComponent.tsx +++ b/src/client/app/components/conversion/ConversionsDetailComponent.tsx @@ -16,7 +16,9 @@ import CreateConversionModalComponent from './CreateConversionModalComponent'; import { ConversionData } from 'types/redux/conversions'; import SpinnerComponent from '../../components/SpinnerComponent'; import HeaderComponent from '../../components/HeaderComponent'; -import { Dispatch } from 'types/redux/actions'; +import { Dispatch } from '../../types/redux/actions'; +import { useAppSelector } from '../../redux/hooks'; +import { selectConversionsDetails } from '../../redux/api/conversionsApi'; /** * Defines the conversions page card view @@ -33,7 +35,8 @@ export default function ConversionsDetailComponent() { }, [dispatch]); // Conversions state - const conversionsState = useSelector((state: State) => state.conversions.conversions); + const { data: conversionsState = [] } = useAppSelector(selectConversionsDetails); + const isUpdatingCikAndDBViews = useSelector((state: State) => state.admin.isUpdatingCikAndDBViews); diff --git a/src/client/app/components/conversion/ConversionsDetailComponentWIP.tsx b/src/client/app/components/conversion/ConversionsDetailComponentWIP.tsx index 179d06377..e34306748 100644 --- a/src/client/app/components/conversion/ConversionsDetailComponentWIP.tsx +++ b/src/client/app/components/conversion/ConversionsDetailComponentWIP.tsx @@ -4,15 +4,13 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import HeaderComponent from '../../components/HeaderComponent'; import SpinnerComponent from '../../components/SpinnerComponent'; -import FooterContainer from '../../containers/FooterContainer'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; import { conversionsApi } from '../../redux/api/conversionsApi'; import { unitsApi } from '../../redux/api/unitsApi'; import { ConversionData } from '../../types/redux/conversions'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import ConversionViewComponent from './ConversionViewComponent'; +import ConversionViewComponentWIP from './ConversionViewComponentWIP'; import CreateConversionModalComponentWIP from './CreateConversionModalComponentWIP'; /** @@ -54,7 +52,6 @@ export default function ConversionsDetailComponent() {
) : (
-
@@ -64,25 +61,27 @@ export default function ConversionsDetailComponent() {
- {unitDataById && -
- -
} +
+ +
{/* Attempt to create a ConversionViewComponent for each ConversionData in Conversions State after sorting by the combination of the identifier of the source and destination of the conversion. */} - {unitDataById && Object.values(conversionsState) - .sort((conversionA: ConversionData, conversionB: ConversionData) => - ((unitDataById[conversionA.sourceId].identifier + unitDataById[conversionA.destinationId].identifier).toLowerCase() > - (unitDataById[conversionB.sourceId].identifier + unitDataById[conversionB.destinationId].identifier).toLowerCase()) ? 1 : - (((unitDataById[conversionB.sourceId].identifier + unitDataById[conversionB.destinationId].identifier).toLowerCase() > - (unitDataById[conversionA.sourceId].identifier + unitDataById[conversionA.destinationId].identifier).toLowerCase()) ? -1 : 0)) - .map(conversionData => (' + (conversionData as ConversionData).destinationId)} - units={unitDataById} />))} + { + Object.values(conversionsState) + .sort((conversionA: ConversionData, conversionB: ConversionData) => + ((unitDataById[conversionA.sourceId].identifier + unitDataById[conversionA.destinationId].identifier).toLowerCase() > + (unitDataById[conversionB.sourceId].identifier + unitDataById[conversionB.destinationId].identifier).toLowerCase()) ? 1 : + (((unitDataById[conversionB.sourceId].identifier + unitDataById[conversionB.destinationId].identifier).toLowerCase() > + (unitDataById[conversionA.sourceId].identifier + unitDataById[conversionA.destinationId].identifier).toLowerCase()) ? -1 : 0)) + .map(conversionData => ( + ' + conversionData.destinationId} + /> + ))}
- )} diff --git a/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx b/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx index 17e4b6052..3b63bd01e 100644 --- a/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx +++ b/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx @@ -7,15 +7,16 @@ import * as React from 'react'; import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; -import { selectIsValidConversion } from '../../redux/selectors/adminSelectors'; -import { UnitData } from '../../types/redux/units'; -import { addConversion } from '../../actions/conversions'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import { conversionsApi } from '../../redux/api/conversionsApi'; import { selectUnitDataById } from '../../redux/api/unitsApi'; -import { useAppDispatch, useAppSelector } from '../../redux/hooks'; +import { useAppSelector } from '../../redux/hooks'; +import { selectIsValidConversion } from '../../redux/selectors/adminSelectors'; import '../../styles/modal.css'; import { tooltipBaseStyle } from '../../styles/modalStyle'; import { TrueFalseType } from '../../types/items'; +import { UnitData } from '../../types/redux/units'; +import { showErrorNotification } from '../../utils/notifications'; import translate from '../../utils/translate'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; @@ -24,8 +25,7 @@ import TooltipMarkerComponent from '../TooltipMarkerComponent'; * @returns Conversion create element */ export default function CreateConversionModalComponent() { - - const dispatch = useAppDispatch(); + const [addConversionMutation] = conversionsApi.useAddConversionMutation() const { data: unitDataById = {} } = useAppSelector(selectUnitDataById) // Want units in sorted order by identifier regardless of case. const unitsSorted = _.sortBy(Object.values(unitDataById), unit => unit.identifier.toLowerCase(), 'asc'); @@ -55,6 +55,15 @@ export default function CreateConversionModalComponent() { // Handlers for each type of input change const [conversionState, setConversionState] = useState(defaultValues); + // If the currently selected conversion is valid + const [validConversion, reason] = useAppSelector( + state => selectIsValidConversion( + state, + conversionState.sourceId, + conversionState.destinationId, + conversionState.bidirectional) + ) + const handleStringChange = (e: React.ChangeEvent) => { setConversionState({ ...conversionState, [e.target.name]: e.target.value }); } @@ -81,97 +90,8 @@ export default function CreateConversionModalComponent() { setConversionState(state => ({ ...state, [e.target.name]: Number(e.target.value) })); } } - - // If the currently selected conversion is valid - const validConversion = useAppSelector( - state => selectIsValidConversion( - state, - conversionState.sourceId, - conversionState.destinationId, - conversionState.bidirectional) - ) /* End State */ - - // //Update the valid conversion state any time the source id, destination id, or bidirectional status changes - // useEffect(() => { - // /** - // * Checks if conversion is valid - // * @param sourceId New conversion sourceId - // * @param destinationId New conversion destinationId - // * @param bidirectional New conversion bidirectional status - // * @returns boolean representing if new conversion is valid or not - // */ - // const isValidConversion = (sourceId: number, destinationId: number, bidirectional: boolean) => { - // /* Create Conversion Validation: - // Source equals destination: invalid conversion - // Conversion exists: invalid conversion - // Conversion does not exist: - // Inverse exists: - // Conversion is bidirectional: invalid conversion - // Destination cannot be a meter - // Cannot mix unit represent - // TODO Some of these can go away when we make the menus dynamic. - // */ - - // // The destination cannot be a meter unit. - // if (destinationId !== -999 && unitDataById[destinationId].typeOfUnit === UnitType.meter) { - // notifyUser(translate('conversion.create.destination.meter')); - // return false; - // } - - // // Source or destination not set - // if (sourceId === -999 || destinationId === -999) { - // return false - // } - - // // Conversion already exists - // if ((conversionState.findIndex(conversionData => (( - // conversionData.sourceId === state.sourceId) && - // conversionData.destinationId === state.destinationId))) !== -1) { - // notifyUser(translate('conversion.create.exists')); - // return false; - // } - - // // You cannot have a conversion between units that differ in unit_represent. - // // This means you cannot mix quantity, flow & raw. - // if (unitDataById[sourceId].unitRepresent !== unitDataById[destinationId].unitRepresent) { - // notifyUser(translate('conversion.create.mixed.represent')); - // return false; - // } - - - // let isValid = true; - // // Loop over conversions and check for existence of inverse of conversion passed in - // // If there exists an inverse that is bidirectional, then there is no point in making a conversion since it is essentially a duplicate. - // // If there is a non bidirectional inverse, then it is a valid conversion - // Object.values(conversionState).forEach(conversion => { - // // Inverse exists - // if ((conversion.sourceId === destinationId) && (conversion.destinationId === sourceId)) { - // // Inverse is bidirectional - // if (conversion.bidirectional) { - // isValid = false; - // } - // // Inverse is not bidirectional - // else { - // // Do not allow for a bidirectional conversion with an inverse that is not bidirectional - // if (bidirectional) { - // // The new conversion is bidirectional - // isValid = false; - // } - // } - // } - // }); - // if (!isValid) { - // notifyUser(translate('conversion.create.exists.inverse')); - // } - // return isValid; - // } - - - // setValidConversion(isValidConversion(state.sourceId, state.destinationId, state.bidirectional)); - // }, [state.sourceId, state.destinationId, state.bidirectional, unitDataById, conversionState]); - // Reset the state to default values const resetState = () => { setConversionState(defaultValues); @@ -182,11 +102,18 @@ export default function CreateConversionModalComponent() { // Submit const handleSubmit = () => { - // Close modal first to avoid repeat clicks - setShowModal(false); - // Add the new conversion and update the store - dispatch(addConversion(conversionState)); - resetState(); + if (validConversion) { + // Close modal first to avoid repeat clicks + setShowModal(false); + //5 Add the new conversion and update the store + // Omit the source options , do not need to send in request so remove here. + // + addConversionMutation(_.omit(conversionState, 'sourceOptions')) + // dispatch(addConversion(conversionState)); + resetState(); + } else { + showErrorNotification(reason) + } }; const tooltipStyle = { @@ -320,16 +247,21 @@ export default function CreateConversionModalComponent() { + { + // Todo looks kind of bad make a better visible notification + !validConversion &&

{reason}

+ } + {/* Hides the modal */} {/* On click calls the function handleSaveChanges in this component */} -
); -} +} \ No newline at end of file diff --git a/src/client/app/components/conversion/EditConversionModalComponentWIP.tsx b/src/client/app/components/conversion/EditConversionModalComponentWIP.tsx new file mode 100644 index 000000000..394917b03 --- /dev/null +++ b/src/client/app/components/conversion/EditConversionModalComponentWIP.tsx @@ -0,0 +1,258 @@ +/* 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'; +// Realize that * is already imported from react +import { useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Button, Col, Container, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; +import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import { conversionsApi } from '../../redux/api/conversionsApi'; +import { selectUnitDataById } from '../../redux/api/unitsApi'; +import { useAppSelector } from '../../redux/hooks'; +import '../../styles/modal.css'; +import { tooltipBaseStyle } from '../../styles/modalStyle'; +import { TrueFalseType } from '../../types/items'; +import { ConversionData } from '../../types/redux/conversions'; +import translate from '../../utils/translate'; +import ConfirmActionModalComponent from '../ConfirmActionModalComponent'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; + + +interface EditConversionModalComponentProps { + show: boolean; + conversion: ConversionData; + header: string; + // passed in to handle opening the modal + handleShow: () => void; + // passed in to handle closing the modal + handleClose: () => void; +} + +/** + * Defines the edit conversion modal form + * @param props Props for the component + * @returns Conversion edit element + */ +export default function EditConversionModalComponent(props: EditConversionModalComponentProps) { + const [editConversion] = conversionsApi.useEditConversionMutation() + const [deleteConversion] = conversionsApi.useDeleteConversionMutation() + const { data: unitDataById = {} } = useAppSelector(selectUnitDataById) + + // Set existing conversion values + const values = { ...props.conversion } + + /* State */ + // Handlers for each type of input change + const [state, setState] = useState(values); + + const handleStringChange = (e: React.ChangeEvent) => { + setState({ ...state, [e.target.name]: e.target.value }); + } + + const handleBooleanChange = (e: React.ChangeEvent) => { + setState({ ...state, [e.target.name]: JSON.parse(e.target.value) }); + } + + const handleNumberChange = (e: React.ChangeEvent) => { + setState({ ...state, [e.target.name]: Number(e.target.value) }); + } + /* End State */ + + /* Confirm Delete Modal */ + // Separate from state comment to keep everything related to the warning confirmation modal together + const [showDeleteConfirmationModal, setShowDeleteConfirmationModal] = useState(false); + const deleteConfirmationMessage = translate('conversion.delete.conversion') + ' [' + props.header + '] ?'; + const deleteConfirmText = translate('conversion.delete.conversion'); + const deleteRejectText = translate('cancel'); + // The first two handle functions below are required because only one Modal can be open at a time (properly) + const handleDeleteConfirmationModalClose = () => { + // Hide the warning modal + setShowDeleteConfirmationModal(false); + // Show the edit modal + handleShow(); + } + const handleDeleteConfirmationModalOpen = () => { + // Hide the edit modal + handleClose(); + // Show the warning modal + setShowDeleteConfirmationModal(true); + } + const handleDeleteConversion = () => { + // Closes the warning modal + // Do not call the handler function because we do not want to open the parent modal + setShowDeleteConfirmationModal(false); + + // Delete the conversion using the state object, it should only require the source and destination ids set + deleteConversion({ sourceId: state.sourceId, destinationId: state.destinationId }) + + } + /* End Confirm Delete Modal */ + + // Reset the state to default values + // To be used for the discard changes button + // Different use case from CreateConversionModalComponent's resetState + // This allows us to reset our state to match the store in the event of an edit failure + // Failure to edit conversions will not trigger a re-render, as no state has changed. Therefore, we must manually reset the values + const resetState = () => { + setState(values); + } + + const handleShow = () => { + props.handleShow(); + } + + const handleClose = () => { + resetState(); + props.handleClose(); + } + + // Save changes + // Currently using the old functionality which is to compare inherited prop values to state values + // If there is a difference between props and state, then a change was made + // Side note, we could probably just set a boolean when any input i + // Edit Conversion Validation: is not needed as no breaking edits can be made + const handleSaveChanges = () => { + // Close the modal first to avoid repeat clicks + props.handleClose(); + + // Need to redo Cik if slope, intercept, or bidirectional changes. + const shouldRedoCik = props.conversion.slope !== state.slope + || props.conversion.intercept !== state.intercept + || props.conversion.bidirectional !== state.bidirectional; + // Check for changes by comparing state to props + const conversionHasChanges = shouldRedoCik || props.conversion.note != state.note; + // Only do work if there are changes + if (conversionHasChanges) { + // Save our changes by dispatching the submitEditedConversion action + // dispatch(submitEditedConversion(state, shouldRedoCik)); + editConversion({ conversionData: state, shouldRedoCik }) + // dispatch(unsavedWarningSlice.actions.removeUnsavedChanges()); + } + } + + const tooltipStyle = { + ...tooltipBaseStyle, + tooltipEditConversionView: 'help.admin.conversionedit' + }; + + return ( + <> + + + + + +
+ +
+
+ {/* when any of the conversion are changed call one of the functions. */} + + + + + {/* Source unit - display only */} + + + + + + + + {/* Destination unit - display only */} + + + + + + + + {/* Bidirectional Y/N input */} + + + handleBooleanChange(e)}> + {Object.keys(TrueFalseType).map(key => { + return () + })} + + + + + {/* Slope input */} + + + handleNumberChange(e)} /> + + + + {/* Intercept input */} + + + handleNumberChange(e)} /> + + + + {/* Note input */} + + + handleStringChange(e)} /> + + + + + + {/* Hides the modal */} + + {/* On click calls the function handleSaveChanges in this component */} + + +
+ + ); +} diff --git a/src/client/app/components/groups/GroupsDetailComponentWIP.tsx b/src/client/app/components/groups/GroupsDetailComponentWIP.tsx index b34b3089a..a6fb8edb0 100644 --- a/src/client/app/components/groups/GroupsDetailComponentWIP.tsx +++ b/src/client/app/components/groups/GroupsDetailComponentWIP.tsx @@ -4,8 +4,6 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import HeaderComponent from '../../components/HeaderComponent'; -import FooterContainer from '../../containers/FooterContainer'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; import { useAppSelector } from '../../redux/hooks'; import { selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; @@ -42,7 +40,6 @@ export default function GroupsDetailComponentWIP() { return (
-
@@ -73,7 +70,6 @@ export default function GroupsDetailComponentWIP() {
}
-
); diff --git a/src/client/app/components/maps/MapCalibrationComponent.tsx b/src/client/app/components/maps/MapCalibrationComponent.tsx index bc7be57fa..46b6e092d 100644 --- a/src/client/app/components/maps/MapCalibrationComponent.tsx +++ b/src/client/app/components/maps/MapCalibrationComponent.tsx @@ -35,7 +35,6 @@ export default class MapCalibrationComponent extends React.Component -
{/* TODO These types of plotly containers expect a lot of passed values and it gives a TS error. Given we plan to replace this diff --git a/src/client/app/components/maps/MapsDetailComponent.tsx b/src/client/app/components/maps/MapsDetailComponent.tsx index a49d91812..fe7a8bfbc 100644 --- a/src/client/app/components/maps/MapsDetailComponent.tsx +++ b/src/client/app/components/maps/MapsDetailComponent.tsx @@ -3,18 +3,16 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { Table, Button } from 'reactstrap'; import { FormattedMessage } from 'react-intl'; -import { hasToken } from '../../utils/token'; -import FooterContainer from '../../containers/FooterContainer'; -import MapViewContainer from '../../containers/maps/MapViewContainer'; import { Link } from 'react-router-dom-v5-compat'; +import { Button, Table } from 'reactstrap'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; -import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import { store } from '../../store'; import UnsavedWarningContainer from '../../containers/UnsavedWarningContainer'; -import HeaderComponent from '../../components/HeaderComponent'; +import MapViewContainer from '../../containers/maps/MapViewContainer'; import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; +import { store } from '../../store'; +import { hasToken } from '../../utils/token'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; interface MapsDetailProps { @@ -61,7 +59,6 @@ export default class MapsDetailComponent extends React.Component -

@@ -109,7 +106,6 @@ export default class MapsDetailComponent extends React.Component }

-
); } diff --git a/src/client/app/components/meters/MetersDetailComponentWIP.tsx b/src/client/app/components/meters/MetersDetailComponentWIP.tsx index b6c91f32d..d049a7b00 100644 --- a/src/client/app/components/meters/MetersDetailComponentWIP.tsx +++ b/src/client/app/components/meters/MetersDetailComponentWIP.tsx @@ -4,8 +4,6 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import HeaderComponent from '../../components/HeaderComponent'; -import FooterContainer from '../../containers/FooterContainer'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; import { metersApi } from '../../redux/api/metersApi'; import { useAppSelector } from '../../redux/hooks'; @@ -32,7 +30,6 @@ export default function MetersDetailComponent() { return (
-
@@ -62,7 +59,6 @@ export default function MetersDetailComponent() {
}
- ); } diff --git a/src/client/app/components/unit/EditUnitModalComponent.tsx b/src/client/app/components/unit/EditUnitModalComponent.tsx index 4c84c223a..00bd6b126 100644 --- a/src/client/app/components/unit/EditUnitModalComponent.tsx +++ b/src/client/app/components/unit/EditUnitModalComponent.tsx @@ -12,6 +12,7 @@ import { Dispatch } from 'types/redux/actions'; import { submitEditedUnit } from '../../actions/units'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; +import { selectConversionsDetails } from '../../redux/api/conversionsApi'; import { selectMeterDataById } from '../../redux/api/metersApi'; import { useAppSelector } from '../../redux/hooks'; import '../../styles/modal.css'; @@ -55,7 +56,8 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp /* State */ // Handlers for each type of input change const [state, setState] = useState(values); - const globalConversionsState = useAppSelector(state => state.conversions.conversions); + const { data: globalConversionsState = [] } = useAppSelector(selectConversionsDetails); + const handleStringChange = (e: React.ChangeEvent) => { setState({ ...state, [e.target.name]: e.target.value }); diff --git a/src/client/app/components/unit/UnitsDetailComponent.tsx b/src/client/app/components/unit/UnitsDetailComponent.tsx index 6256a0ffb..afaa8b069 100644 --- a/src/client/app/components/unit/UnitsDetailComponent.tsx +++ b/src/client/app/components/unit/UnitsDetailComponent.tsx @@ -3,18 +3,15 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import { useSelector } from 'react-redux'; -import { UnitData } from 'types/redux/units'; -import HeaderComponent from '../../components/HeaderComponent'; import SpinnerComponent from '../../components/SpinnerComponent'; -import FooterContainer from '../../containers/FooterContainer'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import { selectUnitDataById } from '../../redux/api/unitsApi'; import { useAppSelector } from '../../redux/hooks'; -import { State } from '../../types/redux/state'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateUnitModalComponent from './CreateUnitModalComponent'; import UnitViewComponent from './UnitViewComponent'; -import { selectUnitDataById } from '../../redux/api/unitsApi'; +import { QueryStatus } from '@reduxjs/toolkit/query'; +import { UnitData } from 'types/redux/units'; /** * Defines the units page card view @@ -22,33 +19,20 @@ import { selectUnitDataById } from '../../redux/api/unitsApi'; */ export default function UnitsDetailComponent() { // The route stops you from getting to this page if not an admin. - const isUpdatingCikAndDBViews = useSelector((state: State) => state.admin.isUpdatingCikAndDBViews); //Units state - const { data: unitDataById = {} } = useAppSelector(selectUnitDataById); + const { data: unitDataById = {}, status } = useAppSelector(selectUnitDataById); - const titleStyle: React.CSSProperties = { - textAlign: 'center' - }; - - const tooltipStyle = { - display: 'inline-block', - fontSize: '50%', - // For now, only an admin can see the unit page. - tooltipUnitView: 'help.admin.unitview' - }; - return (
- {isUpdatingCikAndDBViews ? ( + {status === QueryStatus.pending ? (
) : (
-
@@ -64,15 +48,31 @@ export default function UnitsDetailComponent() {
{/* Create a UnitViewComponent for each UnitData in Units State after sorting by identifier */} - {Object.values(unitDataById) - .sort((unitA: UnitData, unitB: UnitData) => (unitA.identifier.toLowerCase() > unitB.identifier.toLowerCase()) ? 1 : - ((unitB.identifier.toLowerCase() > unitA.identifier.toLowerCase()) ? -1 : 0)) - .map(unitData => ())} + { + Object.values(unitDataById) + .sort((unitA: UnitData, unitB: UnitData) => (unitA.identifier.toLowerCase() > unitB.identifier.toLowerCase()) ? 1 : + ((unitB.identifier.toLowerCase() > unitA.identifier.toLowerCase()) ? -1 : 0)) + .map((unitData: UnitData) => ( + + ))}
-
)} ); } + +const titleStyle: React.CSSProperties = { + textAlign: 'center' +}; + +const tooltipStyle = { + display: 'inline-block', + fontSize: '50%', + // For now, only an admin can see the unit page. + tooltipUnitView: 'help.admin.unitview' +}; diff --git a/src/client/app/containers/admin/CreateUserContainer.tsx b/src/client/app/containers/admin/CreateUserContainer.tsx index cc5b57024..6c26ef5dc 100644 --- a/src/client/app/containers/admin/CreateUserContainer.tsx +++ b/src/client/app/containers/admin/CreateUserContainer.tsx @@ -3,14 +3,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import FooterContainer from '../FooterContainer'; import CreateUserComponent from '../../components/admin/CreateUserComponent'; import { UserRole } from '../../types/items'; import { usersApi } from '../../utils/api'; import { browserHistory } from '../../utils/history'; -import { showSuccessNotification, showErrorNotification } from '../../utils/notifications'; +import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; import translate from '../../utils/translate'; -import HeaderComponent from '../../components/HeaderComponent'; export default class CreateUserFormContainer extends React.Component<{}>{ constructor(props: {}) { @@ -61,7 +59,6 @@ export default class CreateUserFormContainer extends React.Component<{}>{ public render() { return (
- { handleRoleChange={this.handleRoleChange} submitNewUser={this.submitNewUser} /> -
) } diff --git a/src/client/app/index.tsx b/src/client/app/index.tsx index e6c871378..7a16ccf13 100644 --- a/src/client/app/index.tsx +++ b/src/client/app/index.tsx @@ -2,28 +2,24 @@ * 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 'bootstrap/dist/css/bootstrap.css'; import * as React from 'react'; import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux'; -import { store } from './store' -import 'bootstrap/dist/css/bootstrap.css'; +import { store } from './store'; // import RouteContainer from './containers/RouteContainer'; import RouteComponentWIP from './components/RouteComponentWIP'; +import { initializeApp } from './initScript'; import './styles/index.css'; -import InitializationComponent from './components/InitializationComponent'; +initializeApp() // Renders the entire application, starting with RouteComponent, into the root div -const container = document.getElementById('root'); +const container = document.getElementById('root') as HTMLElement; +const root = createRoot(container); -// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -const root = createRoot(container!); root.render( - // Provides the Redux store to all child components - - - {/* Route container is a test of react-router-dom v6 - This update introduces many useful routing hooks which can potentially be useful when migrating the codebase to hooks from Class components. - Very much experimental/ Work in Progress */} - - + // Provides the Redux store to all child components + < Provider store={store} stabilityCheck='always' > + < RouteComponentWIP /> +
); \ No newline at end of file diff --git a/src/client/app/initScript.ts b/src/client/app/initScript.ts index b8b5a996b..52d408c36 100644 --- a/src/client/app/initScript.ts +++ b/src/client/app/initScript.ts @@ -2,15 +2,36 @@ * 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 { Dispatch } from './types/redux/actions'; -import { fetchCurrentUserIfNeeded } from './actions/currentUser'; +import { authApi } from './redux/api/authApi'; +import { conversionsApi } from './redux/api/conversionsApi'; +import { groupsApi } from './redux/api/groupsApi'; +import { metersApi } from './redux/api/metersApi'; +import { preferencesApi } from './redux/api/preferencesApi'; +import { unitsApi } from './redux/api/unitsApi'; +import { store } from './store'; +import { getToken, hasToken } from './utils/token'; -/* eslint-disable jsdoc/require-returns */ -/** - * The purpose of this is to store the user's role or any other information that would rarely change just once into the store. - */ -export default function initScript() { - return (dispatch: Dispatch) => { - dispatch(fetchCurrentUserIfNeeded()); - }; + +// Method initiates many data fetching calls on startup before react begins to render +export const initializeApp = async () => { + // There are two primary ways to fetch data with RTKQuery + // Redux Toolkit generates hooks for use in react components, and standalone initiate dispatches as seen below. + // https://redux-toolkit.js.org/rtk-query/usage/usage-without-react-hooks + + // 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. + store.dispatch(preferencesApi.endpoints.getPreferences.initiate()) + store.dispatch(unitsApi.endpoints.getUnitsDetails.initiate()) + store.dispatch(conversionsApi.endpoints.getConversionsDetails.initiate()) + store.dispatch(conversionsApi.endpoints.getConversionArray.initiate()) + + // If user is an admin, they receive additional meter details. + // To avoid sending duplicate requests upon startup, verify user then fetch + if (hasToken()) { + // User has a session token verify before requesting meter/group details + await store.dispatch(authApi.endpoints.verifyToken.initiate(getToken())) + } + // Request meter/group/details + store.dispatch(metersApi.endpoints.getMeters.initiate()) + store.dispatch(groupsApi.endpoints.getGroups.initiate()) } diff --git a/src/client/app/redux/api/authApi.ts b/src/client/app/redux/api/authApi.ts index d78b3210e..f47784618 100644 --- a/src/client/app/redux/api/authApi.ts +++ b/src/client/app/redux/api/authApi.ts @@ -21,23 +21,23 @@ export const authApi = baseApi.injectEndpoints({ // 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'] - // Listeners (ExtraReducers) for this query: + // Listeners for this query (ExtraReducers): // currentUserSlice->MatchFulfilled }), verifyToken: builder.mutation<{ success: boolean }, string>({ - query: queryArgs => ({ + query: token => ({ url: 'api/verification', method: 'POST', - body: { token: queryArgs } + body: { token: token } }), // Optional endpoint property that does additional logic when the query is initiated. - onQueryStarted: async (queryArgs, { dispatch, queryFulfilled }) => { + onQueryStarted: async (token, { dispatch, queryFulfilled }) => { // wait for the initial query (verifyToken) to finish await queryFulfilled .then(async () => { // 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(queryArgs)) + dispatch(currentUserSlice.actions.setUserToken(token)) // Get userDetails with verified token in headers const response = dispatch(userApi.endpoints.getUserDetails.initiate()); @@ -51,7 +51,7 @@ export const authApi = baseApi.injectEndpoints({ // if no error thrown user is now logged in and cache(s) may be out of date due to potential admin privileges etc. // manually invalidate potentially out of date cache stores - dispatch(baseApi.util.invalidateTags(['MeterData', 'GroupData'])) + dispatch(baseApi.util.invalidateTags(['MeterData', 'GroupData', 'Users'])) // If subscriptions to these tagged endpoints exist, they will automatically re-fetch. // Otherwise subsequent requests will bypass and overwrite cache }) diff --git a/src/client/app/redux/api/baseApi.ts b/src/client/app/redux/api/baseApi.ts index bee24fa6a..4e6834ab1 100644 --- a/src/client/app/redux/api/baseApi.ts +++ b/src/client/app/redux/api/baseApi.ts @@ -1,6 +1,6 @@ import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' import { RootState } from '../../store'; -// TODO Should be env variable +// TODO Should be env variable? const baseHref = (document.getElementsByTagName('base')[0] || {}).href; export const baseApi = createApi({ @@ -17,7 +17,15 @@ export const baseApi = createApi({ } }), // The types of tags that any injected endpoint may, provide, or invalidate. - tagTypes: ['MeterData', 'GroupData', 'GroupChildrenData', 'Preferences','Users'], + // Must be defined here, for use in injected endpoints + tagTypes: [ + 'MeterData', + 'GroupData', + 'GroupChildrenData', + 'Preferences', + 'Users', + 'ConversionDetails' + ], // Initially no defined endpoints, Use rtk query's injectEndpoints endpoints: () => ({}) // Defaults to 60 seconds or 1 minute diff --git a/src/client/app/redux/api/conversionsApi.ts b/src/client/app/redux/api/conversionsApi.ts index 23f7813f8..18e342ef4 100644 --- a/src/client/app/redux/api/conversionsApi.ts +++ b/src/client/app/redux/api/conversionsApi.ts @@ -4,52 +4,86 @@ import { baseApi } from './baseApi'; export const conversionsApi = baseApi.injectEndpoints({ endpoints: builder => ({ getConversionsDetails: builder.query({ - query: () => 'api/conversions' + query: () => 'api/conversions', + providesTags: ['ConversionDetails'] }), - addConversion: builder.query({ + getConversionArray: builder.query({ + query: () => 'api/conversion-array' + }), + addConversion: builder.mutation({ query: conversion => ({ url: 'api/conversions/addConversion', method: 'POST', - body: { conversion } + body: conversion, + responseHandler: 'text' }), - onQueryStarted: async (arg, api) => { - await api.queryFulfilled. - then(() => { - { - api.dispatch( - conversionsApi.endpoints.refresh.initiate({ - redoCik: false, - refreshReadingViews: false - })) - } + onQueryStarted: async (_arg, api) => { + // TODO write more robust logic for error handling, and manually invalidate tags instead? + // TODO Verify Behavior w/ Maintainers + api.queryFulfilled + .then(() => { + api.dispatch( + conversionsApi.endpoints.refresh.initiate({ + redoCik: true, + refreshReadingViews: false + })) }) - } + }), - deleteConversion: builder.query({ + deleteConversion: builder.mutation>({ query: conversion => ({ url: 'api/conversions/delete', method: 'POST', - body: { conversion } - }) + body: conversion, + responseHandler: 'text' + }), + onQueryStarted: async (_, { queryFulfilled, dispatch }) => { + // TODO write more robust logic for error handling, and manually invalidate tags instead? + // TODO Verify Behavior w/ Maintainers + queryFulfilled + .then(() => { + console.log('Refreshing') + dispatch(conversionsApi.endpoints.refresh.initiate({ redoCik: true, refreshReadingViews: false })) + }) + + } }), - editConversion: builder.query({ - query: conversion => ({ + editConversion: builder.mutation({ + query: ({ conversionData }) => ({ url: 'api/conversions/edit', method: 'POST', - body: { ...conversion } - }) - }), - getConversionArray: builder.query({ - query: () => 'api/conversion-array' + body: conversionData + }), + onQueryStarted: async ({ shouldRedoCik }, { queryFulfilled, dispatch }) => { + // TODO write more robust logic for error handling, and manually invalidate tags instead? + // TODO Verify Behavior w/ Maintainers + await queryFulfilled + + if (shouldRedoCik) { + dispatch(conversionsApi.endpoints.refresh.initiate( + { + redoCik: true, + refreshReadingViews: false + } + )) + } else { + dispatch(conversionsApi.util.invalidateTags(['ConversionDetails'])) + } + } }), refresh: builder.mutation({ query: args => ({ url: 'api/conversion-array/refresh', method: 'POST', - body: { redoCik: args.redoCik, refreshReadingViews: args.refreshReadingViews }, + body: { + redoCik: args.redoCik, + refreshReadingViews: args.refreshReadingViews + }, responseHandler: 'text' - }) + }), + // TODO check behavior with maintainers, always invalidates, should be conditional? + invalidatesTags: ['ConversionDetails'] }) }) }) diff --git a/src/client/app/redux/selectors/adminSelectors.ts b/src/client/app/redux/selectors/adminSelectors.ts index 57421b790..c72f6bb4d 100644 --- a/src/client/app/redux/selectors/adminSelectors.ts +++ b/src/client/app/redux/selectors/adminSelectors.ts @@ -9,6 +9,7 @@ import { UnitData, UnitType } from '../../types/redux/units' import { unitsCompatibleWithUnit } from '../../utils/determineCompatibleUnits' import { noUnitTranslated, potentialGraphicUnits } from '../../utils/input' import { selectUnitDataById } from '../api/unitsApi' +import translate from '../../utils/translate' export const selectAdminPreferences = createSelector( selectAdminState, @@ -204,6 +205,7 @@ export const makeSelectGraphicUnitCompatibility = () => { /** * Checks if conversion is valid + * @param state redux store RootState * @param sourceId New conversion sourceId * @param destinationId New conversion destinationId * @param bidirectional New conversion bidirectional status @@ -215,7 +217,7 @@ export const selectIsValidConversion = createSelector( (_state: RootState, sourceId: number) => sourceId, (_state: RootState, _sourceId: number, destinationId: number) => destinationId, (_state: RootState, _sourceId: number, _destinationId: number, bidirectional: boolean) => bidirectional, - ({ data: unitDataById = {} }, { data: conversionData = [] }, sourceId, destinationId, bidirectional) => { + ({ data: unitDataById = {} }, { data: conversionData = [] }, sourceId, destinationId, bidirectional): [boolean, string] => { /* Create Conversion Validation: Source equals destination: invalid conversion Conversion exists: invalid conversion @@ -226,16 +228,18 @@ export const selectIsValidConversion = createSelector( Cannot mix unit represent TODO Some of these can go away when we make the menus dynamic. */ + console.log('running again!') // The destination cannot be a meter unit. if (destinationId !== -999 && unitDataById[destinationId].typeOfUnit === UnitType.meter) { // notifyUser(translate('conversion.create.destination.meter')); - return false; + return [false, translate('conversion.create.destination.meter')]; } // Source or destination not set if (sourceId === -999 || destinationId === -999) { - return false + // TODO Translate Me! + return [false, 'Source or destination not set'] } // Conversion already exists @@ -243,14 +247,14 @@ export const selectIsValidConversion = createSelector( conversionData.sourceId === sourceId) && conversionData.destinationId === destinationId))) !== -1) { // notifyUser(translate('conversion.create.exists')); - return false; + return [false, translate('conversion.create.exists')]; } // You cannot have a conversion between units that differ in unit_represent. // This means you cannot mix quantity, flow & raw. if (unitDataById[sourceId].unitRepresent !== unitDataById[destinationId].unitRepresent) { // notifyUser(translate('conversion.create.mixed.represent')); - return false; + return [false, translate('conversion.create.mixed.represent')]; } @@ -275,9 +279,7 @@ export const selectIsValidConversion = createSelector( } } }); - if (!isValid) { - // notifyUser(translate('conversion.create.exists.inverse')); - } - return isValid; + + return !isValid ? [false, translate('conversion.create.exists.inverse')] : [isValid, 'Conversion is Valid'] } ) \ 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 5603a54e4..7eb413af5 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -356,7 +356,7 @@ export const selectVisibleUnitOrSuffixState = createSelector( ) export const selectUnitSelectData = createSelector( - selectMeterDataById, + selectUnitDataById, selectVisibleUnitOrSuffixState, selectSelectedMeters, selectSelectedGroups, @@ -462,17 +462,17 @@ export function getSelectOptionsByItem( // Once for the initial state type check, again because the interpreter (for some reason) needs to be sure that the property exists in the object // If else statements do not suffer from this if (type === 'unit') { - label = dataById[itemId].identifier; + label = dataById[itemId]?.identifier; } else if (type === 'meter') { - label = dataById[itemId].identifier; + label = dataById[itemId]?.identifier; meterOrGroup = MeterOrGroup.meters - defaultGraphicUnit = dataById[itemId].defaultGraphicUnit; + defaultGraphicUnit = dataById[itemId]?.defaultGraphicUnit; } else if (type === 'group') { - label = dataById[itemId].name; + label = dataById[itemId]?.name; meterOrGroup = MeterOrGroup.groups - defaultGraphicUnit = dataById[itemId].defaultGraphicUnit; + defaultGraphicUnit = dataById[itemId]?.defaultGraphicUnit; } // TODO This is a bit of a hack. When an admin logs in they may not have the new state so label is null. // This should clear once the state is loaded. @@ -496,14 +496,14 @@ export function getSelectOptionsByItem( label = dataById[itemId].identifier; } else if (type === 'meter') { - label = dataById[itemId].identifier; + label = dataById[itemId]?.identifier; meterOrGroup = MeterOrGroup.meters defaultGraphicUnit = dataById[itemId].defaultGraphicUnit; } else if (type === 'group') { - label = dataById[itemId].name; + label = dataById[itemId]?.name; meterOrGroup = MeterOrGroup.groups - defaultGraphicUnit = dataById[itemId].defaultGraphicUnit; + defaultGraphicUnit = dataById[itemId]?.defaultGraphicUnit; } // TODO This is a bit of a hack. When an admin logs in they may not have the new state so label is null. // This should clear once the state is loaded. @@ -518,8 +518,8 @@ export function getSelectOptionsByItem( } as SelectOption ); }); - const sortedCompatibleOptions = _.sortBy(compatibleItemOptions, item => item.label.toLowerCase(), 'asc') - const sortedIncompatibleOptions = _.sortBy(incompatibleItemOptions, item => item.label.toLowerCase(), 'asc') + const sortedCompatibleOptions = _.sortBy(compatibleItemOptions, item => item.label?.toLowerCase(), 'asc') + const sortedIncompatibleOptions = _.sortBy(incompatibleItemOptions, item => item.label?.toLowerCase(), 'asc') return { compatible: sortedCompatibleOptions, incompatible: sortedIncompatibleOptions } diff --git a/src/client/app/styles/DateRangeCustom.css b/src/client/app/styles/DateRangeCustom.css new file mode 100644 index 000000000..30796fdeb --- /dev/null +++ b/src/client/app/styles/DateRangeCustom.css @@ -0,0 +1,12 @@ +/* not ideal, but fixes inconsistent width issue todo find better approach? */ +.react-daterange-picker__wrapper { + max-width: fit-content; +} + +.react-daterange-picker__inputGroup { + min-width: auto; +} + +.react-daterange-picker { + display: inline +} \ No newline at end of file From d30ff3da40f5fbe332772102bf35cf93d7931e1b Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Tue, 7 Nov 2023 15:56:36 +0000 Subject: [PATCH 035/131] Simplify Redux Store - Export component utilizes query data instead of redux state --- src/client/app/components/ExportComponent.tsx | 211 ++++++++---------- .../CreateConversionModalComponentWIP.tsx | 6 +- .../meters/CreateMeterModalComponentWIP.tsx | 9 +- .../meters/EditMeterModalComponentWIP.tsx | 8 +- src/client/app/reducers/index.ts | 12 - src/client/app/redux/api/metersApi.ts | 5 + src/client/app/redux/api/unitsApi.ts | 30 ++- .../app/redux/selectors/adminSelectors.ts | 43 ++-- 8 files changed, 150 insertions(+), 174 deletions(-) diff --git a/src/client/app/components/ExportComponent.tsx b/src/client/app/components/ExportComponent.tsx index c21048733..f25cfe652 100644 --- a/src/client/app/components/ExportComponent.tsx +++ b/src/client/app/components/ExportComponent.tsx @@ -6,10 +6,13 @@ import * as _ from 'lodash'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { Button } from 'reactstrap'; +import { selectConversionsDetails } from '../redux/api/conversionsApi'; import { selectGroupDataById } from '../redux/api/groupsApi'; import { selectMeterDataById } from '../redux/api/metersApi'; +import { readingsApi } from '../redux/api/readingsApi'; import { selectUnitDataById } from '../redux/api/unitsApi'; import { useAppSelector } from '../redux/hooks'; +import { selectChartQueryArgs } from '../redux/selectors/dataSelectors'; import { UserRole } from '../types/items'; import { ConversionData } from '../types/redux/conversions'; import { ChartTypes, MeterOrGroup } from '../types/redux/graph'; @@ -20,7 +23,6 @@ import { barUnitLabel, lineUnitLabel } from '../utils/graphics'; import { hasToken } from '../utils/token'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; -import { selectConversionsDetails } from '../redux/api/conversionsApi'; /** * Creates export buttons and does code for handling export to CSV files. @@ -39,13 +41,18 @@ export default function ExportComponent() { const graphState = useAppSelector(state => state.graph); // admin state const adminState = useAppSelector(state => state.admin); - // readings state - const readingsState = useAppSelector(state => state.readings); // error bar state const errorBarState = useAppSelector(state => state.graph.showMinMax); // Time range of graphic const timeInterval = graphState.queryTimeInterval; + const queryArgs = useAppSelector(selectChartQueryArgs) + + const { data: lineMeterReadings = {}, isFetching: lineMeterIsFetching } = readingsApi.endpoints.line.useQueryState(queryArgs.line.meterArgs); + const { data: lineGroupReadings = {}, isFetching: groupMeterIsFetching } = readingsApi.endpoints.line.useQueryState(queryArgs.line.groupsArgs); + const { data: barMeterReadings = {}, isFetching: barMeterIsFetching } = readingsApi.endpoints.line.useQueryState(queryArgs.bar.meterArgs); + const { data: barGroupReadings = {}, isFetching: barGroupIsFetching } = readingsApi.endpoints.line.useQueryState(queryArgs.bar.groupsArgs); + // Function to export the data in a graph. const exportGraphReading = () => { // What unit is being graphed. Unit of all lines to export. @@ -54,7 +61,7 @@ export default function ExportComponent() { const unitIdentifier = unitsDataById[unitId].identifier; // What type of chart/graphic is being displayed. const chartName = graphState.chartToRender; - if (chartName === ChartTypes.line) { + if (chartName === ChartTypes.line && !lineMeterIsFetching) { // Exporting a line chart // Get the full y-axis unit label for a line const returned = lineUnitLabel(unitsDataById[unitId], graphState.lineGraphRate, graphState.areaNormalization, graphState.selectedAreaUnit); @@ -67,32 +74,25 @@ export default function ExportComponent() { // export if area normalization is off or the meter can be normalized if (!graphState.areaNormalization || (meterArea > 0 && metersDataById[meterId].areaUnit !== AreaUnitType.none)) { // Line readings data for this meter. - const byMeterID = readingsState.line.byMeterID[meterId]; + // Get the readings for the time range and unit graphed + const readingsData = lineMeterReadings[meterId]; // Make sure it exists in case state is not there yet. - if (byMeterID !== undefined) { - // Convert the meter area into the proper unit if normalizing by area or use 1 if not so won't change reading values. - const areaScaling = graphState.areaNormalization ? - meterArea * getAreaUnitConversion(metersDataById[meterId].areaUnit, graphState.selectedAreaUnit) : 1; - // Divide areaScaling into the rate so have complete scaling factor for readings. - const scaling = rateScaling / areaScaling; - // Get the readings for the time range and unit graphed - const byTimeInterval = byMeterID[timeInterval.toString()]; - if (byTimeInterval !== undefined) { - const readingsData = byTimeInterval[unitId]; - // Make sure they are there and not being fetched. - if (readingsData !== undefined && !readingsData.isFetching) { - if (readingsData.readings === undefined) { - throw new Error(`Unacceptable condition: readingsData.readings is undefined for meter ${meterId}.`); - } - // Get the readings from the state. - const readings = _.values(readingsData.readings); - // Sort by start timestamp. - const sortedReadings = _.sortBy(readings, item => item.startTimestamp, 'asc'); - // Identifier for current meter. - const meterIdentifier = metersDataById[meterId].identifier; - graphExport(sortedReadings, meterIdentifier, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.meter, errorBarState); - } - } + // Convert the meter area into the proper unit if normalizing by area or use 1 if not so won't change reading values. + const areaScaling = graphState.areaNormalization ? + meterArea * getAreaUnitConversion(metersDataById[meterId].areaUnit, graphState.selectedAreaUnit) : 1; + // Divide areaScaling into the rate so have complete scaling factor for readings. + const scaling = rateScaling / areaScaling; + // Make sure they are there and not being fetched. + if (readingsData) { + // Get the readings from the state. + const readings = _.values(readingsData); + // Sort by start timestamp. + const sortedReadings = _.sortBy(readings, item => item.startTimestamp, 'asc'); + // Identifier for current meter. + const meterIdentifier = metersDataById[meterId].identifier; + graphExport(sortedReadings, meterIdentifier, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.meter, errorBarState); + } else { + throw new Error(`Unacceptable condition: readingsData.readings is undefined for meter ${meterId}.`); } } } @@ -101,34 +101,26 @@ export default function ExportComponent() { const groupArea = groupsDataById[groupId].area; // export if area normalization is off or the group can be normalized if (!graphState.areaNormalization || (groupArea > 0 && groupsDataById[groupId].areaUnit !== AreaUnitType.none)) { - // Line readings data for this group. - const byGroupID = readingsState.line.byGroupID[groupId]; - // Make sure it exists in case state is not there yet. - if (byGroupID !== undefined) { - // Convert the group area into the proper unit if normalizing by area or use 1 if not so won't change reading values. - const areaScaling = graphState.areaNormalization ? - groupArea * getAreaUnitConversion(groupsDataById[groupId].areaUnit, graphState.selectedAreaUnit) : 1; - // Divide areaScaling into the rate so have complete scaling factor for readings. - const scaling = rateScaling / areaScaling; + // Convert the group area into the proper unit if normalizing by area or use 1 if not so won't change reading values. + const areaScaling = graphState.areaNormalization ? + groupArea * getAreaUnitConversion(groupsDataById[groupId].areaUnit, graphState.selectedAreaUnit) : 1; + // Divide areaScaling into the rate so have complete scaling factor for readings. + const scaling = rateScaling / areaScaling; - // Get the readings for the time range and unit graphed - const byTimeInterval = byGroupID[timeInterval.toString()]; - if (byTimeInterval !== undefined) { - const readingsData = byTimeInterval[unitId]; - // Make sure they are there and not being fetched. - if (readingsData !== undefined && !readingsData.isFetching) { - if (readingsData.readings === undefined) { - throw new Error(`Unacceptable condition: readingsData.readings is undefined for group ${groupId}.`); - } - // Get the readings from the state. - const readings = _.values(readingsData.readings); - // Sort by start timestamp. - const sortedReadings = _.sortBy(readings, item => item.startTimestamp, 'asc'); - // Identifier for current group. - const groupName = groupsDataById[groupId].name; - graphExport(sortedReadings, groupName, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.group); - } - } + + // Line readings data for this group. + const readingsData = lineGroupReadings[groupId] + // Make sure they are there and not being fetched. + if (readingsData && !groupMeterIsFetching) { + // Get the readings from the state. + const readings = _.values(readingsData); + // Sort by start timestamp. + const sortedReadings = _.sortBy(readings, item => item.startTimestamp, 'asc'); + // Identifier for current group. + const groupName = groupsDataById[groupId].name; + graphExport(sortedReadings, groupName, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.group); + } else { + throw new Error(`Unacceptable condition: readingsData.readings is undefined for group ${groupId}.`); } } } @@ -136,43 +128,30 @@ export default function ExportComponent() { // Exporting a bar chart // Get the full y-axis unit label for a bar const unitLabel = barUnitLabel(unitsDataById[unitId], graphState.areaNormalization, graphState.selectedAreaUnit); - // Time width of the bars - const barDuration = graphState.barDuration; // Loop over the displayed meters and export one-by-one. Does nothing if no meters selected. for (const meterId of graphState.selectedMeters) { // export if area normalization is off or the meter can be normalized if (!graphState.areaNormalization || (metersDataById[meterId].area > 0 && metersDataById[meterId].areaUnit !== AreaUnitType.none)) { - // Bar readings data for this meter. - const byMeterID = readingsState.bar.byMeterID[meterId]; - // Make sure it exists in case state is not there yet. - if (byMeterID !== undefined) { - // No scaling if areaNormalization is not enabled - let scaling = 1; - if (graphState.areaNormalization) { - // convert the meter area into the proper unit, if needed - scaling *= getAreaUnitConversion(metersDataById[meterId].areaUnit, graphState.selectedAreaUnit); - } - const byTimeInterval = byMeterID[timeInterval.toString()]; - if (byTimeInterval !== undefined) { - const byBarDuration = byTimeInterval[barDuration.toISOString()]; - if (byBarDuration !== undefined) { - // Get the readings for the time range and unit graphed - const readingsData = byBarDuration[unitId]; - // Make sure they are there and not being fetched. - if (readingsData !== undefined && !readingsData.isFetching) { - if (readingsData.readings === undefined) { - throw new Error(`Unacceptable condition: readingsData.readings is undefined for meter ${meterId}.`); - } - // Get the readings from the state. - const readings = _.values(readingsData.readings); - // Sort by start timestamp. - const sortedReadings = _.sortBy(readings, item => item.startTimestamp, 'asc'); - // Identifier for current meter. - const meterIdentifier = metersDataById[meterId].identifier; - graphExport(sortedReadings, meterIdentifier, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.meter); - } - } - } + // No scaling if areaNormalization is not enabled + let scaling = 1; + if (graphState.areaNormalization) { + // convert the meter area into the proper unit, if needed + scaling *= getAreaUnitConversion(metersDataById[meterId].areaUnit, graphState.selectedAreaUnit); + } + // Get the readings for the time range and unit graphed + const readingsData = barMeterReadings[meterId]; + // Make sure they are there and not being fetched. + if (readingsData && !barMeterIsFetching) { + + // Get the readings from the state. + const readings = _.values(readingsData); + // Sort by start timestamp. + const sortedReadings = _.sortBy(readings, item => item.startTimestamp, 'asc'); + // Identifier for current meter. + const meterIdentifier = metersDataById[meterId].identifier; + graphExport(sortedReadings, meterIdentifier, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.meter); + } else if (!readingsData && !barMeterIsFetching) { + throw new Error(`Unacceptable condition: readingsData.readings is undefined for meter ${meterId}.`); } } } @@ -181,36 +160,26 @@ export default function ExportComponent() { // export if area normalization is off or the group can be normalized if (!graphState.areaNormalization || (groupsDataById[groupId].area > 0 && groupsDataById[groupId].areaUnit !== AreaUnitType.none)) { // Bar readings data for this group. - const byGroupID = readingsState.bar.byGroupID[groupId]; - // Make sure it exists in case state is not there yet. - if (byGroupID !== undefined) { - // No scaling if areaNormalization is not enabled - let scaling = 1; - if (graphState.areaNormalization) { - // convert the meter area into the proper unit, if needed - scaling *= getAreaUnitConversion(groupsDataById[groupId].areaUnit, graphState.selectedAreaUnit); - } - const byTimeInterval = byGroupID[timeInterval.toString()]; - if (byTimeInterval !== undefined) { - const byBarDuration = byTimeInterval[barDuration.toISOString()]; - if (byBarDuration !== undefined) { - // Get the readings for the time range and unit graphed - const readingsData = byBarDuration[unitId]; - // Make sure they are there and not being fetched. - if (readingsData !== undefined && !readingsData.isFetching) { - if (readingsData.readings === undefined) { - throw new Error(`Unacceptable condition: readingsData.readings is undefined for group ${groupId}.`); - } - // Get the readings from the state. - const readings = _.values(readingsData.readings); - // Sort by start timestamp. - const sortedReadings = _.sortBy(readings, item => item.startTimestamp, 'asc'); - // Identifier for current group. - const groupName = groupsDataById[groupId].name; - graphExport(sortedReadings, groupName, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.group); - } - } - } + // No scaling if areaNormalization is not enabled + let scaling = 1; + if (graphState.areaNormalization) { + // convert the meter area into the proper unit, if needed + scaling *= getAreaUnitConversion(groupsDataById[groupId].areaUnit, graphState.selectedAreaUnit); + } + // Get the readings for the time range and unit graphed + const readingsData = barGroupReadings[groupId]; + // Make sure they are there and not being fetched. + if (readingsData && !barGroupIsFetching) { + + // Get the readings from the state. + const readings = _.values(readingsData); + // Sort by start timestamp. + const sortedReadings = _.sortBy(readings, item => item.startTimestamp, 'asc'); + // Identifier for current group. + const groupName = groupsDataById[groupId].name; + graphExport(sortedReadings, groupName, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.group); + } else if (!readingsData && !barGroupIsFetching) { + throw new Error(`Unacceptable condition: readingsData.readings is undefined for group ${groupId}.`); } } } @@ -318,6 +287,10 @@ export default function ExportComponent() { return ( <>
+ {/* + TODO conditionally disable button click if data for current graph is fetching. + TODO VERIFY Behavior with RTK migration + */} diff --git a/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx b/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx index 3b63bd01e..a4f54bbb3 100644 --- a/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx +++ b/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx @@ -57,11 +57,7 @@ export default function CreateConversionModalComponent() { // If the currently selected conversion is valid const [validConversion, reason] = useAppSelector( - state => selectIsValidConversion( - state, - conversionState.sourceId, - conversionState.destinationId, - conversionState.bidirectional) + state => selectIsValidConversion(state, conversionState) ) const handleStringChange = (e: React.ChangeEvent) => { diff --git a/src/client/app/components/meters/CreateMeterModalComponentWIP.tsx b/src/client/app/components/meters/CreateMeterModalComponentWIP.tsx index 2b4e1e6f6..b187cbb02 100644 --- a/src/client/app/components/meters/CreateMeterModalComponentWIP.tsx +++ b/src/client/app/components/meters/CreateMeterModalComponentWIP.tsx @@ -14,7 +14,7 @@ import { makeSelectGraphicUnitCompatibility } from '../../redux/selectors/adminS import '../../styles/modal.css'; import { tooltipBaseStyle } from '../../styles/modalStyle'; import { TrueFalseType } from '../../types/items'; -import { MeterTimeSortType, MeterType } from '../../types/redux/meters'; +import { MeterData, MeterTimeSortType, MeterType } from '../../types/redux/meters'; import { UnitData } from '../../types/redux/units'; import { GPSPoint, isValidGPSInput } from '../../utils/calibration'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; @@ -98,7 +98,8 @@ export default function CreateMeterModalComponent() { compatibleGraphicUnits, compatibleUnits, incompatibleUnits - } = useAppSelector(state => selectGraphicUnitCompatibility(state, meterDetails.unitId, meterDetails.defaultGraphicUnit)) + // Weird Type assertion due to conflicting GPS Property + } = useAppSelector(state => selectGraphicUnitCompatibility(state, meterDetails as unknown as MeterData)) const handleShow = () => setShowModal(true); const handleStringChange = (e: React.ChangeEvent) => { @@ -220,7 +221,7 @@ export default function CreateMeterModalComponent() { if (typeof gpsInput === 'string') { if (isValidGPSInput(gpsInput)) { // Clearly gpsInput is a string but TS complains about the split so cast. - const gpsValues = (gpsInput as string).split(',').map((value: string) => parseFloat(value)); + const gpsValues = gpsInput.split(',').map((value: string) => parseFloat(value)); // It is valid and needs to be in this format for routing. gps = { longitude: gpsValues[longitudeIndex], @@ -255,7 +256,7 @@ export default function CreateMeterModalComponent() { .catch(err => { // TODO Better way than popup with React but want to stay so user can read/copy. - window.alert(translate('meter.failed.to.create.meter') + '"' + err.response.data + '"'); + window.alert(translate('meter.failed.to.create.meter') + '"' + err.response.data + '"'); }) } else { // Tell user that not going to update due to input issues. diff --git a/src/client/app/components/meters/EditMeterModalComponentWIP.tsx b/src/client/app/components/meters/EditMeterModalComponentWIP.tsx index 2b5c86e8f..8b985cb74 100644 --- a/src/client/app/components/meters/EditMeterModalComponentWIP.tsx +++ b/src/client/app/components/meters/EditMeterModalComponentWIP.tsx @@ -10,9 +10,10 @@ import { FormattedMessage } from 'react-intl'; import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; -import { metersApi } from '../../redux/api/metersApi'; +import { metersApi, selectMeterDataWithID } from '../../redux/api/metersApi'; +import { selectUnitDataById } from '../../redux/api/unitsApi'; import { useAppDispatch, useAppSelector } from '../../redux/hooks'; -import { makeSelectGraphicUnitCompatibility, selectMeterDataWithID } from '../../redux/selectors/adminSelectors'; +import { makeSelectGraphicUnitCompatibility } from '../../redux/selectors/adminSelectors'; import '../../styles/modal.css'; import { tooltipBaseStyle } from '../../styles/modalStyle'; import { TrueFalseType } from '../../types/items'; @@ -24,7 +25,6 @@ import { getGPSString, notifyUser, nullToEmptyString } from '../../utils/input'; import translate from '../../utils/translate'; import TimeZoneSelect from '../TimeZoneSelect'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import { selectUnitDataById } from '../../redux/api/unitsApi'; interface EditMeterModalComponentProps { show: boolean; @@ -51,7 +51,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr incompatibleGraphicUnits, compatibleUnits, incompatibleUnits - } = useAppSelector(state => selectGraphicUnitCompatibility(state, localMeterEdits.unitId, localMeterEdits.defaultGraphicUnit)) + } = useAppSelector(state => selectGraphicUnitCompatibility(state, localMeterEdits)) useEffect(() => { setLocalMeterEdits(_.cloneDeep(meterState)) }, [meterState]) /* State */ diff --git a/src/client/app/reducers/index.ts b/src/client/app/reducers/index.ts index 56de29b22..42fea4214 100644 --- a/src/client/app/reducers/index.ts +++ b/src/client/app/reducers/index.ts @@ -3,8 +3,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { combineReducers } from 'redux'; -import lineReadings from './lineReadings'; -import barReadings from './barReadings'; import compareReadings from './compareReadings'; import maps from './maps'; import { adminSlice } from './admin'; @@ -15,27 +13,17 @@ import { conversionsSlice } from './conversions'; import { optionsSlice } from './options'; import { baseApi } from '../redux/api/baseApi'; import { graphSlice } from './graph'; -// removing these in favor of api reducers -// import { metersSlice } from './meters'; -// import { groupsSlice } from './groups'; -// import { unitsSlice } from './units'; - export const rootReducer = combineReducers({ readings: combineReducers({ - line: lineReadings, - bar: barReadings, compare: compareReadings }), graph: graphSlice.reducer, maps, - // meters: metersSlice.reducer, - // groups: groupsSlice.reducer, admin: adminSlice.reducer, version: versionSlice.reducer, currentUser: currentUserSlice.reducer, unsavedWarning: unsavedWarningSlice.reducer, - // units: unitsSlice.reducer, conversions: conversionsSlice.reducer, options: optionsSlice.reducer, // RTK Query's Derived Reducers diff --git a/src/client/app/redux/api/metersApi.ts b/src/client/app/redux/api/metersApi.ts index 213bc2ec5..bfaa973fc 100644 --- a/src/client/app/redux/api/metersApi.ts +++ b/src/client/app/redux/api/metersApi.ts @@ -6,6 +6,7 @@ import { baseApi } from './baseApi'; import { NamedIDItem } from 'types/items'; import { CompareReadings, RawReadings } from 'types/readings'; import { conversionsApi } from './conversionsApi'; +import { RootState } from '../../store'; export const metersApi = baseApi.injectEndpoints({ @@ -76,3 +77,7 @@ export const metersApi = baseApi.injectEndpoints({ }) export const selectMeterDataById = metersApi.endpoints.getMeters.select() +export const selectMeterDataWithID = (state: RootState, meterID: number) => { + const { data: meterDataByID = {} } = selectMeterDataById(state) + return meterDataByID[meterID] +} \ No newline at end of file diff --git a/src/client/app/redux/api/unitsApi.ts b/src/client/app/redux/api/unitsApi.ts index 51aa537a3..acf8a3310 100644 --- a/src/client/app/redux/api/unitsApi.ts +++ b/src/client/app/redux/api/unitsApi.ts @@ -1,4 +1,5 @@ -import * as _ from 'lodash' +import * as _ from 'lodash'; +import { RootState } from 'store'; import { UnitData, UnitDataById } from '../../types/redux/units'; import { baseApi } from './baseApi'; @@ -28,4 +29,29 @@ export const unitsApi = baseApi.injectEndpoints({ }) }) -export const selectUnitDataById = unitsApi.endpoints.getUnitsDetails.select() \ No newline at end of file +/** + * Selects the most recent query status + * @param state - The complete state of the redux store. + * @returns The unit data corresponding to the `unitID` if found, or undefined if not. + * @example + * + * const { data: unitDataById = {} } = useAppSelector(state =>selectUnitDataById(state)) + * const { data: unitDataById = {} } = useAppSelector(selectUnitDataById) + */ +export const selectUnitDataById = unitsApi.endpoints.getUnitsDetails.select() + +/** + * Selects a unit from the state by its unique identifier. + * @param state - The complete state of the redux store. + * @param unitID - The unique identifier for the unit to be retrieved. + * @returns The unit data corresponding to the `unitID` if found, or undefined if not. + * @example + * + * // Get Unit Data for unit with ID of '1' + * const unit = useAppSelector(state => selectUnitWithID(state, 1)) + */ +export const selectUnitWithID = (state: RootState, unitID: number) => { + const { data: unitDataById = {} } = selectUnitDataById(state) + return unitDataById[unitID] + +} diff --git a/src/client/app/redux/selectors/adminSelectors.ts b/src/client/app/redux/selectors/adminSelectors.ts index c72f6bb4d..594655bb1 100644 --- a/src/client/app/redux/selectors/adminSelectors.ts +++ b/src/client/app/redux/selectors/adminSelectors.ts @@ -2,18 +2,20 @@ import { createSelector } from '@reduxjs/toolkit' import * as _ from 'lodash' import { selectAdminState } from '../../reducers/admin' import { selectConversionsDetails } from '../../redux/api/conversionsApi' -import { selectMeterDataById } from '../../redux/api/metersApi' +import { selectMeterDataWithID } from '../../redux/api/metersApi' import { RootState } from '../../store' import { PreferenceRequestItem } from '../../types/items' import { UnitData, UnitType } from '../../types/redux/units' import { unitsCompatibleWithUnit } from '../../utils/determineCompatibleUnits' import { noUnitTranslated, potentialGraphicUnits } from '../../utils/input' -import { selectUnitDataById } from '../api/unitsApi' import translate from '../../utils/translate' +import { selectUnitDataById } from '../api/unitsApi' +import { MeterData } from 'types/redux/meters' +import { ConversionData } from 'types/redux/conversions' export const selectAdminPreferences = createSelector( selectAdminState, - adminState => ({ + (adminState): PreferenceRequestItem => ({ displayTitle: adminState.displayTitle, defaultChartToRender: adminState.defaultChartToRender, defaultBarStacking: adminState.defaultBarStacking, @@ -31,7 +33,7 @@ export const selectAdminPreferences = createSelector( defaultMeterReadingGap: adminState.defaultMeterReadingGap, defaultMeterMaximumErrors: adminState.defaultMeterMaximumErrors, defaultMeterDisableChecks: adminState.defaultMeterDisableChecks - } as PreferenceRequestItem) + }) ) @@ -46,6 +48,7 @@ export const selectPossibleGraphicUnits = createSelector( return potentialGraphicUnits(unitDataById) } ) + /** * Calculates the set of all possible meter units for a meter. * This is any unit that is of type unit or suffix. @@ -68,24 +71,13 @@ export const selectPossibleMeterUnits = createSelector( } ) - -export const selectMeterDataWithID = (state: RootState, meterID: number) => { - const { data: meterDataByID = {} } = selectMeterDataById(state) - return meterDataByID[meterID] -} -export const selectUnitWithID = (state: RootState, unitID: number) => { - const { data: unitDataById = {} } = selectMeterDataById(state) - return unitDataById[unitID] - -} - /** * Selector that returns a unit associated with a meter given an meterID * @param {RootState} state redux global state * @param {number} id redux global state * @returns {string} Unit Name. * @example - * useAppSelector(state => selectUnitName(state, 42)) + * const unitName = useAppSelector(state => selectUnitName(state, 42)) */ export const selectUnitName = createSelector( // This is the unit associated with the meter. @@ -132,14 +124,13 @@ export const selectGraphicName = createSelector( * useAppSelector(state => selectGraphicUnitCompatibility(state, localMeterEdits.unitId, localMeterEdits.defaultGraphicUnit)) */ export const makeSelectGraphicUnitCompatibility = () => { + // 3rd/4th callback used to pass in non-state value in this case the local edits. + // two separate call backs so their return values will pass a === equality check for memoized behavior const selectGraphicUnitCompatibilityInstance = createSelector( selectPossibleGraphicUnits, selectPossibleMeterUnits, - // 3rd/4th callback used to pass in non-state value in this case the local edits. - // two separate call backs so their return values will pass a === equality check for memoized behavior - (state: RootState, unitId: number) => unitId, - (state: RootState, unitId: number, defaultGraphicUnit: number) => defaultGraphicUnit, - (possibleGraphicUnits, possibleMeterUnits, unitId, defaultGraphicUnit) => { + (_state: RootState, meterDetails: MeterData) => meterDetails, + (possibleGraphicUnits, possibleMeterUnits, { unitId, defaultGraphicUnit }) => { // Graphic units compatible with currently selected unit const compatibleGraphicUnits = new Set(); // Graphic units incompatible with currently selected unit @@ -206,18 +197,14 @@ export const makeSelectGraphicUnitCompatibility = () => { /** * Checks if conversion is valid * @param state redux store RootState - * @param sourceId New conversion sourceId - * @param destinationId New conversion destinationId - * @param bidirectional New conversion bidirectional status + * @param conversionData ConversionState Data * @returns boolean representing if new conversion is valid or not */ export const selectIsValidConversion = createSelector( selectUnitDataById, selectConversionsDetails, - (_state: RootState, sourceId: number) => sourceId, - (_state: RootState, _sourceId: number, destinationId: number) => destinationId, - (_state: RootState, _sourceId: number, _destinationId: number, bidirectional: boolean) => bidirectional, - ({ data: unitDataById = {} }, { data: conversionData = [] }, sourceId, destinationId, bidirectional): [boolean, string] => { + (_state: RootState, conversionData: ConversionData) => conversionData, + ({ data: unitDataById = {} }, { data: conversionData = [] }, { sourceId, destinationId, bidirectional }): [boolean, string] => { /* Create Conversion Validation: Source equals destination: invalid conversion Conversion exists: invalid conversion From 78004ab49338342cc305a638b663985298d9bab1 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Wed, 8 Nov 2023 01:51:57 +0000 Subject: [PATCH 036/131] Convert Compare to RTKQuery - Incrementally Delete No-Longer Needed Query Thunks --- src/client/app/actions/barReadings.ts | 191 ----- src/client/app/actions/compareReadings.ts | 188 ----- src/client/app/actions/graph.ts | 174 ---- src/client/app/actions/lineReadings.ts | 162 ---- src/client/app/actions/mapReadings.ts | 26 +- .../components/ChartDataSelectComponent.tsx | 741 +---------------- .../ChartDataSelectComponentSave.tsx | 751 ++++++++++++++++++ .../ChartDataSelectComponentWIP.tsx | 23 - .../app/components/DashboardComponent.tsx | 6 +- src/client/app/components/ExportComponent.tsx | 8 +- .../app/components/MenuModalComponent.tsx | 10 +- .../components/MultiCompareChartComponent.tsx | 3 +- .../MultiCompareChartComponentWIP.tsx | 188 +++++ src/client/app/components/RouteComponent.tsx | 4 +- .../app/components/UIOptionsComponent.tsx | 4 +- .../conversion/ConversionsDetailComponent.tsx | 23 +- .../meters/EditMeterModalComponentWIP.tsx | 2 +- .../components/unit/UnitsDetailComponent.tsx | 7 +- .../app/containers/CompareChartContainer.ts | 31 +- .../app/containers/DashboardContainer.ts | 38 - src/client/app/containers/LoginContainer.tsx | 21 - .../containers/MultiCompareChartContainer.ts | 201 ----- src/client/app/containers/RouteContainer.ts | 45 -- .../app/containers/UIOptionsContainer.ts | 39 - src/client/app/reducers/graph.ts | 6 +- src/client/app/reducers/index.ts | 6 +- src/client/app/redux/api/groupsApi.ts | 12 +- src/client/app/redux/api/metersApi.ts | 78 +- src/client/app/redux/api/readingsApi.ts | 42 +- .../app/redux/selectors/adminSelectors.ts | 4 +- .../app/redux/selectors/dataSelectors.ts | 50 +- src/client/app/store.ts | 6 +- src/client/app/translations/data.ts | 4 +- src/client/app/types/readings.ts | 21 +- src/client/app/types/redux/graph.ts | 5 - src/client/app/types/redux/groups.ts | 5 +- src/client/app/types/redux/map.ts | 11 +- src/client/app/types/redux/meters.ts | 4 +- src/client/app/types/redux/units.ts | 8 +- 39 files changed, 1192 insertions(+), 1956 deletions(-) delete mode 100644 src/client/app/actions/barReadings.ts delete mode 100644 src/client/app/actions/compareReadings.ts delete mode 100644 src/client/app/actions/lineReadings.ts create mode 100644 src/client/app/components/ChartDataSelectComponentSave.tsx delete mode 100644 src/client/app/components/ChartDataSelectComponentWIP.tsx create mode 100644 src/client/app/components/MultiCompareChartComponentWIP.tsx delete mode 100644 src/client/app/containers/DashboardContainer.ts delete mode 100644 src/client/app/containers/LoginContainer.tsx delete mode 100644 src/client/app/containers/MultiCompareChartContainer.ts delete mode 100644 src/client/app/containers/RouteContainer.ts delete mode 100644 src/client/app/containers/UIOptionsContainer.ts diff --git a/src/client/app/actions/barReadings.ts b/src/client/app/actions/barReadings.ts deleted file mode 100644 index d866ee1ed..000000000 --- a/src/client/app/actions/barReadings.ts +++ /dev/null @@ -1,191 +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 * as moment from 'moment'; -import { TimeInterval } from '../../../common/TimeInterval'; -import { Dispatch, GetState, Thunk, ActionType } from '../types/redux/actions'; -import { State } from '../types/redux/state'; -import * as t from '../types/redux/barReadings'; -import { readingsApi } from '../utils/api'; -import { BarReadings } from '../types/readings'; - -/** - * @param state the Redux state - * @param meterID the ID of the meter to check - * @param timeInterval the interval over which to check - * @param barDuration the duration of each bar for which to check - * @param unitID the ID of the unit for which to check - * @returns True if the readings for the given meter, time duration, bar length and unit are missing; false otherwise. - */ -export function shouldFetchMeterBarReadings(state: State, meterID: number, timeInterval: TimeInterval, - barDuration: moment.Duration, unitID: number): boolean { - const timeIntervalIndex = timeInterval.toString(); - const barDurationIndex = barDuration.toISOString(); - - const readingsForID = state.readings.bar.byMeterID[meterID]; - if (readingsForID === undefined) { - return true; - } - - const readingsForTimeInterval = readingsForID[timeIntervalIndex]; - if (readingsForTimeInterval === undefined) { - return true; - } - - const readingsForBarDuration = readingsForTimeInterval[barDurationIndex]; - if (readingsForBarDuration === undefined) { - return true; - } - - const readingsForUnit = readingsForBarDuration[unitID]; - if (readingsForUnit === undefined) { - return true; - } - - return !readingsForUnit.isFetching; -} - -/** - * @param state the Redux state - * @param groupID the ID of the group to check - * @param timeInterval the interval over which to check - * @param barDuration the duration of each bar for which to check - * @param unitID the ID of the unit for which to check - * @returns True if the readings for the given group, time duration, bar length and unit are missing; false otherwise. - */ -export function shouldFetchGroupBarReadings(state: State, groupID: number, timeInterval: TimeInterval, - barDuration: moment.Duration, unitID: number): boolean { - const timeIntervalIndex = timeInterval.toString(); - const barDurationIndex = barDuration.toISOString(); - - const readingsForID = state.readings.bar.byGroupID[groupID]; - if (readingsForID === undefined) { - return true; - } - - const readingsForTimeInterval = readingsForID[timeIntervalIndex]; - if (readingsForTimeInterval === undefined) { - return true; - } - - const readingsForBarDuration = readingsForTimeInterval[barDurationIndex]; - if (readingsForBarDuration === undefined) { - return true; - } - - const readingsForUnit = readingsForBarDuration[unitID]; - if (readingsForUnit === undefined) { - return true; - } - - return !readingsForUnit.isFetching; -} - - -/** - * @param meterIDs the IDs of the meters to get readings - * @param timeInterval the interval over which to check - * @param barDuration the duration of each bar for which to check - * @param unitID the ID of the unit for which to check - */ -export function requestMeterBarReadings(meterIDs: number[], timeInterval: TimeInterval, barDuration: moment.Duration, - unitID: number): t.RequestMeterBarReadingsAction { - return { type: ActionType.RequestMeterBarReadings, meterIDs, timeInterval, barDuration, unitID }; -} - -/** - * @param groupIDs the IDs of the groups to get readings - * @param timeInterval the interval over which to check - * @param barDuration the duration of each bar for which to check - * @param unitID the ID of the unit for which to check - */ -export function requestGroupBarReadings(groupIDs: number[], timeInterval: TimeInterval, barDuration: moment.Duration, - unitID: number): t.RequestGroupBarReadingsAction { - return { type: ActionType.RequestGroupBarReadings, groupIDs, timeInterval, barDuration, unitID }; -} - -/** - * @param meterIDs the IDs of the meters to get readings - * @param timeInterval the interval over which to check - * @param barDuration the duration of each bar for which to check - * @param unitID the ID of the unit for which to check - * @param readings the readings for the given meters - */ -export function receiveMeterBarReadings(meterIDs: number[], timeInterval: TimeInterval, barDuration: moment.Duration, - unitID: number, readings: BarReadings): t.ReceiveMeterBarReadingsAction { - return { type: ActionType.ReceiveMeterBarReadings, meterIDs, timeInterval, unitID, barDuration, readings }; -} - -/** - * @param groupIDs the IDs of the groups to get readings - * @param timeInterval the interval over which to check - * @param barDuration the duration of each bar for which to check - * @param unitID the ID of the unit for which to check - * @param readings the readings for the given groups - */ -export function receiveGroupBarReadings(groupIDs: number[], timeInterval: TimeInterval, barDuration: moment.Duration, - unitID: number, readings: BarReadings): t.ReceiveGroupBarReadingsAction { - return { type: ActionType.ReceiveGroupBarReadings, groupIDs, timeInterval, barDuration, unitID, readings }; -} - -/** - * @param meterIDs the IDs of the meters to get readings - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - */ -function fetchMeterBarReadings(meterIDs: number[], timeInterval: TimeInterval, unitID: number): Thunk { - return async (dispatch: Dispatch, getState: GetState) => { - const barDuration = getState().graph.barDuration; - dispatch(requestMeterBarReadings(meterIDs, timeInterval, barDuration, unitID)); - const meterBarReadings = await readingsApi.meterBarReadings(meterIDs, timeInterval, Math.round(barDuration.asDays()), unitID); - dispatch(receiveMeterBarReadings(meterIDs, timeInterval, barDuration, unitID, meterBarReadings)); - }; -} - -/** - * @param groupIDs the IDs of the groups to get readings - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - */ -function fetchGroupBarReadings(groupIDs: number[], timeInterval: TimeInterval, unitID: number): Thunk { - return async (dispatch: Dispatch, getState: GetState) => { - const barDuration = getState().graph.barDuration; - dispatch(requestGroupBarReadings(groupIDs, timeInterval, barDuration, unitID)); - const groupBarReadings = await readingsApi.groupBarReadings(groupIDs, timeInterval, Math.round(barDuration.asDays()), unitID); - dispatch(receiveGroupBarReadings(groupIDs, timeInterval, barDuration, unitID, groupBarReadings)); - }; -} - -/** - * Fetches readings for the bar chart of all selected meters and groups, if needed. - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - */ -export function fetchNeededBarReadings(timeInterval: TimeInterval, unitID: number): Thunk { - return (dispatch: Dispatch, getState: GetState) => { - const state = getState(); - const promises: Array> = []; - /* tslint:enable:array-type */ - const barDuration = state.graph.barDuration; - - // Determine which meters are missing data for this time interval - const meterIDsToFetchForBar = state.graph.selectedMeters.filter( - id => shouldFetchMeterBarReadings(state, id, timeInterval, barDuration, unitID) - ); - // Fetch data for any missing meters - if (meterIDsToFetchForBar.length > 0) { - promises.push(dispatch(fetchMeterBarReadings(meterIDsToFetchForBar, timeInterval, unitID))); - } - - // Determine which groups are missing data for this time interval - const groupIDsToFetchForBar = state.graph.selectedGroups.filter( - id => shouldFetchGroupBarReadings(state, id, timeInterval, barDuration, unitID) - ); - // Fetch data for any missing groups - if (groupIDsToFetchForBar.length > 0) { - promises.push(dispatch(fetchGroupBarReadings(groupIDsToFetchForBar, timeInterval, unitID))); - } - return Promise.all(promises); - }; -} diff --git a/src/client/app/actions/compareReadings.ts b/src/client/app/actions/compareReadings.ts deleted file mode 100644 index a77e5bb49..000000000 --- a/src/client/app/actions/compareReadings.ts +++ /dev/null @@ -1,188 +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 * as moment from 'moment'; -import { TimeInterval } from '../../../common/TimeInterval'; -import { Dispatch, Thunk, ActionType, GetState } from '../types/redux/actions'; -import { State } from '../types/redux/state'; -import { CompareReadings } from '../types/readings'; -import * as t from '../types/redux/compareReadings'; -import { metersApi, groupsApi } from '../utils/api'; -import { ComparePeriod, calculateCompareShift } from '../utils/calculateCompare'; - -/** - * @param state the Redux state - * @param meterID the ID of the meter to check - * @param timeInterval the interval over which to check - * @param compareShift The time shift between curr and prev - * @param unitID the ID of the unit for which to check - * @returns True if the readings for the given meter, time and unit are missing; false otherwise. - */ -function shouldFetchMeterCompareReadings(state: State, meterID: number, timeInterval: TimeInterval, - compareShift: moment.Duration, unitID: number): boolean { - const readingsForID = state.readings.compare.byMeterID[meterID]; - if (readingsForID === undefined) { - return true; - } - const readingsForTimeInterval = readingsForID[timeInterval.toString()]; - if (readingsForTimeInterval === undefined) { - return true; - } - const readingsForCompareShift = readingsForTimeInterval[compareShift.toISOString()]; - if (readingsForCompareShift === undefined) { - return true; - } - - const readingsForUnit = readingsForCompareShift[unitID]; - if (readingsForUnit === undefined) { - return true; - } - - return !readingsForUnit.isFetching; -} - -/** - * @param state the Redux state - * @param groupID the ID of the group to check - * @param timeInterval the interval over which to check - * @param compareShift The time shift between curr and prev - * @param unitID the ID of the unit for which to check - * @returns True if the readings for the given group, and time are missing; false otherwise. - */ -function shouldFetchGroupCompareReadings(state: State, groupID: number, timeInterval: TimeInterval, - compareShift: moment.Duration, unitID: number): boolean { - const readingsForID = state.readings.compare.byGroupID[groupID]; - if (readingsForID === undefined) { - return true; - } - const readingsForTimeInterval = readingsForID[timeInterval.toString()]; - if (readingsForTimeInterval === undefined) { - return true; - } - const readingsForCompareShift = readingsForTimeInterval[compareShift.toISOString()]; - if (readingsForCompareShift === undefined) { - return true; - } - - const readingsForUnit = readingsForCompareShift[unitID]; - if (readingsForUnit === undefined) { - return true; - } - - return !readingsForUnit.isFetching; -} - -/** - * @param meterIDs the IDs of the meters to get readings - * @param timeInterval the interval over which to check - * @param compareShift time to shift the timeInterval to get previous interval - * @param unitID the ID of the unit for which to check - */ -function requestMeterCompareReadings(meterIDs: number[], timeInterval: TimeInterval, - compareShift: moment.Duration, unitID: number): - t.RequestMeterCompareReadingsAction { - return { type: ActionType.RequestMeterCompareReadings, meterIDs, timeInterval, compareShift, unitID }; -} - -/** - * @param groupIDs the IDs of the groups to get readings - * @param timeInterval the interval over which to check - * @param compareShift time to shift the timeInterval to get previous interval - * @param unitID the ID of the unit for which to check - */ -function requestGroupCompareReadings(groupIDs: number[], timeInterval: TimeInterval, - compareShift: moment.Duration, unitID: number): - t.RequestGroupCompareReadingsAction { - return { type: ActionType.RequestGroupCompareReadings, groupIDs, timeInterval, compareShift, unitID }; -} - -/** - * @param meterIDs the IDs of the meters to get readings - * @param timeInterval the interval over which to check - * @param compareShift time to shift the timeInterval to get previous interval - * @param unitID the ID of the unit for which to check - * @param readings the readings for the given meters - */ -function receiveMeterCompareReadings(meterIDs: number[], timeInterval: TimeInterval, compareShift: moment.Duration, - unitID: number, readings: CompareReadings): t.ReceiveMeterCompareReadingsAction { - return { type: ActionType.ReceiveMeterCompareReadings, meterIDs, timeInterval, compareShift, unitID, readings }; -} - -/** - * @param groupIDs the IDs of the groups to get readings - * @param timeInterval the interval over which to check - * @param compareShift time to shift the timeInterval to get previous interval - * @param unitID the ID of the unit for which to check - * @param readings the readings for the given meters - */ -function receiveGroupCompareReadings(groupIDs: number[], timeInterval: TimeInterval, compareShift: moment.Duration, - unitID: number, readings: CompareReadings): t.ReceiveGroupCompareReadingsAction { - return { type: ActionType.ReceiveGroupCompareReadings, groupIDs, timeInterval, compareShift, unitID, readings }; -} - -/** - * @param meterIDs the IDs of the meters to get readings - * @param comparePeriod the period over which to check - * @param unitID the ID of the unit to get readings in - */ -function fetchMeterCompareReadings(meterIDs: number[], comparePeriod: ComparePeriod, unitID: number): Thunk { - return async (dispatch: Dispatch, getState: GetState) => { - const compareShift = calculateCompareShift(comparePeriod); - const currTimeInterval = getState().graph.compareTimeInterval; - dispatch(requestMeterCompareReadings(meterIDs, currTimeInterval, compareShift, unitID)); - const readings: CompareReadings = await metersApi.meterCompareReadings(meterIDs, currTimeInterval, compareShift, unitID); - dispatch(receiveMeterCompareReadings(meterIDs, currTimeInterval, compareShift, unitID, readings)); - }; -} - -/** - * Fetch the data for the given groups over the given interval. Fully manages the Redux lifecycle. - * @param groupIDs The IDs of the groups whose data should be fetched - * @param comparePeriod enum to represent a kind of time shift between curr and prev - * @param unitID the ID of the unit for which to check - */ -function fetchGroupCompareReadings(groupIDs: number[], comparePeriod: ComparePeriod, unitID: number): Thunk { - return async (dispatch: Dispatch, getState: GetState) => { - const compareShift = calculateCompareShift(comparePeriod); - const currTimeInterval = getState().graph.compareTimeInterval; - dispatch(requestGroupCompareReadings(groupIDs, currTimeInterval, compareShift, unitID)); - const readings = await groupsApi.groupCompareReadings(groupIDs, currTimeInterval, compareShift, unitID); - dispatch(receiveGroupCompareReadings(groupIDs, currTimeInterval, compareShift, unitID, readings)); - }; -} - - -/** - * Fetches readings for the compare chart of all selected meterIDs if they are not already fetched or being fetched - * @param comparePeriod The period to fetch readings for on the compare chart - * @param unitID the ID of the unit for which to check - * @returns An action to fetch the needed readings - */ -export function fetchNeededCompareReadings(comparePeriod: ComparePeriod, unitID: number): Thunk { - return (dispatch: Dispatch, getState: GetState) => { - const state = getState(); - const compareShift = calculateCompareShift(comparePeriod); - const timeInterval = state.graph.compareTimeInterval; - const promises: Array> = []; - - // Determine which meters are missing data for this time interval - const meterIDsToFetchForCompare = state.graph.selectedMeters.filter( - (id: number) => shouldFetchMeterCompareReadings(state, id, timeInterval, compareShift, unitID) - ); - // Fetch data for any missing meters - if (meterIDsToFetchForCompare.length > 0) { - promises.push(dispatch(fetchMeterCompareReadings(meterIDsToFetchForCompare, comparePeriod, unitID))); - } - - // Determine which groups are missing data for this time interval - const groupIDsToFetchForCompare = state.graph.selectedGroups.filter( - (id: number) => shouldFetchGroupCompareReadings(state, id, timeInterval, compareShift, unitID) - ); - // Fetch data for any missing groups - if (groupIDsToFetchForCompare.length > 0) { - promises.push(dispatch(fetchGroupCompareReadings(groupIDsToFetchForCompare, comparePeriod, unitID))); - } - return Promise.all(promises); - }; -} diff --git a/src/client/app/actions/graph.ts b/src/client/app/actions/graph.ts index d4a1d6c92..4328316d0 100644 --- a/src/client/app/actions/graph.ts +++ b/src/client/app/actions/graph.ts @@ -7,16 +7,8 @@ import { TimeInterval } from '../../../common/TimeInterval'; import { graphSlice } from '../reducers/graph'; import { Dispatch, GetState, Thunk } from '../types/redux/actions'; import * as t from '../types/redux/graph'; -import * as m from '../types/redux/map'; -import { State } from '../types/redux/state'; import { ComparePeriod, SortingOrder } from '../utils/calculateCompare'; -import { AreaUnitType } from '../utils/getAreaUnitConversion'; -import { fetchNeededBarReadings } from './barReadings'; -import { fetchNeededCompareReadings } from './compareReadings'; -import { fetchNeededLineReadings } from './lineReadings'; -import { changeSelectedMap } from './map'; import { fetchNeededMapReadings } from './mapReadings'; -import { fetchUnitsDetailsIfNeeded } from './units'; export function setHotlinkedAsync(hotlinked: boolean): Thunk { return (dispatch: Dispatch) => { @@ -29,36 +21,6 @@ export function toggleOptionsVisibility() { return graphSlice.actions.toggleOptionsVisibility(); } -function changeGraphZoom(timeInterval: TimeInterval) { - return graphSlice.actions.updateTimeInterval(timeInterval); -} - -export function changeBarDuration(barDuration: moment.Duration): Thunk { - return (dispatch: Dispatch, getState: GetState) => { - dispatch(graphSlice.actions.updateBarDuration(barDuration)); - dispatch(fetchNeededBarReadings(getState().graph.queryTimeInterval, getState().graph.selectedUnit)); - return Promise.resolve(); - }; -} - -function updateComparePeriod(comparePeriod: ComparePeriod, currentTime: moment.Moment) { - return graphSlice.actions.updateComparePeriod({ comparePeriod, currentTime }); -} - -export function changeCompareGraph(comparePeriod: ComparePeriod): Thunk { - return (dispatch: Dispatch, getState: GetState) => { - // Here there is no shift since we want to do it in terms of the current time in the browser. - // Note this does mean that if someone is in a different time zone then they may be ahead of - // reading on the server (so get 0 readings for those times) or behind (so miss recent readings). - // TODO At some point we may want to see if we can use the server time to avoid this. - dispatch(updateComparePeriod(comparePeriod, moment())); - dispatch((dispatch2: Dispatch) => { - dispatch2(fetchNeededCompareReadings(comparePeriod, getState().graph.selectedUnit)); - }); - return Promise.resolve(); - }; -} - export function changeCompareSortingOrder(compareSortingOrder: SortingOrder) { return graphSlice.actions.changeCompareSortingOrder(compareSortingOrder); } @@ -68,9 +30,6 @@ export function changeSelectedMeters(meterIDs: number[]): Thunk { dispatch(graphSlice.actions.updateSelectedMeters(meterIDs)); // Nesting dispatches to preserve that updateSelectedMeters() is called before fetching readings dispatch((dispatch2: Dispatch) => { - dispatch2(fetchNeededLineReadings(getState().graph.queryTimeInterval, getState().graph.selectedUnit)); - dispatch2(fetchNeededBarReadings(getState().graph.queryTimeInterval, getState().graph.selectedUnit)); - dispatch2(fetchNeededCompareReadings(getState().graph.comparePeriod, getState().graph.selectedUnit)); dispatch2(fetchNeededMapReadings(getState().graph.queryTimeInterval, getState().graph.selectedUnit)); }); return Promise.resolve(); @@ -82,77 +41,12 @@ export function changeSelectedGroups(groupIDs: number[]): Thunk { dispatch(graphSlice.actions.updateSelectedGroups(groupIDs)); // Nesting dispatches to preserve that updateSelectedGroups() is called before fetching readings dispatch((dispatch2: Dispatch) => { - dispatch2(fetchNeededLineReadings(getState().graph.queryTimeInterval, getState().graph.selectedUnit)); - dispatch2(fetchNeededBarReadings(getState().graph.queryTimeInterval, getState().graph.selectedUnit)); - dispatch2(fetchNeededCompareReadings(getState().graph.comparePeriod, getState().graph.selectedUnit)); dispatch2(fetchNeededMapReadings(getState().graph.queryTimeInterval, getState().graph.selectedUnit)); }); return Promise.resolve(); }; } -export function changeSelectedUnit(unitID: number): Thunk { - return (dispatch: Dispatch, getState: GetState) => { - dispatch(graphSlice.actions.updateSelectedUnit(unitID)); - dispatch((dispatch2: Dispatch) => { - dispatch(fetchNeededLineReadings(getState().graph.queryTimeInterval, unitID)); - dispatch2(fetchNeededBarReadings(getState().graph.queryTimeInterval, unitID)); - dispatch2(fetchNeededCompareReadings(getState().graph.comparePeriod, unitID)); - dispatch2(fetchNeededMapReadings(getState().graph.queryTimeInterval, unitID)); - }); - return Promise.resolve(); - } -} - -function fetchNeededReadingsForGraph(timeInterval: TimeInterval, unitID: number): Thunk { - return (dispatch: Dispatch) => { - dispatch(fetchNeededLineReadings(timeInterval, unitID)); - dispatch(fetchNeededBarReadings(timeInterval, unitID)); - dispatch(fetchNeededMapReadings(timeInterval, unitID)); - return Promise.resolve(); - }; -} - -function shouldChangeGraphZoom(state: State, timeInterval: TimeInterval): boolean { - return !state.graph.queryTimeInterval.equals(timeInterval); -} - -export function changeGraphZoomIfNeeded(timeInterval: TimeInterval): Thunk { - return (dispatch: Dispatch, getState: GetState) => { - if (shouldChangeGraphZoom(getState(), timeInterval)) { - dispatch(resetRangeSliderStack()); - dispatch(changeGraphZoom(timeInterval)); - dispatch(fetchNeededReadingsForGraph(timeInterval, getState().graph.selectedUnit)); - } - return Promise.resolve(); - }; -} - -function shouldChangeRangeSlider(range: TimeInterval): boolean { - return range !== TimeInterval.unbounded(); -} - -function changeRangeSlider(sliderInterval: TimeInterval) { - return graphSlice.actions.changeSliderRange(sliderInterval); -} - -/** - * remove constraints for rangeslider after user clicked redraw or restore - * by setting sliderRange to an empty string - */ -function resetRangeSliderStack() { - return graphSlice.actions.resetRangeSliderStack(); -} - -function changeRangeSliderIfNeeded(interval: TimeInterval): Thunk { - return (dispatch: Dispatch) => { - if (shouldChangeRangeSlider(interval)) { - dispatch(changeRangeSlider(interval)); - } - return Promise.resolve(); - }; -} - export function updateThreeDReadingInterval(readingInterval: t.ReadingInterval): Thunk { return (dispatch: Dispatch) => { dispatch(graphSlice.actions.updateThreeDReadingInterval(readingInterval)); @@ -193,71 +87,3 @@ export interface LinkOptions { meterOrGroup?: t.MeterOrGroup; readingInterval?: t.ReadingInterval; } - -/** - * Update graph options from a link - * @param options - Object of possible values to dispatch with keys: meterIDs, groupIDs, chartType, barDuration, toggleBarStacking, ... - */ -export function changeOptionsFromLink(options: LinkOptions) { - const dispatchFirst: Thunk[] = [setHotlinkedAsync(true)]; - const dispatchSecond: Array> = []; - if (options.meterIDs) { - dispatchSecond.push(changeSelectedMeters(options.meterIDs)); - } - if (options.groupIDs) { - dispatchSecond.push(changeSelectedGroups(options.groupIDs)); - } - if (options.meterOrGroupID && options.meterOrGroup) { - dispatchSecond.push(updateThreeDMeterOrGroupInfo(options.meterOrGroupID, options.meterOrGroup)); - } - if (options.chartType) { - dispatchSecond.push(graphSlice.actions.changeChartToRender(options.chartType)); - } - if (options.unitID) { - dispatchFirst.push(fetchUnitsDetailsIfNeeded()); - dispatchSecond.push(changeSelectedUnit(options.unitID)); - } - if (options.rate) { - dispatchSecond.push(graphSlice.actions.updateLineGraphRate(options.rate)); - } - if (options.barDuration) { - dispatchFirst.push(changeBarDuration(options.barDuration)); - } - if (options.serverRange) { - dispatchSecond.push(changeGraphZoomIfNeeded(options.serverRange)); - } - if (options.sliderRange) { - dispatchSecond.push(changeRangeSliderIfNeeded(options.sliderRange)); - } - if (options.toggleAreaNormalization) { - dispatchSecond.push(graphSlice.actions.toggleAreaNormalization()); - } - if (options.areaUnit) { - dispatchSecond.push(graphSlice.actions.updateSelectedAreaUnit(options.areaUnit as AreaUnitType)); - } - if (options.toggleMinMax) { - dispatchSecond.push(graphSlice.actions.toggleShowMinMax()); - } - if (options.toggleBarStacking) { - dispatchSecond.push(graphSlice.actions.changeBarStacking()); - } - if (options.comparePeriod) { - dispatchSecond.push(changeCompareGraph(options.comparePeriod)); - } - if (options.compareSortingOrder) { - dispatchSecond.push(changeCompareSortingOrder(options.compareSortingOrder)); - } - if (options.optionsVisibility != null) { - dispatchSecond.push(toggleOptionsVisibility()); - } - if (options.mapID) { - // TODO here and elsewhere should be IfNeeded but need to check that all state updates are done when edit, etc. - // TODO Not currently working with RTK migration - dispatchSecond.push(changeSelectedMap(options.mapID)); - } - if (options.readingInterval) { - dispatchSecond.push(updateThreeDReadingInterval(options.readingInterval)); - } - return (dispatch: Dispatch) => Promise.all(dispatchFirst.map(dispatch)) - .then(() => Promise.all(dispatchSecond.map(dispatch))); -} diff --git a/src/client/app/actions/lineReadings.ts b/src/client/app/actions/lineReadings.ts deleted file mode 100644 index f2301489c..000000000 --- a/src/client/app/actions/lineReadings.ts +++ /dev/null @@ -1,162 +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 { TimeInterval } from '../../../common/TimeInterval'; -import { ActionType, Thunk, Dispatch, GetState } from '../types/redux/actions'; -import { State } from '../types/redux/state'; -import * as t from '../types/redux/lineReadings'; -import { readingsApi } from '../utils/api'; -import { LineReadings } from '../types/readings'; - -/** - * @param state the Redux state - * @param meterID the ID of the meter to check - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - * @returns True if the readings for the given meter, time duration and unit are missing; false otherwise. - */ -function shouldFetchMeterLineReadings(state: State, meterID: number, timeInterval: TimeInterval, unitID: number): boolean { - const timeIntervalIndex = timeInterval.toString(); - - const readingsForID = state.readings.line.byMeterID[meterID]; - if (readingsForID === undefined) { - return true; - } - - const readingsForTimeInterval = readingsForID[timeIntervalIndex]; - if (readingsForTimeInterval === undefined) { - return true; - } - - const readingsForUnit = readingsForTimeInterval[unitID]; - if (readingsForUnit === undefined) { - return true; - } - - return !readingsForUnit.isFetching; -} - -/** - * @param state the Redux state - * @param groupID the ID of the group to check - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - * @returns True if the readings for the given group, time duration and unit are missing; false otherwise. - */ -function shouldFetchGroupLineReadings(state: State, groupID: number, timeInterval: TimeInterval, unitID: number): boolean { - const timeIntervalIndex = timeInterval.toString(); - - const readingsForID = state.readings.line.byGroupID[groupID]; - if (readingsForID === undefined) { - return true; - } - - const readingsForTimeInterval = readingsForID[timeIntervalIndex]; - if (readingsForTimeInterval === undefined) { - return true; - } - - const readingsForUnit = readingsForTimeInterval[unitID]; - if (readingsForUnit === undefined) { - return true; - } - - return !readingsForUnit.isFetching; -} - -/** - * @param meterIDs the IDs of the meters to get readings - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - */ -function requestMeterLineReadings(meterIDs: number[], timeInterval: TimeInterval, unitID: number): t.RequestMeterLineReadingsAction { - return { type: ActionType.RequestMeterLineReadings, meterIDs, timeInterval, unitID }; -} - -/** - * @param groupIDs the IDs of the groups to get readings - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - */ -function requestGroupLineReadings(groupIDs: number[], timeInterval: TimeInterval, unitID: number): t.RequestGroupLineReadingsAction { - return { type: ActionType.RequestGroupLineReadings, groupIDs, timeInterval, unitID }; -} - -/** - * @param meterIDs the IDs of the meters to get readings - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - * @param readings the readings for the given meters - */ -function receiveMeterLineReadings( - meterIDs: number[], timeInterval: TimeInterval, unitID: number, readings: LineReadings): t.ReceiveMeterLineReadingsAction { - return { type: ActionType.ReceiveMeterLineReadings, meterIDs, timeInterval, unitID, readings }; -} - -/** - * @param groupIDs the IDs of the groups to get readings - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - * @param readings the readings for the given groups - */ -function receiveGroupLineReadings( - groupIDs: number[], timeInterval: TimeInterval, unitID: number, readings: LineReadings): t.ReceiveGroupLineReadingsAction { - return { type: ActionType.ReceiveGroupLineReadings, groupIDs, timeInterval, unitID, readings }; -} - -/** - * @param meterIDs the IDs of the meters to get readings - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - */ -function fetchMeterLineReadings(meterIDs: number[], timeInterval: TimeInterval, unitID: number): Thunk { - return async (dispatch: Dispatch) => { - dispatch(requestMeterLineReadings(meterIDs, timeInterval, unitID)); - const meterLineReadings = await readingsApi.meterLineReadings(meterIDs, timeInterval, unitID); - dispatch(receiveMeterLineReadings(meterIDs, timeInterval, unitID, meterLineReadings)); - }; -} - -/** - * @param groupIDs the IDs of the groups to get readings - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - */ -function fetchGroupLineReadings(groupIDs: number[], timeInterval: TimeInterval, unitID: number): Thunk { - return async (dispatch: Dispatch) => { - dispatch(requestGroupLineReadings(groupIDs, timeInterval, unitID)); - const groupLineReadings = await readingsApi.groupLineReadings(groupIDs, timeInterval, unitID); - dispatch(receiveGroupLineReadings(groupIDs, timeInterval, unitID, groupLineReadings)); - }; -} - -/** - * Fetches readings for the line chart of all selected meters and groups, if needed. - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - */ -export function fetchNeededLineReadings(timeInterval: TimeInterval, unitID: number): Thunk { - return (dispatch: Dispatch, getState: GetState) => { - const state = getState(); - const promises: Array> = []; - - // Determine which meters are missing data for this time interval - const meterIDsToFetchForLine = state.graph.selectedMeters.filter( - id => shouldFetchMeterLineReadings(state, id, timeInterval, unitID) - ); - if (meterIDsToFetchForLine.length > 0) { - promises.push(dispatch(fetchMeterLineReadings(meterIDsToFetchForLine, timeInterval, unitID))); - } - - // Determine which groups are missing data for this time interval - const groupIDsToFetchForLine = state.graph.selectedGroups.filter( - id => shouldFetchGroupLineReadings(state, id, timeInterval, unitID) - ); - if (groupIDsToFetchForLine.length > 0) { - promises.push(dispatch(fetchGroupLineReadings(groupIDsToFetchForLine, timeInterval, unitID))); - } - - return Promise.all(promises); - }; -} diff --git a/src/client/app/actions/mapReadings.ts b/src/client/app/actions/mapReadings.ts index 7ccb2d167..8a60f00ea 100644 --- a/src/client/app/actions/mapReadings.ts +++ b/src/client/app/actions/mapReadings.ts @@ -1,19 +1,20 @@ +/* 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 */ + +fetchGroupMapReadings(); + /* 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 {Dispatch, GetState, Thunk} from '../types/redux/actions'; -import {TimeInterval} from '../../../common/TimeInterval'; +import { Dispatch, GetState, Thunk } from '../types/redux/actions'; +import { TimeInterval } from '../../../common/TimeInterval'; import * as moment from 'moment'; -import { - receiveGroupBarReadings, - receiveMeterBarReadings, - requestGroupBarReadings, - requestMeterBarReadings, - shouldFetchGroupBarReadings, - shouldFetchMeterBarReadings -} from './barReadings'; -import {readingsApi} from '../utils/api'; + +import { readingsApi } from '../utils/api'; /** * Fetch the data for the given meters over the given interval. Fully manages the Redux lifecycle. @@ -39,11 +40,14 @@ function fetchMeterMapReadings(meterIDs: number[], timeInterval: TimeInterval, d * @param duration The length of time covered in this timeInterval * @param unitID the ID of the unit for which to check */ + function fetchGroupMapReadings(groupIDs: number[], timeInterval: TimeInterval, duration: moment.Duration, unitID: number): Thunk { return async (dispatch: Dispatch) => { dispatch(requestGroupBarReadings(groupIDs, timeInterval, duration, unitID)); const groupMapReadings = await readingsApi.groupBarReadings(groupIDs, timeInterval, Math.round(duration.asDays()), unitID); dispatch(receiveGroupBarReadings(groupIDs, timeInterval, duration, unitID, groupMapReadings)); + return Promise.resolve() + }; } diff --git a/src/client/app/components/ChartDataSelectComponent.tsx b/src/client/app/components/ChartDataSelectComponent.tsx index 0a5a42d9b..7d91aabf9 100644 --- a/src/client/app/components/ChartDataSelectComponent.tsx +++ b/src/client/app/components/ChartDataSelectComponent.tsx @@ -2,749 +2,22 @@ * 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 _ from 'lodash'; import * as React from 'react'; -import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { useDispatch, useSelector } from 'react-redux'; -import { GroupsState } from 'types/redux/groups'; -import { MetersState } from 'types/redux/meters'; -import { changeMeterOrGroupInfo, changeSelectedGroups, changeSelectedMeters, changeSelectedUnit } from '../actions/graph'; -import { graphSlice } from '../reducers/graph'; -import { DataType } from '../types/Datasources'; -import { SelectOption } from '../types/items'; -import { Dispatch } from '../types/redux/actions'; -import { ChartTypes, MeterOrGroup } from '../types/redux/graph'; -import { State } from '../types/redux/state'; -import { DisplayableType, UnitData, UnitRepresentType, UnitsState, UnitType } from '../types/redux/units'; -import { - calculateScaleFromEndpoints, - CartesianPoint, Dimensions, - gpsToUserGrid, - itemDisplayableOnMap, itemMapInfoOk, - normalizeImageDimensions -} from '../utils/calibration'; -import { metersInGroup, unitsCompatibleWithMeters } from '../utils/determineCompatibleUnits'; -import { AreaUnitType } from '../utils/getAreaUnitConversion'; -import MultiSelectComponent from './MultiSelectComponent'; -import TooltipMarkerComponent from './TooltipMarkerComponent'; -import translate from '../utils/translate'; +import { MeterOrGroup } from '../types/redux/graph'; +import MeterAndGroupSelectComponent from './MeterAndGroupSelectComponent'; +import UnitSelectComponent from './UnitSelectComponent'; /** * A component which allows the user to select which data should be displayed on the chart. * @returns Chart data select element */ export default function ChartDataSelectComponent() { - // Must specify type if using ThunkDispatch - const dispatch: Dispatch = useDispatch(); - const intl = useIntl(); - const dataProps = useSelector((state: State) => { - const allMeters = state.meters.byMeterID; - const allGroups = state.groups.byGroupID; - - // Map information about meters, groups and units into a format the component can display. - let sortedMeters = getMeterCompatibilityForDropdown(state); - let sortedGroups = getGroupCompatibilityForDropdown(state); - const sortedUnits = getUnitCompatibilityForDropdown(state); - - // store meters which are found to be incompatible. - const incompatibleMeters = new Set(); - const incompatibleGroups = new Set(); - - // only run this check if area normalization is on - if (state.graph.areaNormalization) { - sortedMeters.forEach(meter => { - // do not allow meter to be selected if it has zero area or no area unit - if (allMeters[meter.value].area === 0 || allMeters[meter.value].areaUnit === AreaUnitType.none) { - meter.isDisabled = true; - incompatibleMeters.add(meter.value); - } - }); - sortedGroups.forEach(group => { - // do not allow group to be selected if it has zero area or no area unit - if (allGroups[group.value].area === 0 || allGroups[group.value].areaUnit === AreaUnitType.none) { - group.isDisabled = true; - incompatibleGroups.add(group.value); - } - }); - } - - // ony run this check if we are displaying a map chart - const chartToRender = state.graph.chartToRender; - const selectedMap = state.maps.selectedMap; - const threeDState = state.graph.threeD; - if (chartToRender === ChartTypes.map && selectedMap !== 0) { - const mp = state.maps.byMapID[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 - }; - // Determine the dimensions so within the Plotly coordinates on the user map. - const imageDimensionNormalized = normalizeImageDimensions(imageDimensions); - // The following is needed to get the map scale. Now that the system accepts maps that are not - // pointed north, it would be better to store the origin GPS and the scale factor instead of - // the origin and opposite GPS. For now, not going to change but could if redo DB and interface - // for maps. - // 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 axes parallel to the map axes. - // 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. - // This is the origin & opposite from the calibration. It is the lower, left - // 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; - sortedMeters.forEach(meter => { - // This meter's GPS value. - const gps = allMeters[meter.value].gps; - if (origin !== undefined && opposite !== undefined && gps !== undefined && gps !== null) { - // 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); - // 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(meter.value, DataType.Meter, mp, gps) && - itemDisplayableOnMap(imageDimensionNormalized, meterGPSInUserGrid))) { - meter.isDisabled = true; - incompatibleMeters.add(meter.value); - } - } else { - // Lack info on this map so skip. This is mostly done since TS complains about the undefined possibility. - meter.isDisabled = true; - incompatibleMeters.add(meter.value); - } - }); - // The below code follows the logic for meters shown above. See comments above for clarification on the below code. - sortedGroups.forEach(group => { - const gps = allGroups[group.value].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(group.value, DataType.Group, mp, gps) && - itemDisplayableOnMap(imageDimensionNormalized, groupGPSInUserGrid))) { - group.isDisabled = true; - incompatibleGroups.add(group.value); - } - } else { - group.isDisabled = true; - incompatibleGroups.add(group.value); - } - }); - } - - //Map information about the currently selected meters into a format the component can display. - const compatibleSelectedMeters: SelectOption[] = []; - const allSelectedMeters: SelectOption[] = []; - state.graph.selectedMeters.forEach(meterID => { - allSelectedMeters.push({ - // For meters we display the identifier. - label: state.meters.byMeterID[meterID] ? state.meters.byMeterID[meterID].identifier : '', - value: meterID, - isDisabled: false - } as SelectOption) - // don't include meters that can't be graphed with current settings - if (!incompatibleMeters.has(meterID)) { - // If the selected unit is -99 then there is not graphic unit yet. In this case you can only select a - // meter that has a default graphic unit because that will become the selected unit. This should only - // happen if no meter or group is yet selected. - if (state.graph.selectedUnit == -99) { - // If no unit is set then this should always be the first meter (or group) selected. - // The selectedUnit becomes the unit of the meter selected. Note is should always be set (not -99) since - // those meters should not have been visible. The only exception is if there are no selected meters but - // then this loop does not run. The loop is assumed to only run once in this case. - dispatch(changeSelectedUnit(state.meters.byMeterID[meterID].defaultGraphicUnit)); - } - compatibleSelectedMeters.push({ - // For meters we display the identifier. - label: state.meters.byMeterID[meterID] ? state.meters.byMeterID[meterID].identifier : '', - value: meterID, - isDisabled: false - } as SelectOption) - } - }); - - // re-sort by disabled because that status may have changed - sortedMeters = _.sortBy(sortedMeters, item => item.isDisabled, 'asc'); - // push a dummy item as a divider. - const firstDisabledMeter: number = sortedMeters.findIndex(item => item.isDisabled); - if (firstDisabledMeter != -1) { - sortedMeters.splice(firstDisabledMeter, 0, { - value: 0, - label: '----- ' + translate('incompatible.meters') + ' -----', - isDisabled: true - } as SelectOption - ); - } - - const compatibleSelectedGroups: SelectOption[] = []; - const allSelectedGroups: SelectOption[] = []; - state.graph.selectedGroups.forEach(groupID => { - allSelectedGroups.push({ - // For groups we display the name since no identifier. - label: state.groups.byGroupID[groupID] ? state.groups.byGroupID[groupID].name : '', - value: groupID, - isDisabled: false - } as SelectOption); - // don't include groups that can't be graphed with current settings - if (!incompatibleGroups.has(groupID)) { - // If the selected unit is -99 then there is no graphic unit yet. In this case you can only select a - // group that has a default graphic unit because that will become the selected unit. This should only - // happen if no meter or group is yet selected. - if (state.graph.selectedUnit == -99) { - // If no unit is set then this should always be the first group (or meter) selected. - // The selectedUnit becomes the unit of the group selected. Note is should always be set (not -99) since - // those groups should not have been visible. The only exception is if there are no selected groups but - // then this loop does not run. The loop is assumed to only run once in this case. - dispatch(changeSelectedUnit(state.groups.byGroupID[groupID].defaultGraphicUnit)); - } - compatibleSelectedGroups.push({ - // For groups we display the name since no identifier. - label: state.groups.byGroupID[groupID] ? state.groups.byGroupID[groupID].name : '', - value: groupID, - isDisabled: false - } as SelectOption); - } - }); - - // re-sort by disabled because that status may have changed - sortedGroups = _.sortBy(sortedGroups, item => item.isDisabled, 'asc'); - // dummy item as a divider - const firstDisabledGroup: number = sortedGroups.findIndex(item => item.isDisabled); - if (firstDisabledGroup != -1) { - sortedGroups.splice(firstDisabledGroup, 0, { - value: 0, - label: '----- ' + translate('incompatible.groups') + ' -----', - isDisabled: true - } as SelectOption - ); - } - - // You can only select one unit so variable name is singular. - // This does not need to be an array but we make it one for now so works similarly to meters & groups. - // TODO Might want to make it work as a single item. - const selectedUnit: SelectOption[] = []; - [state.graph.selectedUnit].forEach(unitID => { - if (unitID !== -99) { - // Only use if valid/selected unit which means it is not -99. - selectedUnit.push({ - // Units use the identifier to display. - label: state.graph.selectedUnit ? state.units.units[state.graph.selectedUnit].identifier : '', - value: unitID, - isDisabled: false - } as SelectOption); - } - }); - - // push a dummy item as a divider. - const firstDisabledUnit: number = sortedUnits.findIndex(item => item.isDisabled); - if (firstDisabledUnit != -1) { - sortedUnits.splice(firstDisabledUnit, 0, { - value: 0, - label: '----- ' + translate('incompatible.units') + ' -----', - isDisabled: true - } as SelectOption - ); - } - - return { - // all items, sorted alphabetically and by compatibility - sortedMeters, - sortedGroups, - sortedUnits, - // only selected items which are compatible with the current graph type - compatibleSelectedMeters, - compatibleSelectedGroups, - // all selected items regardless of compatibility - allSelectedMeters, - allSelectedGroups, - // currently selected unit - selectedUnit, - // chart currently being rendered - chartToRender, - // current state of threeD - threeDState - } - }); return (
-

- : - -

-
- { - // see meters code below for comments, as the code functions the same - if (dataProps.compatibleSelectedGroups.length !== 0 || newSelectedGroupOptions.length !== 0) { - const allSelectedGroupIDs: number[] = dataProps.allSelectedGroups.map(s => s.value); - const oldSelectedGroupIDs: number[] = dataProps.compatibleSelectedGroups.map(s => s.value); - const newSelectedGroupIDs: number[] = newSelectedGroupOptions.map(s => s.value); - const difference: number = oldSelectedGroupIDs.filter(x => !newSelectedGroupIDs.includes(x))[0]; - if (difference === undefined) { - allSelectedGroupIDs.push(newSelectedGroupIDs.filter(x => !oldSelectedGroupIDs.includes(x))[0]); - } else { - allSelectedGroupIDs.splice(allSelectedGroupIDs.indexOf(difference), 1); - } - dispatch(changeSelectedGroups(allSelectedGroupIDs)); - - // Do additional things relevant to 3D graphics - syncThreeDState(dataProps, allSelectedGroupIDs, oldSelectedGroupIDs, difference, MeterOrGroup.groups, dispatch); - } - }} - /> -
-

- : - -

-
- { - // If the user deletes when no item then this is considered a change so this executes. - // The difference is undefined and the resulting push puts undefined due to the filter. - // This if statement stops this case from being considered a change by not doing it - // if both the old and new lengths are zero. - if (dataProps.compatibleSelectedMeters.length !== 0 || newSelectedMeterOptions.length !== 0) { - //computes difference between previously selected meters and current selected meters, - // then makes the change to all selected meters, which includes incompatible selected meters - const allSelectedMeterIDs: number[] = dataProps.allSelectedMeters.map(s => s.value); - const oldSelectedMeterIDs: number[] = dataProps.compatibleSelectedMeters.map(s => s.value); - const newSelectedMeterIDs: number[] = newSelectedMeterOptions.map(s => s.value); - // It is assumed there can only be one element in this array, because this is triggered every time the selection is changed - // first filter finds items in the old list than are not in the new (deletions) - const difference: number = oldSelectedMeterIDs.filter(x => !newSelectedMeterIDs.includes(x))[0]; - if (difference === undefined) { - // finds items in the new list which are not in the old list (insertions) - allSelectedMeterIDs.push(newSelectedMeterIDs.filter(x => !oldSelectedMeterIDs.includes(x))[0]); - } else { - allSelectedMeterIDs.splice(allSelectedMeterIDs.indexOf(difference), 1); - } - dispatch(changeSelectedMeters(allSelectedMeterIDs)); - - // Do additional things relevant to 3D graphics - syncThreeDState(dataProps, allSelectedMeterIDs, oldSelectedMeterIDs, difference, MeterOrGroup.meters, dispatch); - - } - }} - /> -
-

- : - -

-
- {/* TODO this could be converted to a regular Select component */} - { - // TODO I don't quite understand why the component results in an array of size 2 when updating state - // For now I have hardcoded a fix that allows units to be selected over other units without clicking the x button - if (newSelectedUnitOptions.length === 0) { - // Update the selected meters and groups to empty to avoid graphing errors - // The update selected meters/groups functions are essentially the same as the change functions - // However, they do not attempt to graph. - dispatch(graphSlice.actions.updateSelectedGroups([])); - dispatch(graphSlice.actions.updateSelectedMeters([])); - dispatch(graphSlice.actions.updateSelectedUnit(-99)); - // Sync threeD state. - dispatch(changeMeterOrGroupInfo(undefined)); - } - else if (newSelectedUnitOptions.length === 1) { dispatch(changeSelectedUnit(newSelectedUnitOptions[0].value)); } - else if (newSelectedUnitOptions.length > 1) { dispatch(changeSelectedUnit(newSelectedUnitOptions[1].value)); } - // This should not happen - else { dispatch(changeSelectedUnit(-99)); } - }} - /> -
+ + +
); -} - -/** - * Determines the compatibility of units in the redux state for display in dropdown - * @param state - current redux state - * @returns a list of compatible units - */ -function getUnitCompatibilityForDropdown(state: State) { - - // Holds all units that are compatible with selected meters/groups - const compatibleUnits = new Set(); - // Holds all units that are not compatible with selected meters/groups - const incompatibleUnits = new Set(); - - // Holds all selected meters, including those retrieved from groups - const allSelectedMeters = new Set(); - - // Get for all meters - state.graph.selectedMeters.forEach(meter => { - allSelectedMeters.add(meter); - }); - // Get for all groups - state.graph.selectedGroups.forEach(group => { - // Get for all deep meters in group - metersInGroup(group).forEach(meter => { - allSelectedMeters.add(meter); - }); - }); - - if (allSelectedMeters.size == 0) { - // No meters/groups are selected. This includes the case where the selectedUnit is -99. - // Every unit is okay/compatible in this case so skip the work needed below. - // Filter the units to be displayed by user status and displayable type - getVisibleUnitOrSuffixState(state).forEach(unit => { - if (state.graph.areaNormalization && unit.unitRepresent === UnitRepresentType.raw) { - incompatibleUnits.add(unit.id); - } else { - compatibleUnits.add(unit.id); - } - }); - } else { - // Some meter or group is selected - // Retrieve set of units compatible with list of selected meters and/or groups - const units = unitsCompatibleWithMeters(allSelectedMeters); - - // Loop over all units (they must be of type unit or suffix - case 1) - getVisibleUnitOrSuffixState(state).forEach(o => { - // Control displayable ones (case 2) - if (units.has(o.id)) { - // Should show as compatible (case 3) - compatibleUnits.add(o.id); - } else { - // Should show as incompatible (case 4) - incompatibleUnits.add(o.id); - } - }); - } - // Ready to display unit. Put selectable ones before non-selectable ones. - const finalUnits = getSelectOptionsByItem(compatibleUnits, incompatibleUnits, state.units); - return finalUnits; -} - -// NOTE: getMeterCompatibilityForDropdown and getGroupCompatibilityForDropdown are essentially the same function. -// Keeping them separate for now for readability, perhaps they can be consolidated in the future - -/** - * Determines the compatibility of meters in the redux state for display in dropdown - * @param state - current redux state - * @returns a list of compatible meters - */ -export function getMeterCompatibilityForDropdown(state: State) { - // Holds all meters visible to the user - const visibleMeters = new Set(); - - // Get all the meters that this user can see. - if (state.currentUser.profile?.role === 'admin') { - // Can see all meters - Object.values(state.meters.byMeterID).forEach(meter => { - visibleMeters.add(meter.id); - }); - } - else { - // Regular user or not logged in so only add displayable meters - Object.values(state.meters.byMeterID).forEach(meter => { - if (meter.displayable) { - visibleMeters.add(meter.id); - } - }); - } - - // meters that can graph - const compatibleMeters = new Set(); - // meters that cannot graph. - const incompatibleMeters = new Set(); - - if (state.graph.selectedUnit === -99) { - // No unit is selected then no meter/group should be selected. - // In this case, every meter is valid (provided it has a default graphic unit) - // If the meter has a default graphic unit set then it can graph, otherwise it cannot. - visibleMeters.forEach(meterId => { - const meterGraphingUnit = state.meters.byMeterID[meterId].defaultGraphicUnit; - if (meterGraphingUnit === -99) { - //Default graphic unit is not set - incompatibleMeters.add(meterId); - } - else { - //Default graphic unit is set - if (state.graph.areaNormalization && state.units.units[meterGraphingUnit] - && state.units.units[meterGraphingUnit].unitRepresent === UnitRepresentType.raw) { - // area normalization is enabled and meter type is raw - incompatibleMeters.add(meterId); - } else { - compatibleMeters.add(meterId); - } - } - }); - } - else { - // A unit is selected - // For each meter get all of its compatible units - // Then, check if the selected unit exists in that set of compatible units - visibleMeters.forEach(meterId => { - // Get the set of units compatible with the current meter - const compatibleUnits = unitsCompatibleWithMeters(new Set([meterId])); - if (compatibleUnits.has(state.graph.selectedUnit)) { - // The selected unit is part of the set of compatible units with this meter - compatibleMeters.add(meterId); - } - else { - // The selected unit is not part of the compatible units set for this meter - incompatibleMeters.add(meterId); - } - }); - } - - // Retrieve select options from meter sets - const finalMeters = getSelectOptionsByItem(compatibleMeters, incompatibleMeters, state.meters); - return finalMeters; -} - -/** - * Determines the compatibility of group in the redux state for display in dropdown - * @param state - current redux state - * @returns a list of compatible groups - */ -export function getGroupCompatibilityForDropdown(state: State) { - // Holds all groups visible to the user - const visibleGroup = new Set(); - - // Get all the groups that this user can see. - if (state.currentUser.profile?.role === 'admin') { - // Can see all groups - Object.values(state.groups.byGroupID).forEach(group => { - visibleGroup.add(group.id); - }); - } - else { - // Regular user or not logged in so only add displayable groups - Object.values(state.groups.byGroupID).forEach(group => { - if (group.displayable) { - visibleGroup.add(group.id); - } - }); - } - - // groups that can graph - const compatibleGroups = new Set(); - // groups that cannot graph. - const incompatibleGroups = new Set(); - - if (state.graph.selectedUnit === -99) { - // If no unit is selected then no meter/group should be selected. - // In this case, every group is valid (provided it has a default graphic unit) - // If the group has a default graphic unit set then it can graph, otherwise it cannot. - visibleGroup.forEach(groupId => { - const groupGraphingUnit = state.groups.byGroupID[groupId].defaultGraphicUnit; - if (groupGraphingUnit === -99) { - //Default graphic unit is not set - incompatibleGroups.add(groupId); - } - else { - //Default graphic unit is set - if (state.graph.areaNormalization && state.units.units[groupGraphingUnit] && - state.units.units[groupGraphingUnit].unitRepresent === UnitRepresentType.raw) { - // area normalization is enabled and meter type is raw - incompatibleGroups.add(groupId); - } else { - compatibleGroups.add(groupId); - } - } - }); - } - else { - // A unit is selected - // For each group get all of its compatible units - // Then, check if the selected unit exists in that set of compatible units - visibleGroup.forEach(groupId => { - // Get the set of units compatible with the current group (through its deepMeters attribute) - // TODO If a meter in a group is not visible to this user then it is not in Redux state and this fails. - const compatibleUnits = unitsCompatibleWithMeters(metersInGroup(groupId)); - if (compatibleUnits.has(state.graph.selectedUnit)) { - // The selected unit is part of the set of compatible units with this group - compatibleGroups.add(groupId); - } - else { - // The selected unit is not part of the compatible units set for this group - incompatibleGroups.add(groupId); - } - }); - } - - // Retrieve select options from group sets - const finalGroups = getSelectOptionsByItem(compatibleGroups, incompatibleGroups, state.groups); - return finalGroups; -} - -/** - * Filters all units that are of type meter or displayable type none from the redux state, as well as admin only units if the user is not an admin. - * @param state - current redux state - * @returns an array of UnitData - */ -export function getVisibleUnitOrSuffixState(state: State) { - let visibleUnitsOrSuffixes; - if (state.currentUser.profile?.role === 'admin') { - // User is an admin, allow all units to be seen - visibleUnitsOrSuffixes = _.filter(state.units.units, (o: UnitData) => { - return (o.typeOfUnit == UnitType.unit || o.typeOfUnit == UnitType.suffix) && o.displayable != DisplayableType.none; - }); - } - else { - // User is not an admin, do not allow for admin units to be seen - visibleUnitsOrSuffixes = _.filter(state.units.units, (o: UnitData) => { - return (o.typeOfUnit == UnitType.unit || o.typeOfUnit == UnitType.suffix) && o.displayable == DisplayableType.all; - }); - } - return visibleUnitsOrSuffixes; -} - -/** - * Returns a set of SelectOptions based on the type of state passed in and sets the visibility. - * Visibility is determined by which set the items are contained in. - * @param compatibleItems - items that are compatible with current selected options - * @param incompatibleItems - units that are not compatible with current selected options - * @param state - current redux state, must be one of UnitsState, MetersState, or GroupsState - * @returns list of selectOptions of the given item - */ -export function getSelectOptionsByItem(compatibleItems: Set, incompatibleItems: Set, state: UnitsState | MetersState | GroupsState) { - // Holds the label of the select item, set dynamically according to the type of item passed in - let label = ''; - - //The final list of select options to be displayed - const finalItems: SelectOption[] = []; - - //Loop over each itemId and create an activated select option - compatibleItems.forEach(itemId => { - // Perhaps in the future this can be done differently - // Loop over the state type to see what state was passed in (units, meter, group, etc) - // Set the label correctly based on the type of state - // If this is converted to a switch statement the instanceOf function needs to be called twice - // Once for the initial state type check, again because the interpreter (for some reason) needs to be sure that the property exists in the object - // If else statements do not suffer from this - if (instanceOfUnitsState(state)) { - label = state.units[itemId].identifier; - } - else if (instanceOfMetersState(state)) { - label = state.byMeterID[itemId].identifier; - } - else if (instanceOfGroupsState(state)) { - label = state.byGroupID[itemId].name; - } - else { label = ''; } - // TODO This is a bit of a hack. When an admin logs in they may not have the new state so label is null. - // This should clear once the state is loaded. - label = label === null ? '' : label; - finalItems.push({ - value: itemId, - label: label, - isDisabled: false - } as SelectOption - ); - }); - //Loop over each itemId and create a disabled select option - incompatibleItems.forEach(itemId => { - if (instanceOfUnitsState(state)) { - label = state.units[itemId].identifier; - } - else if (instanceOfMetersState(state)) { - label = state.byMeterID[itemId].identifier; - } - else if (instanceOfGroupsState(state)) { - label = state.byGroupID[itemId].name; - } - else { label = ''; } - // TODO This is a bit of a hack. When an admin logs in they may not have the new state so label is null. - // This should clear once the state is loaded. - label = label === null ? '' : label; - finalItems.push({ - value: itemId, - label: label, - isDisabled: true - } as SelectOption - ); - }) - return _.sortBy(_.sortBy(finalItems, item => item.label.toLowerCase(), 'asc'), item => item.isDisabled, 'asc'); -} - -/** - * Helper function to determine what type of state was passed in - * @param state The state to check - * @returns Whether or not this is a UnitsState - */ -export function instanceOfUnitsState(state: any): state is UnitsState { return 'units' in state; } - -/** - * Helper function to determine what type of state was passed in - * @param state The state to check - * @returns Whether or not this is a MetersState - */ -export function instanceOfMetersState(state: any): state is MetersState { return 'byMeterID' in state; } - -/** - * Helper function to determine what type of state was passed in - * @param state The state to check - * @returns Whether or not this is a GroupsState - */ -export function instanceOfGroupsState(state: any): state is GroupsState { return 'byGroupID' in state; } - -/** - * 3D helper function used to keep 3D redux state in sync with dropdown menus - * @param dataProps used to extract relevant useSelect state values - * @param allSelected all selected meters - * @param oldSelected previously selected meters - * @param difference integer value that represents the removed meter Or Group - * @param meterOrGroup used to set whether a meter or group is currently active. - * @param dispatch instance of the dispatch for altering redux state. - */ -function syncThreeDState( - dataProps: any, - allSelected: number[], - oldSelected: number[], - difference: number, - meterOrGroup: MeterOrGroup, - dispatch: Dispatch): void { - - // checks to see if meter has been removed - const meterOrGroupAdded = allSelected.length > oldSelected.length; - const meterOrGroupRemoved = !meterOrGroupAdded; - - //Check to see if potentially removed meterOrGroup is currently active. - const meterOrGroupIsSelected = difference === dataProps.threeDState.meterOrGroupID; - - if (meterOrGroupAdded && dataProps.chartToRender === ChartTypes.threeD) { - // when a meter or group is selected, make it the currently active in 3D state. - // only tracks when on 3d page. - const addedMeterOrGroup = allSelected[allSelected.length - 1]; - dispatch(changeMeterOrGroupInfo(addedMeterOrGroup, meterOrGroup)); - } else if (meterOrGroupRemoved && meterOrGroupIsSelected) { - // reset currently active threeD Meter or group when it is removed and is currently active. - dispatch(changeMeterOrGroupInfo(undefined)); - } -} - -const divBottomPadding: React.CSSProperties = { - paddingBottom: '15px' -}; -const labelStyle: React.CSSProperties = { - fontWeight: 'bold', - margin: 0 -}; -const messages = defineMessages({ - selectGroups: { id: 'select.groups' }, - selectMeters: { id: 'select.meters' }, - selectUnit: { id: 'select.unit' }, - helpSelectGroups: { id: 'help.home.select.groups' }, - helpSelectMeters: { id: 'help.home.select.meters' } -}); \ No newline at end of file +} \ No newline at end of file diff --git a/src/client/app/components/ChartDataSelectComponentSave.tsx b/src/client/app/components/ChartDataSelectComponentSave.tsx new file mode 100644 index 000000000..2b61c5b51 --- /dev/null +++ b/src/client/app/components/ChartDataSelectComponentSave.tsx @@ -0,0 +1,751 @@ +// /* 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 _ from 'lodash'; +// import * as React from 'react'; +// import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +// import { useDispatch, useSelector } from 'react-redux'; +// import { GroupsState } from 'types/redux/groups'; +// import { MetersState } from 'types/redux/meters'; +// import { changeMeterOrGroupInfo, changeSelectedGroups, changeSelectedMeters, changeSelectedUnit } from '../actions/graph'; +// import { graphSlice } from '../reducers/graph'; +// import { DataType } from '../types/Datasources'; +// import { SelectOption } from '../types/items'; +// import { Dispatch } from '../types/redux/actions'; +// import { ChartTypes, MeterOrGroup } from '../types/redux/graph'; +// import { State } from '../types/redux/state'; +// import { DisplayableType, UnitData, UnitRepresentType, UnitsState, UnitType } from '../types/redux/units'; +// import { +// calculateScaleFromEndpoints, +// CartesianPoint, Dimensions, +// gpsToUserGrid, +// itemDisplayableOnMap, itemMapInfoOk, +// normalizeImageDimensions +// } from '../utils/calibration'; +// import { metersInGroup, unitsCompatibleWithMeters } from '../utils/determineCompatibleUnits'; +// import { AreaUnitType } from '../utils/getAreaUnitConversion'; +// import MultiSelectComponent from './MultiSelectComponent'; +// import TooltipMarkerComponent from './TooltipMarkerComponent'; +// import translate from '../utils/translate'; + +// /** +// * A component which allows the user to select which data should be displayed on the chart. +// * @returns Chart data select element +// */ +// export default function ChartDataSelectComponent() { +// // Must specify type if using ThunkDispatch +// const dispatch: Dispatch = useDispatch(); +// const intl = useIntl(); +// const dataProps = useSelector((state: State) => { +// const allMeters = state.meters.byMeterID; +// const allGroups = state.groups.byGroupID; + +// // Map information about meters, groups and units into a format the component can display. +// let sortedMeters = getMeterCompatibilityForDropdown(state); +// let sortedGroups = getGroupCompatibilityForDropdown(state); +// const sortedUnits = getUnitCompatibilityForDropdown(state); + +// // store meters which are found to be incompatible. +// const incompatibleMeters = new Set(); +// const incompatibleGroups = new Set(); + +// // only run this check if area normalization is on +// if (state.graph.areaNormalization) { +// sortedMeters.forEach(meter => { +// // do not allow meter to be selected if it has zero area or no area unit +// if (allMeters[meter.value].area === 0 || allMeters[meter.value].areaUnit === AreaUnitType.none) { +// meter.isDisabled = true; +// incompatibleMeters.add(meter.value); +// } +// }); +// sortedGroups.forEach(group => { +// // do not allow group to be selected if it has zero area or no area unit +// if (allGroups[group.value].area === 0 || allGroups[group.value].areaUnit === AreaUnitType.none) { +// group.isDisabled = true; +// incompatibleGroups.add(group.value); +// } +// }); +// } + +// // ony run this check if we are displaying a map chart +// const chartToRender = state.graph.chartToRender; +// const selectedMap = state.maps.selectedMap; +// const threeDState = state.graph.threeD; +// if (chartToRender === ChartTypes.map && selectedMap !== 0) { +// const mp = state.maps.byMapID[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 +// }; +// // Determine the dimensions so within the Plotly coordinates on the user map. +// const imageDimensionNormalized = normalizeImageDimensions(imageDimensions); +// // The following is needed to get the map scale. Now that the system accepts maps that are not +// // pointed north, it would be better to store the origin GPS and the scale factor instead of +// // the origin and opposite GPS. For now, not going to change but could if redo DB and interface +// // for maps. +// // 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 axes parallel to the map axes. +// // 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. +// // This is the origin & opposite from the calibration. It is the lower, left +// // 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; +// sortedMeters.forEach(meter => { +// // This meter's GPS value. +// const gps = allMeters[meter.value].gps; +// if (origin !== undefined && opposite !== undefined && gps !== undefined && gps !== null) { +// // 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); +// // 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(meter.value, DataType.Meter, mp, gps) && +// itemDisplayableOnMap(imageDimensionNormalized, meterGPSInUserGrid))) { +// meter.isDisabled = true; +// incompatibleMeters.add(meter.value); +// } +// } else { +// // Lack info on this map so skip. This is mostly done since TS complains about the undefined possibility. +// meter.isDisabled = true; +// incompatibleMeters.add(meter.value); +// } +// }); +// // The below code follows the logic for meters shown above. See comments above for clarification on the below code. +// sortedGroups.forEach(group => { +// const gps = allGroups[group.value].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(group.value, DataType.Group, mp, gps) && +// itemDisplayableOnMap(imageDimensionNormalized, groupGPSInUserGrid))) { +// group.isDisabled = true; +// incompatibleGroups.add(group.value); +// } +// } else { +// group.isDisabled = true; +// incompatibleGroups.add(group.value); +// } +// }); +// } + +// //Map information about the currently selected meters into a format the component can display. +// const compatibleSelectedMeters: SelectOption[] = []; +// const allSelectedMeters: SelectOption[] = []; +// state.graph.selectedMeters.forEach(meterID => { +// allSelectedMeters.push({ +// // For meters we display the identifier. +// label: state.meters.byMeterID[meterID] ? state.meters.byMeterID[meterID].identifier : '', +// value: meterID, +// isDisabled: false +// } as SelectOption) +// // don't include meters that can't be graphed with current settings +// if (!incompatibleMeters.has(meterID)) { +// // If the selected unit is -99 then there is not graphic unit yet. In this case you can only select a +// // meter that has a default graphic unit because that will become the selected unit. This should only +// // happen if no meter or group is yet selected. +// if (state.graph.selectedUnit == -99) { +// // If no unit is set then this should always be the first meter (or group) selected. +// // The selectedUnit becomes the unit of the meter selected. Note is should always be set (not -99) since +// // those meters should not have been visible. The only exception is if there are no selected meters but +// // then this loop does not run. The loop is assumed to only run once in this case. +// dispatch(changeSelectedUnit(state.meters.byMeterID[meterID].defaultGraphicUnit)); +// } +// compatibleSelectedMeters.push({ +// // For meters we display the identifier. +// label: state.meters.byMeterID[meterID] ? state.meters.byMeterID[meterID].identifier : '', +// value: meterID, +// isDisabled: false +// } as SelectOption) +// } +// }); + +// // re-sort by disabled because that status may have changed +// sortedMeters = _.sortBy(sortedMeters, item => item.isDisabled, 'asc'); +// // push a dummy item as a divider. +// const firstDisabledMeter: number = sortedMeters.findIndex(item => item.isDisabled); +// if (firstDisabledMeter != -1) { +// sortedMeters.splice(firstDisabledMeter, 0, { +// value: 0, +// label: '----- ' + translate('incompatible.meters') + ' -----', +// isDisabled: true +// } as SelectOption +// ); +// } + +// const compatibleSelectedGroups: SelectOption[] = []; +// const allSelectedGroups: SelectOption[] = []; +// state.graph.selectedGroups.forEach(groupID => { +// allSelectedGroups.push({ +// // For groups we display the name since no identifier. +// label: state.groups.byGroupID[groupID] ? state.groups.byGroupID[groupID].name : '', +// value: groupID, +// isDisabled: false +// } as SelectOption); +// // don't include groups that can't be graphed with current settings +// if (!incompatibleGroups.has(groupID)) { +// // If the selected unit is -99 then there is no graphic unit yet. In this case you can only select a +// // group that has a default graphic unit because that will become the selected unit. This should only +// // happen if no meter or group is yet selected. +// if (state.graph.selectedUnit == -99) { +// // If no unit is set then this should always be the first group (or meter) selected. +// // The selectedUnit becomes the unit of the group selected. Note is should always be set (not -99) since +// // those groups should not have been visible. The only exception is if there are no selected groups but +// // then this loop does not run. The loop is assumed to only run once in this case. +// dispatch(changeSelectedUnit(state.groups.byGroupID[groupID].defaultGraphicUnit)); +// } +// compatibleSelectedGroups.push({ +// // For groups we display the name since no identifier. +// label: state.groups.byGroupID[groupID] ? state.groups.byGroupID[groupID].name : '', +// value: groupID, +// isDisabled: false +// } as SelectOption); +// } +// }); + +// // re-sort by disabled because that status may have changed +// sortedGroups = _.sortBy(sortedGroups, item => item.isDisabled, 'asc'); +// // dummy item as a divider +// const firstDisabledGroup: number = sortedGroups.findIndex(item => item.isDisabled); +// if (firstDisabledGroup != -1) { +// sortedGroups.splice(firstDisabledGroup, 0, { +// value: 0, +// label: '----- ' + translate('incompatible.groups') + ' -----', +// isDisabled: true +// } as SelectOption +// ); +// } + +// // You can only select one unit so variable name is singular. +// // This does not need to be an array but we make it one for now so works similarly to meters & groups. +// // TODO Might want to make it work as a single item. +// const selectedUnit: SelectOption[] = []; +// [state.graph.selectedUnit].forEach(unitID => { +// if (unitID !== -99) { +// // Only use if valid/selected unit which means it is not -99. +// selectedUnit.push({ +// // Units use the identifier to display. +// label: state.graph.selectedUnit ? state.units.units[state.graph.selectedUnit].identifier : '', +// value: unitID, +// isDisabled: false +// } as SelectOption); +// } +// }); + +// // push a dummy item as a divider. +// const firstDisabledUnit: number = sortedUnits.findIndex(item => item.isDisabled); +// if (firstDisabledUnit != -1) { +// sortedUnits.splice(firstDisabledUnit, 0, { +// value: 0, +// label: '----- ' + translate('incompatible.units') + ' -----', +// isDisabled: true +// } as SelectOption +// ); +// } + +// return { +// // all items, sorted alphabetically and by compatibility +// sortedMeters, +// sortedGroups, +// sortedUnits, +// // only selected items which are compatible with the current graph type +// compatibleSelectedMeters, +// compatibleSelectedGroups, +// // all selected items regardless of compatibility +// allSelectedMeters, +// allSelectedGroups, +// // currently selected unit +// selectedUnit, +// // chart currently being rendered +// chartToRender, +// // current state of threeD +// threeDState +// } +// }); + +// return ( +//
+//

+// : +// +//

+//
+// { +// // see meters code below for comments, as the code functions the same +// if (dataProps.compatibleSelectedGroups.length !== 0 || newSelectedGroupOptions.length !== 0) { +// const allSelectedGroupIDs: number[] = dataProps.allSelectedGroups.map(s => s.value); +// const oldSelectedGroupIDs: number[] = dataProps.compatibleSelectedGroups.map(s => s.value); +// const newSelectedGroupIDs: number[] = newSelectedGroupOptions.map(s => s.value); +// const difference: number = oldSelectedGroupIDs.filter(x => !newSelectedGroupIDs.includes(x))[0]; +// if (difference === undefined) { +// allSelectedGroupIDs.push(newSelectedGroupIDs.filter(x => !oldSelectedGroupIDs.includes(x))[0]); +// } else { +// allSelectedGroupIDs.splice(allSelectedGroupIDs.indexOf(difference), 1); +// } +// dispatch(changeSelectedGroups(allSelectedGroupIDs)); + +// // Do additional things relevant to 3D graphics +// syncThreeDState(dataProps, allSelectedGroupIDs, oldSelectedGroupIDs, difference, MeterOrGroup.groups, dispatch); +// } +// }} +// /> +//
+//

+// : +// +//

+//
+// { +// // If the user deletes when no item then this is considered a change so this executes. +// // The difference is undefined and the resulting push puts undefined due to the filter. +// // This if statement stops this case from being considered a change by not doing it +// // if both the old and new lengths are zero. +// if (dataProps.compatibleSelectedMeters.length !== 0 || newSelectedMeterOptions.length !== 0) { +// //computes difference between previously selected meters and current selected meters, +// // then makes the change to all selected meters, which includes incompatible selected meters +// const allSelectedMeterIDs: number[] = dataProps.allSelectedMeters.map(s => s.value); +// const oldSelectedMeterIDs: number[] = dataProps.compatibleSelectedMeters.map(s => s.value); +// const newSelectedMeterIDs: number[] = newSelectedMeterOptions.map(s => s.value); +// // It is assumed there can only be one element in this array, because this is triggered every time the selection is changed +// // first filter finds items in the old list than are not in the new (deletions) +// const difference: number = oldSelectedMeterIDs.filter(x => !newSelectedMeterIDs.includes(x))[0]; +// if (difference === undefined) { +// // finds items in the new list which are not in the old list (insertions) +// allSelectedMeterIDs.push(newSelectedMeterIDs.filter(x => !oldSelectedMeterIDs.includes(x))[0]); +// } else { +// allSelectedMeterIDs.splice(allSelectedMeterIDs.indexOf(difference), 1); +// } +// dispatch(changeSelectedMeters(allSelectedMeterIDs)); + +// // Do additional things relevant to 3D graphics +// syncThreeDState(dataProps, allSelectedMeterIDs, oldSelectedMeterIDs, difference, MeterOrGroup.meters, dispatch); + +// } +// }} +// /> +//
+//

+// : +// +//

+//
+// {/* TODO this could be converted to a regular Select component */} +// { +// // TODO I don't quite understand why the component results in an array of size 2 when updating state +// // For now I have hardcoded a fix that allows units to be selected over other units without clicking the x button +// if (newSelectedUnitOptions.length === 0) { +// // Update the selected meters and groups to empty to avoid graphing errors +// // The update selected meters/groups functions are essentially the same as the change functions +// // However, they do not attempt to graph. +// dispatch(graphSlice.actions.updateSelectedGroups([])); +// dispatch(graphSlice.actions.updateSelectedMeters([])); +// dispatch(graphSlice.actions.updateSelectedUnit(-99)); +// // Sync threeD state. +// dispatch(changeMeterOrGroupInfo(undefined)); +// } +// else if (newSelectedUnitOptions.length === 1) { dispatch(changeSelectedUnit(newSelectedUnitOptions[0].value)); } +// else if (newSelectedUnitOptions.length > 1) { dispatch(changeSelectedUnit(newSelectedUnitOptions[1].value)); } +// // This should not happen +// else { dispatch(changeSelectedUnit(-99)); } +// }} +// /> +//
+//
+// ); +// } + +// /** +// * Determines the compatibility of units in the redux state for display in dropdown +// * @param state - current redux state +// * @returns a list of compatible units +// */ +// function getUnitCompatibilityForDropdown(state: State) { + +// // Holds all units that are compatible with selected meters/groups +// const compatibleUnits = new Set(); +// // Holds all units that are not compatible with selected meters/groups +// const incompatibleUnits = new Set(); + +// // Holds all selected meters, including those retrieved from groups +// const allSelectedMeters = new Set(); + +// // Get for all meters +// state.graph.selectedMeters.forEach(meter => { +// allSelectedMeters.add(meter); +// }); +// // Get for all groups +// state.graph.selectedGroups.forEach(group => { +// // Get for all deep meters in group +// metersInGroup(group).forEach(meter => { +// allSelectedMeters.add(meter); +// }); +// }); + +// if (allSelectedMeters.size == 0) { +// // No meters/groups are selected. This includes the case where the selectedUnit is -99. +// // Every unit is okay/compatible in this case so skip the work needed below. +// // Filter the units to be displayed by user status and displayable type +// getVisibleUnitOrSuffixState(state).forEach(unit => { +// if (state.graph.areaNormalization && unit.unitRepresent === UnitRepresentType.raw) { +// incompatibleUnits.add(unit.id); +// } else { +// compatibleUnits.add(unit.id); +// } +// }); +// } else { +// // Some meter or group is selected +// // Retrieve set of units compatible with list of selected meters and/or groups +// const units = unitsCompatibleWithMeters(allSelectedMeters); + +// // Loop over all units (they must be of type unit or suffix - case 1) +// getVisibleUnitOrSuffixState(state).forEach(o => { +// // Control displayable ones (case 2) +// if (units.has(o.id)) { +// // Should show as compatible (case 3) +// compatibleUnits.add(o.id); +// } else { +// // Should show as incompatible (case 4) +// incompatibleUnits.add(o.id); +// } +// }); +// } +// // Ready to display unit. Put selectable ones before non-selectable ones. +// const finalUnits = getSelectOptionsByItem(compatibleUnits, incompatibleUnits, state.units); +// return finalUnits; +// } + +// // NOTE: getMeterCompatibilityForDropdown and getGroupCompatibilityForDropdown are essentially the same function. +// // Keeping them separate for now for readability, perhaps they can be consolidated in the future + +// /** +// * Determines the compatibility of meters in the redux state for display in dropdown +// * @param state - current redux state +// * @returns a list of compatible meters +// */ +// export function getMeterCompatibilityForDropdown(state: State) { +// // Holds all meters visible to the user +// const visibleMeters = new Set(); + +// // Get all the meters that this user can see. +// if (state.currentUser.profile?.role === 'admin') { +// // Can see all meters +// Object.values(state.meters.byMeterID).forEach(meter => { +// visibleMeters.add(meter.id); +// }); +// } +// else { +// // Regular user or not logged in so only add displayable meters +// Object.values(state.meters.byMeterID).forEach(meter => { +// if (meter.displayable) { +// visibleMeters.add(meter.id); +// } +// }); +// } + +// // meters that can graph +// const compatibleMeters = new Set(); +// // meters that cannot graph. +// const incompatibleMeters = new Set(); + +// if (state.graph.selectedUnit === -99) { +// // No unit is selected then no meter/group should be selected. +// // In this case, every meter is valid (provided it has a default graphic unit) +// // If the meter has a default graphic unit set then it can graph, otherwise it cannot. +// visibleMeters.forEach(meterId => { +// const meterGraphingUnit = state.meters.byMeterID[meterId].defaultGraphicUnit; +// if (meterGraphingUnit === -99) { +// //Default graphic unit is not set +// incompatibleMeters.add(meterId); +// } +// else { +// //Default graphic unit is set +// if (state.graph.areaNormalization && state.units.units[meterGraphingUnit] +// && state.units.units[meterGraphingUnit].unitRepresent === UnitRepresentType.raw) { +// // area normalization is enabled and meter type is raw +// incompatibleMeters.add(meterId); +// } else { +// compatibleMeters.add(meterId); +// } +// } +// }); +// } +// else { +// // A unit is selected +// // For each meter get all of its compatible units +// // Then, check if the selected unit exists in that set of compatible units +// visibleMeters.forEach(meterId => { +// // Get the set of units compatible with the current meter +// const compatibleUnits = unitsCompatibleWithMeters(new Set([meterId])); +// if (compatibleUnits.has(state.graph.selectedUnit)) { +// // The selected unit is part of the set of compatible units with this meter +// compatibleMeters.add(meterId); +// } +// else { +// // The selected unit is not part of the compatible units set for this meter +// incompatibleMeters.add(meterId); +// } +// }); +// } + +// // Retrieve select options from meter sets +// const finalMeters = getSelectOptionsByItem(compatibleMeters, incompatibleMeters, state.meters); +// return finalMeters; +// } + +// /** +// * Determines the compatibility of group in the redux state for display in dropdown +// * @param state - current redux state +// * @returns a list of compatible groups +// */ +// export function getGroupCompatibilityForDropdown(state: State) { +// // Holds all groups visible to the user +// const visibleGroup = new Set(); + +// // Get all the groups that this user can see. +// if (state.currentUser.profile?.role === 'admin') { +// // Can see all groups +// Object.values(state.groups.byGroupID).forEach(group => { +// visibleGroup.add(group.id); +// }); +// } +// else { +// // Regular user or not logged in so only add displayable groups +// Object.values(state.groups.byGroupID).forEach(group => { +// if (group.displayable) { +// visibleGroup.add(group.id); +// } +// }); +// } + +// // groups that can graph +// const compatibleGroups = new Set(); +// // groups that cannot graph. +// const incompatibleGroups = new Set(); + +// if (state.graph.selectedUnit === -99) { +// // If no unit is selected then no meter/group should be selected. +// // In this case, every group is valid (provided it has a default graphic unit) +// // If the group has a default graphic unit set then it can graph, otherwise it cannot. +// visibleGroup.forEach(groupId => { +// const groupGraphingUnit = state.groups.byGroupID[groupId].defaultGraphicUnit; +// if (groupGraphingUnit === -99) { +// //Default graphic unit is not set +// incompatibleGroups.add(groupId); +// } +// else { +// //Default graphic unit is set +// if (state.graph.areaNormalization && state.units.units[groupGraphingUnit] && +// state.units.units[groupGraphingUnit].unitRepresent === UnitRepresentType.raw) { +// // area normalization is enabled and meter type is raw +// incompatibleGroups.add(groupId); +// } else { +// compatibleGroups.add(groupId); +// } +// } +// }); +// } +// else { +// // A unit is selected +// // For each group get all of its compatible units +// // Then, check if the selected unit exists in that set of compatible units +// visibleGroup.forEach(groupId => { +// // Get the set of units compatible with the current group (through its deepMeters attribute) +// // TODO If a meter in a group is not visible to this user then it is not in Redux state and this fails. +// const compatibleUnits = unitsCompatibleWithMeters(metersInGroup(groupId)); +// if (compatibleUnits.has(state.graph.selectedUnit)) { +// // The selected unit is part of the set of compatible units with this group +// compatibleGroups.add(groupId); +// } +// else { +// // The selected unit is not part of the compatible units set for this group +// incompatibleGroups.add(groupId); +// } +// }); +// } + +// // Retrieve select options from group sets +// const finalGroups = getSelectOptionsByItem(compatibleGroups, incompatibleGroups, state.groups); +// return finalGroups; +// } + +// /** +// * Filters all units that are of type meter or displayable type none from the redux state, as well as admin only units if the user is not an admin. +// * @param state - current redux state +// * @returns an array of UnitData +// */ +// export function getVisibleUnitOrSuffixState(state: State) { +// let visibleUnitsOrSuffixes; +// if (state.currentUser.profile?.role === 'admin') { +// // User is an admin, allow all units to be seen +// visibleUnitsOrSuffixes = _.filter(state.units.units, (o: UnitData) => { +// return (o.typeOfUnit == UnitType.unit || o.typeOfUnit == UnitType.suffix) && o.displayable != DisplayableType.none; +// }); +// } +// else { +// // User is not an admin, do not allow for admin units to be seen +// visibleUnitsOrSuffixes = _.filter(state.units.units, (o: UnitData) => { +// return (o.typeOfUnit == UnitType.unit || o.typeOfUnit == UnitType.suffix) && o.displayable == DisplayableType.all; +// }); +// } +// return visibleUnitsOrSuffixes; +// } + +// /** +// * Returns a set of SelectOptions based on the type of state passed in and sets the visibility. +// * Visibility is determined by which set the items are contained in. +// * @param compatibleItems - items that are compatible with current selected options +// * @param incompatibleItems - units that are not compatible with current selected options +// * @param state - current redux state, must be one of UnitsState, MetersState, or GroupsState +// * @returns list of selectOptions of the given item +// */ +// export function getSelectOptionsByItem( +// compatibleItems: Set, incompatibleItems: Set, state: UnitsState | MetersState | GroupsState) { +// // Holds the label of the select item, set dynamically according to the type of item passed in +// let label = ''; + +// //The final list of select options to be displayed +// const finalItems: SelectOption[] = []; + +// //Loop over each itemId and create an activated select option +// compatibleItems.forEach(itemId => { +// // Perhaps in the future this can be done differently +// // Loop over the state type to see what state was passed in (units, meter, group, etc) +// // Set the label correctly based on the type of state +// // If this is converted to a switch statement the instanceOf function needs to be called twice +// // Once for the initial state type check, again because the interpreter (for some reason) needs to be sure that the property exists in the object +// // If else statements do not suffer from this +// if (instanceOfUnitsState(state)) { +// label = state.units[itemId].identifier; +// } +// else if (instanceOfMetersState(state)) { +// label = state.byMeterID[itemId].identifier; +// } +// else if (instanceOfGroupsState(state)) { +// label = state.byGroupID[itemId].name; +// } +// else { label = ''; } +// // TODO This is a bit of a hack. When an admin logs in they may not have the new state so label is null. +// // This should clear once the state is loaded. +// label = label === null ? '' : label; +// finalItems.push({ +// value: itemId, +// label: label, +// isDisabled: false +// } as SelectOption +// ); +// }); +// //Loop over each itemId and create a disabled select option +// incompatibleItems.forEach(itemId => { +// if (instanceOfUnitsState(state)) { +// label = state.units[itemId].identifier; +// } +// else if (instanceOfMetersState(state)) { +// label = state.byMeterID[itemId].identifier; +// } +// else if (instanceOfGroupsState(state)) { +// label = state.byGroupID[itemId].name; +// } +// else { label = ''; } +// // TODO This is a bit of a hack. When an admin logs in they may not have the new state so label is null. +// // This should clear once the state is loaded. +// label = label === null ? '' : label; +// finalItems.push({ +// value: itemId, +// label: label, +// isDisabled: true +// } as SelectOption +// ); +// }) +// return _.sortBy(_.sortBy(finalItems, item => item.label.toLowerCase(), 'asc'), item => item.isDisabled, 'asc'); +// } + +// /** +// * Helper function to determine what type of state was passed in +// * @param state The state to check +// * @returns Whether or not this is a UnitsState +// */ +// export function instanceOfUnitsState(state: any): state is UnitsState { return 'units' in state; } + +// /** +// * Helper function to determine what type of state was passed in +// * @param state The state to check +// * @returns Whether or not this is a MetersState +// */ +// export function instanceOfMetersState(state: any): state is MetersState { return 'byMeterID' in state; } + +// /** +// * Helper function to determine what type of state was passed in +// * @param state The state to check +// * @returns Whether or not this is a GroupsState +// */ +// export function instanceOfGroupsState(state: any): state is GroupsState { return 'byGroupID' in state; } + +// /** +// * 3D helper function used to keep 3D redux state in sync with dropdown menus +// * @param dataProps used to extract relevant useSelect state values +// * @param allSelected all selected meters +// * @param oldSelected previously selected meters +// * @param difference integer value that represents the removed meter Or Group +// * @param meterOrGroup used to set whether a meter or group is currently active. +// * @param dispatch instance of the dispatch for altering redux state. +// */ +// function syncThreeDState( +// dataProps: any, +// allSelected: number[], +// oldSelected: number[], +// difference: number, +// meterOrGroup: MeterOrGroup, +// dispatch: Dispatch): void { + +// // checks to see if meter has been removed +// const meterOrGroupAdded = allSelected.length > oldSelected.length; +// const meterOrGroupRemoved = !meterOrGroupAdded; + +// //Check to see if potentially removed meterOrGroup is currently active. +// const meterOrGroupIsSelected = difference === dataProps.threeDState.meterOrGroupID; + +// if (meterOrGroupAdded && dataProps.chartToRender === ChartTypes.threeD) { +// // when a meter or group is selected, make it the currently active in 3D state. +// // only tracks when on 3d page. +// const addedMeterOrGroup = allSelected[allSelected.length - 1]; +// dispatch(changeMeterOrGroupInfo(addedMeterOrGroup, meterOrGroup)); +// } else if (meterOrGroupRemoved && meterOrGroupIsSelected) { +// // reset currently active threeD Meter or group when it is removed and is currently active. +// dispatch(changeMeterOrGroupInfo(undefined)); +// } +// } + +// const divBottomPadding: React.CSSProperties = { +// paddingBottom: '15px' +// }; +// const labelStyle: React.CSSProperties = { +// fontWeight: 'bold', +// margin: 0 +// }; +// const messages = defineMessages({ +// selectGroups: { id: 'select.groups' }, +// selectMeters: { id: 'select.meters' }, +// selectUnit: { id: 'select.unit' }, +// helpSelectGroups: { id: 'help.home.select.groups' }, +// helpSelectMeters: { id: 'help.home.select.meters' } +// }); \ No newline at end of file diff --git a/src/client/app/components/ChartDataSelectComponentWIP.tsx b/src/client/app/components/ChartDataSelectComponentWIP.tsx deleted file mode 100644 index 4ebcbd3a6..000000000 --- a/src/client/app/components/ChartDataSelectComponentWIP.tsx +++ /dev/null @@ -1,23 +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 * as React from 'react'; -import { MeterOrGroup } from '../types/redux/graph'; -import MeterAndGroupSelectComponent from './MeterAndGroupSelectComponent'; -import UnitSelectComponent from './UnitSelectComponent'; - -/** - * A component which allows the user to select which data should be displayed on the chart. - * @returns Chart data select element - */ -export default function ChartDataSelectComponentWIP() { - - return ( -
- - - -
- ); -} \ No newline at end of file diff --git a/src/client/app/components/DashboardComponent.tsx b/src/client/app/components/DashboardComponent.tsx index 43bc3d8fd..881b62988 100644 --- a/src/client/app/components/DashboardComponent.tsx +++ b/src/client/app/components/DashboardComponent.tsx @@ -4,15 +4,15 @@ import * as React from 'react'; import MapChartContainer from '../containers/MapChartContainer'; -import MultiCompareChartContainer from '../containers/MultiCompareChartContainer'; import { useAppSelector } from '../redux/hooks'; import { selectChartQueryArgs } from '../redux/selectors/dataSelectors'; import { ChartTypes } from '../types/redux/graph'; import BarChartComponent from './BarChartComponent'; +import HistoryComponent from './HistoryComponent'; import LineChartComponent from './LineChartComponent'; +import MultiCompareChartComponentWIP from './MultiCompareChartComponentWIP'; import ThreeDComponent from './ThreeDComponent'; import UIOptionsComponent from './UIOptionsComponent'; -import HistoryComponent from './HistoryComponent'; /** * React component that controls the dashboard @@ -40,7 +40,7 @@ export default function DashboardComponent() { {chartToRender === ChartTypes.line && } {chartToRender === ChartTypes.bar && } - {chartToRender === ChartTypes.compare && } + {chartToRender === ChartTypes.compare && } {chartToRender === ChartTypes.map && } {chartToRender === ChartTypes.threeD && }
diff --git a/src/client/app/components/ExportComponent.tsx b/src/client/app/components/ExportComponent.tsx index f25cfe652..d3b7395de 100644 --- a/src/client/app/components/ExportComponent.tsx +++ b/src/client/app/components/ExportComponent.tsx @@ -90,7 +90,7 @@ export default function ExportComponent() { const sortedReadings = _.sortBy(readings, item => item.startTimestamp, 'asc'); // Identifier for current meter. const meterIdentifier = metersDataById[meterId].identifier; - graphExport(sortedReadings, meterIdentifier, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.meter, errorBarState); + graphExport(sortedReadings, meterIdentifier, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.meters, errorBarState); } else { throw new Error(`Unacceptable condition: readingsData.readings is undefined for meter ${meterId}.`); } @@ -118,7 +118,7 @@ export default function ExportComponent() { const sortedReadings = _.sortBy(readings, item => item.startTimestamp, 'asc'); // Identifier for current group. const groupName = groupsDataById[groupId].name; - graphExport(sortedReadings, groupName, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.group); + graphExport(sortedReadings, groupName, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.groups); } else { throw new Error(`Unacceptable condition: readingsData.readings is undefined for group ${groupId}.`); } @@ -149,7 +149,7 @@ export default function ExportComponent() { const sortedReadings = _.sortBy(readings, item => item.startTimestamp, 'asc'); // Identifier for current meter. const meterIdentifier = metersDataById[meterId].identifier; - graphExport(sortedReadings, meterIdentifier, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.meter); + graphExport(sortedReadings, meterIdentifier, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.meters); } else if (!readingsData && !barMeterIsFetching) { throw new Error(`Unacceptable condition: readingsData.readings is undefined for meter ${meterId}.`); } @@ -177,7 +177,7 @@ export default function ExportComponent() { const sortedReadings = _.sortBy(readings, item => item.startTimestamp, 'asc'); // Identifier for current group. const groupName = groupsDataById[groupId].name; - graphExport(sortedReadings, groupName, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.group); + graphExport(sortedReadings, groupName, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.groups); } else if (!readingsData && !barGroupIsFetching) { throw new Error(`Unacceptable condition: readingsData.readings is undefined for group ${groupId}.`); } diff --git a/src/client/app/components/MenuModalComponent.tsx b/src/client/app/components/MenuModalComponent.tsx index 4feb7ba24..98f3e63ba 100644 --- a/src/client/app/components/MenuModalComponent.tsx +++ b/src/client/app/components/MenuModalComponent.tsx @@ -3,13 +3,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { Modal, ModalHeader, ModalBody, Button } from 'reactstrap'; -import UIOptionsContainer from '../containers/UIOptionsContainer'; -import HeaderButtonsComponent from './HeaderButtonsComponent'; +import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; import ReactTooltip from 'react-tooltip'; -import { useState } from 'react'; +import { Button, Modal, ModalBody, ModalHeader } from 'reactstrap'; import getPage from '../utils/getPage'; +import HeaderButtonsComponent from './HeaderButtonsComponent'; +import UIOptionsComponent from './UIOptionsComponent'; /** * React component to define the collapsed menu modal @@ -34,7 +34,7 @@ export default function MenuModalComponent() { {/* Only render graph options if on the graph page */} {getPage() === '' && - + } diff --git a/src/client/app/components/MultiCompareChartComponent.tsx b/src/client/app/components/MultiCompareChartComponent.tsx index 915e1e75e..2562a0453 100644 --- a/src/client/app/components/MultiCompareChartComponent.tsx +++ b/src/client/app/components/MultiCompareChartComponent.tsx @@ -5,8 +5,7 @@ import * as React from 'react'; import { UncontrolledAlert } from 'reactstrap'; import { FormattedMessage } from 'react-intl'; -import CompareChartContainer from '../containers/CompareChartContainer'; -import { CompareEntity } from '../containers/MultiCompareChartContainer'; +import CompareChartContainer, { CompareEntity } from '../containers/CompareChartContainer'; interface MultiCompareChartProps { selectedCompareEntities: CompareEntity[]; diff --git a/src/client/app/components/MultiCompareChartComponentWIP.tsx b/src/client/app/components/MultiCompareChartComponentWIP.tsx new file mode 100644 index 000000000..1837cffd1 --- /dev/null +++ b/src/client/app/components/MultiCompareChartComponentWIP.tsx @@ -0,0 +1,188 @@ +/* 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 { FormattedMessage } from 'react-intl'; +import { UncontrolledAlert } from 'reactstrap'; +import CompareChartContainer, { CompareEntity } from '../containers/CompareChartContainer'; +import { selectGraphAreaNormalization, selectSelectedGroups, selectSelectedMeters, selectSortingOrder } from '../reducers/graph'; +import { groupsApi } from '../redux/api/groupsApi'; +import { metersApi } from '../redux/api/metersApi'; +import { readingsApi } from '../redux/api/readingsApi'; +import { useAppSelector } from '../redux/hooks'; +import { selectChartQueryArgs } from '../redux/selectors/dataSelectors'; +import { SortingOrder } from '../utils/calculateCompare'; +import { AreaUnitType } from '../utils/getAreaUnitConversion'; + +export interface MultiCompareChartProps { + selectedCompareEntities: CompareEntity[]; + errorEntities: string[]; +} + +/** + * Component that defines compare chart + * @returns Multi Compare Chart element + */ +export default function MultiCompareChartComponentWIP() { + const areaNormalization = useAppSelector(selectGraphAreaNormalization) + const sortingOrder = useAppSelector(selectSortingOrder) + const selectedMeters = useAppSelector(selectSelectedMeters) + const selectedGroups = useAppSelector(selectSelectedGroups) + const { compare: { meterArgs, meterSkipQuery, groupSkipQuery, groupsArgs } } = useAppSelector(selectChartQueryArgs) + const { data: meterReadings = {} } = readingsApi.useCompareQuery(meterArgs, { skip: meterSkipQuery }) + const { data: groupReadings = {} } = readingsApi.useCompareQuery(groupsArgs, { skip: groupSkipQuery }) + + const { data: meterDataByID = {} } = metersApi.useGetMetersQuery() + const { data: groupDataByID = {} } = groupsApi.useGetGroupsQuery() + + // TODO SEEMS UNUSED, kept due to uncertainty when migrating to RTK VERIFY BEHAVIOR + const errorEntities: string[] = []; + let selectedCompareEntities: CompareEntity[] = [] + + Object.entries(meterReadings).forEach(([key, value]) => { + const name = meterDataByID[Number(key)].name + const identifier = meterDataByID[Number(key)].identifier + + const areaNormValid = (!areaNormalization || (meterDataByID[Number(key)].area > 0 && meterDataByID[Number(key)].areaUnit !== AreaUnitType.none)) + if (areaNormValid && selectedMeters.includes(Number(key))) { + const change = calculateChange(value.curr_use, value.prev_use); + const entity: CompareEntity = { + id: Number(key), + isGroup: false, + name, + identifier, + change, + currUsage: value.curr_use, + prevUsage: value.prev_use + }; + selectedCompareEntities.push(entity); + } + }) + Object.entries(groupReadings).forEach(([key, value]) => { + const identifier = groupDataByID[Number(key)].name + const areaNormValid = (!areaNormalization || (groupDataByID[Number(key)].area > 0 && groupDataByID[Number(key)].areaUnit !== AreaUnitType.none)) + if (areaNormValid && selectedGroups.includes(Number(key))) { + const change = calculateChange(value.curr_use, value.prev_use); + const entity: CompareEntity = { + id: Number(key), + isGroup: false, + name: identifier, + identifier, + change, + currUsage: value.curr_use, + prevUsage: value.prev_use + }; + selectedCompareEntities.push(entity); + } + }) + + selectedCompareEntities = sortIDs(selectedCompareEntities, sortingOrder) + + + // Compute how much space should be used in the bootstrap grid system + let size = 3; + const numSelectedItems = selectedCompareEntities.length; + if (numSelectedItems < 3) { + size = numSelectedItems; + } + const childClassName = `col-12 col-lg-${12 / size}`; + const centeredStyle = { + marginTop: '20%' + }; + + return ( +
+
+ {errorEntities.map(name => +
+ + {name} + +
+ )} +
+
+ {selectedCompareEntities.map(compareEntity => +
+ {/* 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 */} + +
+ )} +
+ {selectedCompareEntities.length === 0 && +
+ +
+ } +
+ ); +} + +/** + * + * @param currentPeriodUsage TODO temp to appease linter fix Later + * @param usedToThisPointLastTimePeriod TODO temp to appease linter fix Later + * @returns TODO temp to appease linter fix Later + */ +function calculateChange(currentPeriodUsage: number, usedToThisPointLastTimePeriod: number): number { + return -1 + (currentPeriodUsage / usedToThisPointLastTimePeriod); +} + + + +/** + * @param ids TODO temp to appease linter fix Later + * @param sortingOrder TODO temp to appease linter fix Later + * @returns TODO temp to appease linter fix Later + */ +function sortIDs(ids: CompareEntity[], sortingOrder: SortingOrder): CompareEntity[] { + switch (sortingOrder) { + case SortingOrder.Alphabetical: + ids.sort((a, b) => { + const identifierA = a.identifier.toLowerCase(); + const identifierB = b.identifier.toLowerCase(); + if (identifierA < identifierB) { + return -1; + } + if (identifierA > identifierB) { + return 1; + } + return 0; + }); + break; + case SortingOrder.Ascending: + ids.sort((a, b) => { + if (a.change < b.change) { + return -1; + } + if (a.change > b.change) { + return 1; + } + return 0; + }); + break; + case SortingOrder.Descending: + ids.sort((a, b) => { + if (a.change > b.change) { + return -1; + } + if (a.change < b.change) { + return 1; + } + return 0; + }); + break; + default: + throw new Error(`Unknown sorting order: ${sortingOrder}`); + } + return ids; +} diff --git a/src/client/app/components/RouteComponent.tsx b/src/client/app/components/RouteComponent.tsx index 8d5ebc504..01090944e 100644 --- a/src/client/app/components/RouteComponent.tsx +++ b/src/client/app/components/RouteComponent.tsx @@ -10,7 +10,6 @@ import { browserHistory } from '../utils/history'; import * as _ from 'lodash'; import * as moment from 'moment'; import HomeComponent from './HomeComponent'; -import LoginContainer from '../containers/LoginContainer'; import AdminComponent from './admin/AdminComponent'; import { LinkOptions } from '../actions/graph'; import { hasToken, deleteToken } from '../utils/token'; @@ -33,6 +32,7 @@ import MetersDetailComponent from './meters/MetersDetailComponent'; import GroupsDetailComponent from './groups/GroupsDetailComponent'; import ConversionsDetailComponent from './conversion/ConversionsDetailComponent'; import * as queryString from 'query-string'; +import LoginComponent from './LoginComponent'; interface RouteProps { barStacking: boolean; @@ -289,7 +289,7 @@ export default class RouteComponent extends React.Component { <> - + this.requireAuth(AdminComponent())} /> this.requireRole(UserRole.CSV, )} /> this.checkAuth()} /> diff --git a/src/client/app/components/UIOptionsComponent.tsx b/src/client/app/components/UIOptionsComponent.tsx index 0358411bb..a07973bd7 100644 --- a/src/client/app/components/UIOptionsComponent.tsx +++ b/src/client/app/components/UIOptionsComponent.tsx @@ -11,7 +11,6 @@ import { useAppSelector } from '../redux/hooks'; import { ChartTypes } from '../types/redux/graph'; import AreaUnitSelectComponent from './AreaUnitSelectComponent'; import BarControlsComponent from './BarControlsComponent'; -import ChartDataSelectComponentWIP from './ChartDataSelectComponentWIP'; import ChartSelectComponent from './ChartSelectComponent'; import CompareControlsComponent from './CompareControlsComponent'; import DateRangeComponent from './DateRangeComponent'; @@ -19,6 +18,7 @@ import ErrorBarComponent from './ErrorBarComponent'; import GraphicRateMenuComponent from './GraphicRateMenuComponent'; import MapControlsComponent from './MapControlsComponent'; import ThreeDSelectComponent from './ReadingsPerDaySelectComponent'; +import ChartDataSelectComponent from './ChartDataSelectComponent'; /** @@ -30,7 +30,7 @@ export default function UIOptionsComponent() { return (
- + diff --git a/src/client/app/components/conversion/ConversionsDetailComponent.tsx b/src/client/app/components/conversion/ConversionsDetailComponent.tsx index 7ae4f43cc..56a6209a7 100644 --- a/src/client/app/components/conversion/ConversionsDetailComponent.tsx +++ b/src/client/app/components/conversion/ConversionsDetailComponent.tsx @@ -2,23 +2,22 @@ * 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 { useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; +import { useSelector } from 'react-redux'; +import { ConversionData } from 'types/redux/conversions'; +import { fetchConversionsDetailsIfNeeded } from '../../actions/conversions'; +import HeaderComponent from '../../components/HeaderComponent'; +import SpinnerComponent from '../../components/SpinnerComponent'; import FooterContainer from '../../containers/FooterContainer'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; -import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import * as React from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { useEffect } from 'react'; +import { selectConversionsDetails } from '../../redux/api/conversionsApi'; +import { useAppDispatch, useAppSelector } from '../../redux/hooks'; import { State } from '../../types/redux/state'; -import { fetchConversionsDetailsIfNeeded } from '../../actions/conversions'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; import ConversionViewComponent from './ConversionViewComponent'; import CreateConversionModalComponent from './CreateConversionModalComponent'; -import { ConversionData } from 'types/redux/conversions'; -import SpinnerComponent from '../../components/SpinnerComponent'; -import HeaderComponent from '../../components/HeaderComponent'; -import { Dispatch } from '../../types/redux/actions'; -import { useAppSelector } from '../../redux/hooks'; -import { selectConversionsDetails } from '../../redux/api/conversionsApi'; /** * Defines the conversions page card view @@ -27,7 +26,7 @@ import { selectConversionsDetails } from '../../redux/api/conversionsApi'; export default function ConversionsDetailComponent() { // The route stops you from getting to this page if not an admin. - const dispatch: Dispatch = useDispatch(); + const dispatch = useAppDispatch(); useEffect(() => { // Makes async call to conversions API for conversions details if one has not already been made somewhere else, stores conversion by ids in state diff --git a/src/client/app/components/meters/EditMeterModalComponentWIP.tsx b/src/client/app/components/meters/EditMeterModalComponentWIP.tsx index 8b985cb74..0a777a2cc 100644 --- a/src/client/app/components/meters/EditMeterModalComponentWIP.tsx +++ b/src/client/app/components/meters/EditMeterModalComponentWIP.tsx @@ -44,7 +44,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr // to have a single selector per modal instance. Memo ensures that this is a stable reference const selectGraphicUnitCompatibility = React.useMemo(makeSelectGraphicUnitCompatibility, []) // The current meter's state of meter being edited. It should always be valid. - const meterState = useAppSelector(state => selectMeterDataWithID(state, props.meter.id)); + const meterState = useAppSelector(state => selectMeterDataWithID(state, props.meter.id)) as MeterData; const [localMeterEdits, setLocalMeterEdits] = useState(_.cloneDeep(meterState)); const { compatibleGraphicUnits, diff --git a/src/client/app/components/unit/UnitsDetailComponent.tsx b/src/client/app/components/unit/UnitsDetailComponent.tsx index afaa8b069..c1a958931 100644 --- a/src/client/app/components/unit/UnitsDetailComponent.tsx +++ b/src/client/app/components/unit/UnitsDetailComponent.tsx @@ -1,6 +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 { QueryStatus } from '@reduxjs/toolkit/query'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import SpinnerComponent from '../../components/SpinnerComponent'; @@ -10,8 +11,6 @@ import { useAppSelector } from '../../redux/hooks'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateUnitModalComponent from './CreateUnitModalComponent'; import UnitViewComponent from './UnitViewComponent'; -import { QueryStatus } from '@reduxjs/toolkit/query'; -import { UnitData } from 'types/redux/units'; /** * Defines the units page card view @@ -50,9 +49,9 @@ export default function UnitsDetailComponent() { {/* Create a UnitViewComponent for each UnitData in Units State after sorting by identifier */} { Object.values(unitDataById) - .sort((unitA: UnitData, unitB: UnitData) => (unitA.identifier.toLowerCase() > unitB.identifier.toLowerCase()) ? 1 : + .sort((unitA, unitB) => (unitA.identifier.toLowerCase() > unitB.identifier.toLowerCase()) ? 1 : ((unitB.identifier.toLowerCase() > unitA.identifier.toLowerCase()) ? -1 : 0)) - .map((unitData: UnitData) => ( + .map(unitData => ( dispatch(changeGraphZoomIfNeeded(timeInterval)) - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(DashboardComponent); diff --git a/src/client/app/containers/LoginContainer.tsx b/src/client/app/containers/LoginContainer.tsx deleted file mode 100644 index ef2a4dc5b..000000000 --- a/src/client/app/containers/LoginContainer.tsx +++ /dev/null @@ -1,21 +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 { User } from '../types/items'; -import LoginComponent from '../components/LoginComponent'; -import { Dispatch } from '../types/redux/actions'; -import { currentUserSlice } from '../reducers/currentUser'; - -/** - * A container that does data fetching for FooterComponent and connects it to the redux store. - */ -function mapDispatchToProps(dispatch: Dispatch) { - return { - saveCurrentUser: (profile: User) => dispatch(currentUserSlice.actions.receiveCurrentUser(profile)) - }; -} - -export default connect(null, mapDispatchToProps)(LoginComponent); - diff --git a/src/client/app/containers/MultiCompareChartContainer.ts b/src/client/app/containers/MultiCompareChartContainer.ts deleted file mode 100644 index 3acc2ec4a..000000000 --- a/src/client/app/containers/MultiCompareChartContainer.ts +++ /dev/null @@ -1,201 +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 MultiCompareChartComponent from '../components/MultiCompareChartComponent'; -import { State } from '../types/redux/state'; -import { calculateCompareShift, SortingOrder } from '../utils/calculateCompare'; -import { CompareReadingsData } from '../types/redux/compareReadings'; -import { TimeInterval } from '../../../common/TimeInterval'; -import * as moment from 'moment'; -import { AreaUnitType } from '../utils/getAreaUnitConversion'; - -export interface CompareEntity { - id: number; - isGroup: boolean; - name: string; - identifier: string; - change: number; - currUsage: number; - prevUsage: number; - prevTotalUsage?: number; -} - -let errorEntities: string[] = []; - -function mapStateToProps(state: State) { - errorEntities = []; - const meters: CompareEntity[] = getDataForIDs(state.graph.selectedMeters, false, state); - const groups: CompareEntity[] = getDataForIDs(state.graph.selectedGroups, true, state); - const compareEntities: CompareEntity[] = meters.concat(groups); - const sortingOrder = state.graph.compareSortingOrder; - return { - selectedCompareEntities: sortIDs(compareEntities, sortingOrder), - errorEntities: errorEntities as string[] - }; -} - -function getDataForIDs(ids: number[], isGroup: boolean, state: State): CompareEntity[] { - const timeInterval = state.graph.compareTimeInterval; - const comparePeriod = state.graph.comparePeriod; - const compareShift = calculateCompareShift(comparePeriod); - const entities: CompareEntity[] = []; - for (const id of ids) { - let name: string; - let identifier: string; - let readingsData: CompareReadingsData | undefined; - if (isGroup) { - name = getGroupName(state, id); - // This is a bit of a kluge but the compare graphic uses the identifier. Since it does - // not easily know if it is group or meter, we set the identifier for the group to be - // the name to make it easier. - identifier = name; - readingsData = getGroupReadingsData(state, id, timeInterval, compareShift); - } else { - name = getMeterName(state, id); - identifier = getMeterIdentifier(state, id); - readingsData = getMeterReadingsData(state, id, timeInterval, compareShift); - } - if (isReadingsDataValid(readingsData) && areaNormalizationValid(state, id, isGroup)) { - /* eslint-disable @typescript-eslint/no-non-null-assertion */ - const currUsage = readingsData!.curr_use!; - const prevUsage = readingsData!.prev_use!; - const change = calculateChange(currUsage, prevUsage); - const entity: CompareEntity = { id, isGroup, name, identifier, change, currUsage, prevUsage }; - entities.push(entity); - /* eslint-enable @typescript-eslint/no-non-null-assertion */ - } - } - return entities; -} - -function getGroupName(state: State, groupID: number): string { - if (state.groups.byGroupID[groupID] === undefined) { - return ''; - } - return state.groups.byGroupID[groupID].name; -} - -function getMeterName(state: State, meterID: number): string { - if (state.meters.byMeterID[meterID] === undefined) { - return ''; - } - return state.meters.byMeterID[meterID].name; -} - -function getMeterIdentifier(state: State, meterID: number): string { - if (state.meters.byMeterID[meterID] === undefined) { - return ''; - } - return state.meters.byMeterID[meterID].identifier; -} - -function getGroupReadingsData(state: State, groupID: number, timeInterval: TimeInterval, - compareShift: moment.Duration): CompareReadingsData | undefined { - const unitID = state.graph.selectedUnit; - let readingsData: CompareReadingsData | undefined; - const readingsDataByID = state.readings.compare.byGroupID[groupID]; - if (readingsDataByID !== undefined) { - const readingsDataByTimeInterval = readingsDataByID[timeInterval.toString()]; - if (readingsDataByTimeInterval !== undefined) { - const readingsDataByCompareShift = readingsDataByTimeInterval[compareShift.toISOString()]; - if (readingsDataByCompareShift !== undefined) { - const readingsDataByUnitID = readingsDataByCompareShift[unitID]; - if (readingsDataByUnitID !== undefined) { - readingsData = readingsDataByUnitID; - } - } - } - } - return readingsData; -} - -function getMeterReadingsData(state: State, meterID: number, timeInterval: TimeInterval, - compareShift: moment.Duration): CompareReadingsData | undefined { - const unitID = state.graph.selectedUnit; - let readingsData: CompareReadingsData | undefined; - const readingsDataByID = state.readings.compare.byMeterID[meterID]; - if (readingsDataByID !== undefined) { - const readingsDataByTimeInterval = readingsDataByID[timeInterval.toString()]; - if (readingsDataByTimeInterval !== undefined) { - const readingsDataByCompareShift = readingsDataByTimeInterval[compareShift.toISOString()]; - if (readingsDataByCompareShift !== undefined) { - const readingsDataByUnitID = readingsDataByCompareShift[unitID]; - if (readingsDataByUnitID !== undefined) { - readingsData = readingsDataByUnitID; - } - } - } - } - return readingsData; -} - -function isReadingsDataValid(readingsData: CompareReadingsData | undefined): boolean { - return readingsData !== undefined && !readingsData.isFetching && readingsData.curr_use !== undefined && readingsData.prev_use !== undefined; -} - -function calculateChange(currentPeriodUsage: number, usedToThisPointLastTimePeriod: number): number { - return -1 + (currentPeriodUsage / usedToThisPointLastTimePeriod); -} - -function sortIDs(ids: CompareEntity[], sortingOrder: SortingOrder): CompareEntity[] { - switch (sortingOrder) { - case SortingOrder.Alphabetical: - ids.sort((a, b) => { - const identifierA = a.identifier.toLowerCase(); - const identifierB = b.identifier.toLowerCase(); - if (identifierA < identifierB) { - return -1; - } - if (identifierA > identifierB) { - return 1; - } - return 0; - }); - break; - case SortingOrder.Ascending: - ids.sort((a, b) => { - if (a.change < b.change) { - return -1; - } - if (a.change > b.change) { - return 1; - } - return 0; - }); - break; - case SortingOrder.Descending: - ids.sort((a, b) => { - if (a.change > b.change) { - return -1; - } - if (a.change < b.change) { - return 1; - } - return 0; - }); - break; - default: - throw new Error(`Unknown sorting order: ${sortingOrder}`); - } - return ids; -} - -function areaNormalizationValid(state: State, id: number, isGroup: boolean): boolean { - if (!state.graph.areaNormalization) { - return true; - } - // normalization is valid if the group/meter has a nonzero area and an area unit - if (isGroup) { - if (state.groups.byGroupID[id].area > 0 && state.groups.byGroupID[id].areaUnit !== AreaUnitType.none) { - return true; - } - } - if (state.meters.byMeterID[id].area > 0 && state.meters.byMeterID[id].areaUnit !== AreaUnitType.none) { - return true; - } - return false; -} - -export default connect(mapStateToProps)(MultiCompareChartComponent); diff --git a/src/client/app/containers/RouteContainer.ts b/src/client/app/containers/RouteContainer.ts deleted file mode 100644 index 51eb80e8a..000000000 --- a/src/client/app/containers/RouteContainer.ts +++ /dev/null @@ -1,45 +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 RouteComponent from '../components/RouteComponent'; -import { Dispatch } from '../types/redux/actions'; -import { State } from '../types/redux/state'; -import { changeOptionsFromLink, LinkOptions } from '../actions/graph'; -import { isRoleAdmin } from '../utils/hasPermissions'; -import { UserRole } from '../types/items'; -import { graphSlice } from '../reducers/graph'; -import { currentUserSlice } from '../reducers/currentUser'; - -function mapStateToProps(state: State) { - const currentUser = state.currentUser.profile; - let loggedInAsAdmin = false; - let role: UserRole | null = null; - if (currentUser !== null) { - loggedInAsAdmin = isRoleAdmin(currentUser.role); - role = currentUser.role; - } - - return { - barStacking: state.graph.barStacking, - selectedLanguage: state.options.selectedLanguage, - loggedInAsAdmin, - role, - // true if the chartlink rendering has been done. - renderOnce: state.graph.renderOnce, - areaNormalization: state.graph.areaNormalization, - minMax: state.graph.showMinMax - }; -} - -function mapDispatchToProps(dispatch: Dispatch) { - return { - changeOptionsFromLink: (options: LinkOptions) => dispatch(changeOptionsFromLink(options)), - clearCurrentUser: () => dispatch(currentUserSlice.actions.clearCurrentUser()), - // Set the state to indicate chartlinks have been rendered. - changeRenderOnce: () => dispatch(graphSlice.actions.confirmGraphRenderOnce()) - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(RouteComponent); diff --git a/src/client/app/containers/UIOptionsContainer.ts b/src/client/app/containers/UIOptionsContainer.ts deleted file mode 100644 index d17830ac0..000000000 --- a/src/client/app/containers/UIOptionsContainer.ts +++ /dev/null @@ -1,39 +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 * as moment from 'moment'; -import { connect } from 'react-redux'; -import UIOptionsComponent from '../components/UIOptionsComponent'; -import { - changeBarDuration, - changeCompareGraph, - changeCompareSortingOrder -} from '../actions/graph'; -import { Dispatch } from '../types/redux/actions'; -import { State } from '../types/redux/state'; -import {ComparePeriod, SortingOrder} from '../utils/calculateCompare'; -import { graphSlice } from '../reducers/graph'; - -function mapStateToProps(state: State) { - return { - chartToRender: state.graph.chartToRender, - areaNormalization: state.graph.areaNormalization, - barStacking: state.graph.barStacking, - barDuration: state.graph.barDuration, - comparePeriod: state.graph.comparePeriod, - compareSortingOrder: state.graph.compareSortingOrder - }; -} - -function mapDispatchToProps(dispatch: Dispatch) { - return { - changeDuration: (barDuration: moment.Duration) => dispatch(changeBarDuration(barDuration)), - changeBarStacking: () => dispatch(graphSlice.actions.changeBarStacking()), - changeCompareGraph: (comparePeriod: ComparePeriod) => dispatch(changeCompareGraph(comparePeriod)), - changeCompareSortingOrder: (sortingOrder: SortingOrder) => dispatch(changeCompareSortingOrder(sortingOrder)) - }; -} - - -export default connect(mapStateToProps, mapDispatchToProps)(UIOptionsComponent); diff --git a/src/client/app/reducers/graph.ts b/src/client/app/reducers/graph.ts index 71f8da8c6..739265737 100644 --- a/src/client/app/reducers/graph.ts +++ b/src/client/app/reducers/graph.ts @@ -268,7 +268,8 @@ export const graphSlice = createSlice({ selectThreeDMeterOrGroupID: state => state.threeD.meterOrGroupID, selectThreeDReadingInterval: state => state.threeD.readingInterval, selectLineGraphRate: state => state.lineGraphRate, - selectAreaUnit: state => state.selectedAreaUnit + selectAreaUnit: state => state.selectedAreaUnit, + selectSortingOrder: state => state.compareSortingOrder } }) @@ -287,7 +288,8 @@ export const { selectThreeDMeterOrGroupID, selectThreeDReadingInterval, selectLineGraphRate, - selectAreaUnit + selectAreaUnit, + selectSortingOrder } = graphSlice.selectors // actionCreators exports diff --git a/src/client/app/reducers/index.ts b/src/client/app/reducers/index.ts index 42fea4214..9b753b34d 100644 --- a/src/client/app/reducers/index.ts +++ b/src/client/app/reducers/index.ts @@ -3,7 +3,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { combineReducers } from 'redux'; -import compareReadings from './compareReadings'; import maps from './maps'; import { adminSlice } from './admin'; import { versionSlice } from './version'; @@ -15,11 +14,8 @@ import { baseApi } from '../redux/api/baseApi'; import { graphSlice } from './graph'; export const rootReducer = combineReducers({ - readings: combineReducers({ - compare: compareReadings - }), - graph: graphSlice.reducer, maps, + graph: graphSlice.reducer, admin: adminSlice.reducer, version: versionSlice.reducer, currentUser: currentUserSlice.reducer, diff --git a/src/client/app/redux/api/groupsApi.ts b/src/client/app/redux/api/groupsApi.ts index 0d0260c73..ab4ab74a9 100644 --- a/src/client/app/redux/api/groupsApi.ts +++ b/src/client/app/redux/api/groupsApi.ts @@ -97,4 +97,14 @@ export const groupsApi = baseApi.injectEndpoints({ }) }) -export const selectGroupDataById = groupsApi.endpoints.getGroups.select(); \ No newline at end of file +export const selectGroupDataById = groupsApi.endpoints.getGroups.select(); + +export const selectGroupDataWithID = (state: RootState, groupId: number): GroupData | undefined => { + const { data: groupDataById = {} } = selectGroupDataById(state) + return groupDataById[groupId] +} + +export const selectGroupNameWithID = (state: RootState, groupId: number) => { + const groupInfo = selectGroupDataWithID(state, groupId) + return groupInfo ? groupInfo.name : ''; +} \ No newline at end of file diff --git a/src/client/app/redux/api/metersApi.ts b/src/client/app/redux/api/metersApi.ts index bfaa973fc..1a34ecf39 100644 --- a/src/client/app/redux/api/metersApi.ts +++ b/src/client/app/redux/api/metersApi.ts @@ -1,12 +1,12 @@ import * as _ from 'lodash'; +import { NamedIDItem } from 'types/items'; +import { RawReadings } from 'types/readings'; import { TimeInterval } from '../../../../common/TimeInterval'; +import { RootState } from '../../store'; import { MeterData, MeterDataByID } from '../../types/redux/meters'; import { durationFormat } from '../../utils/durationFormat'; import { baseApi } from './baseApi'; -import { NamedIDItem } from 'types/items'; -import { CompareReadings, RawReadings } from 'types/readings'; import { conversionsApi } from './conversionsApi'; -import { RootState } from '../../store'; export const metersApi = baseApi.injectEndpoints({ @@ -53,31 +53,59 @@ export const metersApi = baseApi.injectEndpoints({ }), rawLineReadings: builder.query({ query: ({ meterID, timeInterval }) => `api/readings/line/raw/meter/${meterID}?timeInterval=${timeInterval.toString()}` - }), - /** - * Gets compare readings for meters for the given current time range and a shift for previous time range - * @param meterIDs The meter IDs to get readings for - * @param timeInterval start and end of current/this compare period - * @param shift how far to shift back in time from current period to previous period - * @param unitID The unit id that the reading should be returned in, i.e., the graphic unit - * @returns CompareReadings in sorted order - */ - meterCompareReadings: builder.query({ - query: ({ meterIDs, timeInterval, shift, unitID }) => { - const stringifiedIDs = meterIDs.join(','); - const currStart = timeInterval.getStartTimestamp().toISOString(); - const currEnd = timeInterval.getEndTimestamp().toISOString(); - const apiURL = `/api/compareReadings/meters/${stringifiedIDs}?` - const params = `curr_start=${currStart}&curr_end=${currEnd}&shift=${shift.toISOString()}&graphicUnitId=${unitID.toString()}` - return `${apiURL}${params}` - } }) - }) }) +/** + * Selects the meter data associated with a given meter ID from the Redux state. + * @param {RootState} state - The current state of the Redux store. + * @returns The latest query state for the given which can be destructured for the dataById + * @example + * const endpointState = useAppSelector(state => selectMeterDataById(state)) + * const meterDataByID = endpointState.data + * or + * const { data: meterDataByID } = useAppSelector(state => selectMeterDataById(state)) + */ export const selectMeterDataById = metersApi.endpoints.getMeters.select() -export const selectMeterDataWithID = (state: RootState, meterID: number) => { - const { data: meterDataByID = {} } = selectMeterDataById(state) - return meterDataByID[meterID] + + +/** + * Selects the meter data associated with a given meter ID from the Redux state. + * @param state - The current state of the Redux store. + * @param meterID - The unique identifier for the meter. + * @returns The data for the specified meter or undefined if not found. + * @example + * const meterData = useAppSelector(state => selectMeterDataWithID(state, 42)) + */ +export const selectMeterDataWithID = (state: RootState, meterID: number): MeterData | undefined => { + const { data: meterDataByID = {} } = selectMeterDataById(state); + return meterDataByID[meterID]; +} + + +/** + * Selects the name of the meter associated with a given meter ID from the Redux state. + * @param state - The current state of the Redux store. + * @param meterID - The unique identifier for the meter. + * @returns The name of the specified meter or an empty string if not found. + * @example + * const meterName = useAppSelector(state => selectMeterNameWithID(state, 42)) + */ +export const selectMeterNameWithID = (state: RootState, meterID: number) => { + const meterInfo = selectMeterDataWithID(state, meterID); + return meterInfo ? meterInfo.name : ''; +} + +/** + * Selects the identifier (not the meter ID) of the meter associated with a given meter ID from the Redux state. + * @param state - The current state of the Redux store. + * @param meterID - The unique identifier for the meter. + * @returns The identifier for the specified meter or an empty string if not found. + * @example + * const meterIdentifier = useAppSelector(state => selectMeterIdentifier(state, 42)) + */ +export const selectMeterIdentifierWithID = (state: RootState, meterID: number) => { + const meterInfo = selectMeterDataWithID(state, meterID); + return meterInfo ? meterInfo.identifier : ''; } \ No newline at end of file diff --git a/src/client/app/redux/api/readingsApi.ts b/src/client/app/redux/api/readingsApi.ts index 8bcb97c93..4ffb55df3 100644 --- a/src/client/app/redux/api/readingsApi.ts +++ b/src/client/app/redux/api/readingsApi.ts @@ -1,12 +1,9 @@ import * as _ from 'lodash'; -import { BarReadingApiArgs, LineReadingApiArgs, ThreeDReadingApiArgs } from '../../redux/selectors/dataSelectors'; +import { BarReadingApiArgs, CompareReadingApiArgs, LineReadingApiArgs, ThreeDReadingApiArgs } from '../../redux/selectors/dataSelectors'; import { RootState } from '../../store'; -import { BarReadings, LineReadings, ThreeDReading } from '../../types/readings'; +import { BarReadings, CompareReadings, LineReadings, ThreeDReading } from '../../types/readings'; import { baseApi } from './baseApi'; - - - export const readingsApi = baseApi.injectEndpoints({ endpoints: builder => ({ // threeD: the queryEndpoint name // builder.query @@ -113,8 +110,41 @@ export const readingsApi = baseApi.injectEndpoints({ const queryArgs = `timeInterval=${timeInterval}&barWidthDays=${barWidthDays}&graphicUnitId=${unitID}` const endpointURL = `${endpoint}${queryArgs}` const { data, error } = await baseQuery(endpointURL) - return error ? { error } : { data: data as LineReadings } + return error ? { error } : { data: data as BarReadings } } + }), + /** + * Gets compare readings for meters for the given current time range and a shift for previous time range + * @param meterIDs The meter IDs to get readings for + * @param timeInterval start and end of current/this compare period + * @param shift how far to shift back in time from current period to previous period + * @param unitID The unit id that the reading should be returned in, i.e., the graphic unit + * @returns CompareReadings in sorted order + */ + compare: builder.query({ + serializeQueryArgs: ({ queryArgs }) => _.omit(queryArgs, 'ids'), + merge: (currentCacheData, responseData) => { Object.assign(currentCacheData, responseData) }, + forceRefetch: ({ currentArg, endpointState }) => { + const currentData = endpointState?.data ? Object.keys(endpointState.data).map(Number) : undefined + if (!currentData) { return true } + const dataInCache = currentArg?.ids.every(id => currentData.includes(id)) + return !dataInCache ? true : false + }, + queryFn: async (args, queryApi, _extra, baseQuery) => { + const { ids, curr_start, curr_end, shift, unitID, meterOrGroup } = args + const state = queryApi.getState() as RootState + const cachedData = readingsApi.endpoints.compare.select(args)(state).data + const cachedIDs = cachedData ? Object.keys(cachedData).map(Number) : [] + const idsToFetch = _.difference(ids, cachedIDs).join(',') + const apiURL = `/api/compareReadings/${meterOrGroup}/${idsToFetch}?` + const params = `curr_start=${curr_start}&curr_end=${curr_end}&shift=${shift}&graphicUnitId=${unitID}` + const URL = `${apiURL}${params}` + const { data, error } = await baseQuery(URL) + return error ? { error } : { data: data as CompareReadings } + } + // } }) + }) + }) \ No newline at end of file diff --git a/src/client/app/redux/selectors/adminSelectors.ts b/src/client/app/redux/selectors/adminSelectors.ts index 594655bb1..6c522fb39 100644 --- a/src/client/app/redux/selectors/adminSelectors.ts +++ b/src/client/app/redux/selectors/adminSelectors.ts @@ -88,7 +88,7 @@ export const selectUnitName = createSelector( selectUnitDataById, selectMeterDataWithID, ({ data: unitDataById = {} }, meterData) => { - const unitName = (Object.keys(unitDataById).length === 0 || meterData.unitId === -99) ? + const unitName = (Object.keys(unitDataById).length === 0 || !meterData || meterData.unitId === -99) ? noUnitTranslated().identifier : unitDataById[meterData.defaultGraphicUnit].identifier return unitName } @@ -109,7 +109,7 @@ export const selectGraphicName = createSelector( selectUnitDataById, selectMeterDataWithID, ({ data: unitDataById = {} }, meterData) => { - const graphicName = (Object.keys(unitDataById).length === 0 || meterData.defaultGraphicUnit === -99) ? + const graphicName = (Object.keys(unitDataById).length === 0 || !meterData || meterData.defaultGraphicUnit === -99) ? noUnitTranslated().identifier : unitDataById[meterData.defaultGraphicUnit].identifier return graphicName } diff --git a/src/client/app/redux/selectors/dataSelectors.ts b/src/client/app/redux/selectors/dataSelectors.ts index a50386427..d23d3a6b6 100644 --- a/src/client/app/redux/selectors/dataSelectors.ts +++ b/src/client/app/redux/selectors/dataSelectors.ts @@ -7,6 +7,7 @@ import { readingsApi } from '../api/readingsApi'; import { MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; import { selectIsLoggedInAsAdmin } from './authSelectors'; +import { calculateCompareShift } from '../../utils/calculateCompare'; // Props that are passed to plotly components export interface ChartMultiQueryProps { @@ -21,18 +22,30 @@ export interface ChartMultiQueryArgs { meta: ChartQueryArgsMeta } -// query args that all graphs share +// query args that 'most' graphs share export interface commonArgsMultiID { ids: number[]; timeInterval: string; unitID: number; meterOrGroup: MeterOrGroup; } +export interface commonArgsSingleID extends Omit { id: number } + +// endpoint specific args +export interface LineReadingApiArgs extends commonArgsMultiID { } +export interface BarReadingApiArgs extends commonArgsMultiID { barWidthDays: number } +export interface ThreeDReadingApiArgs extends commonArgsSingleID { readingInterval: ReadingInterval } +export interface CompareReadingApiArgs extends Omit { + // compare breaks the timeInterval pattern query pattern therefore omit and add required for api. + shift: string, + curr_start: string, + curr_end: string +} +// { meterIDs: number[], timeInterval: TimeInterval, shift: moment.Duration, unitID: number } export interface ChartSingleQueryProps { queryArgs: ChartQuerySingleArgs } - export interface ChartQuerySingleArgs { args: T; skipQuery: boolean; @@ -41,13 +54,9 @@ export interface ChartQuerySingleArgs { export interface ChartQueryArgsMeta { endpoint: string; } -export interface commonArgsSingleID extends Omit { id: number } -// endpoint specific args -export interface LineReadingApiArgs extends commonArgsMultiID { } -export interface BarReadingApiArgs extends commonArgsMultiID { barWidthDays: number } -export interface ThreeDReadingApiArgs extends commonArgsSingleID { readingInterval: ReadingInterval } -// Selector prepares the query args for each endpoint based on the current graph slice state +// Selector prepares the query args for ALL graph endpoints based on the current graph slice state +// TODO Break down into individual selectors? Verify if prop drilling is required export const selectChartQueryArgs = createSelector( selectGraphState, graphState => { @@ -94,7 +103,7 @@ export const selectChartQueryArgs = createSelector( endpoint: readingsApi.endpoints.bar.name } } - // TODO; Make 2 types for multi-id and single-id request ARGS + // TODO; Make 2 types for multi-id and single-id request ARGS not idea, but works for now. const threeD: ChartQuerySingleArgs = { // Fix not null assertion(s) args: { @@ -110,10 +119,31 @@ export const selectChartQueryArgs = createSelector( } } - return { line, bar, threeD } + const compare: ChartMultiQueryArgs = { + meterArgs: { + ...baseMeterArgs, + shift: calculateCompareShift(graphState.comparePeriod).toISOString(), + curr_start: graphState.compareTimeInterval.getStartTimestamp()?.toISOString(), + curr_end: graphState.compareTimeInterval.getEndTimestamp()?.toISOString() + }, + groupsArgs: { + ...baseGroupArgs, + shift: calculateCompareShift(graphState.comparePeriod).toISOString(), + curr_start: graphState.compareTimeInterval.getStartTimestamp()?.toISOString(), + curr_end: graphState.compareTimeInterval.getEndTimestamp()?.toISOString() + }, + meterSkipQuery: !baseMeterArgs.ids.length, + groupSkipQuery: !baseGroupArgs.ids.length, + meta: { + endpoint: readingsApi.endpoints.compare.name + } + } + + return { line, bar, threeD, compare } } ) +// TODO DUPLICATE SELECTOR? UI SELECTOR MAY CONTAIN SAME LOGIC, CONSOLIDATE IF POSSIBLE? export const selectVisibleMetersGroupsDataByID = createSelector( selectMeterDataById, selectGroupDataById, diff --git a/src/client/app/store.ts b/src/client/app/store.ts index 05399fc7a..70a7258cf 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -6,6 +6,7 @@ import { configureStore } from '@reduxjs/toolkit' import { rootReducer } from './reducers'; import { baseApi } from './redux/api/baseApi'; import { historyMiddleware } from './redux/middleware/graphHistory'; +import { Dispatch } from './types/redux/actions'; export const store = configureStore({ @@ -21,4 +22,7 @@ export const store = configureStore({ // Infer the `RootState` and `AppDispatch` types from the store itself // https://react-redux.js.org/using-react-redux/usage-with-typescript#define-root-state-and-dispatch-types export type RootState = ReturnType -export type AppDispatch = typeof store.dispatch \ No newline at end of file +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 + & Dispatch \ No newline at end of file diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 1756b2584..90b2bf4ed 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -1410,6 +1410,6 @@ export type TranslationKey = keyof typeof LocaleTranslationData // All locales should share the same keys, but intersection over all to be safe? // Will probably error when forgetting to add same key to all locales when using translate() export type LocaleDataKey = - keyof typeof LocaleTranslationData['en'] & - keyof typeof LocaleTranslationData['es'] & + keyof typeof LocaleTranslationData['en'] | + keyof typeof LocaleTranslationData['es'] | keyof typeof LocaleTranslationData['fr'] \ No newline at end of file diff --git a/src/client/app/types/readings.ts b/src/client/app/types/readings.ts index 4625af4e5..e53fb9bce 100644 --- a/src/client/app/types/readings.ts +++ b/src/client/app/types/readings.ts @@ -7,9 +7,7 @@ export interface CompareReading { prev_use: number; } -export interface CompareReadings { - [id: number]: CompareReading; -} +export interface CompareReadings extends Record { } export interface RawReadings { // Note that the identifiers are not the usual ones so the route @@ -21,24 +19,25 @@ export interface RawReadings { e: string } -export interface LineReading extends BarReading{ +export interface LineReading extends BarReading { min: number; max: number; } -export interface LineReadings { - [id: number]: LineReading[]; -} - +// export interface LineReadings { +// [id: number]: LineReading[]; +// } +export type LineReadings = Record export interface BarReading { reading: number; startTimestamp: number; endTimestamp: number; } -export interface BarReadings { - [id: number]: BarReading[]; -} +// export interface BarReadings { +// [id: number]: BarReading[]; +// } +export type BarReadings = Record interface ReadingInterval { startTimestamp: number; diff --git a/src/client/app/types/redux/graph.ts b/src/client/app/types/redux/graph.ts index 604675aba..e6fb2923f 100644 --- a/src/client/app/types/redux/graph.ts +++ b/src/client/app/types/redux/graph.ts @@ -15,11 +15,6 @@ export enum ChartTypes { threeD = '3D' } -export enum MeterOrGroup { - meter = 'meter', - group = 'group' -} - // Rates that can be graphed, only relevant to line graphs. export const LineGraphRates = { 'second': (1 / 3600), diff --git a/src/client/app/types/redux/groups.ts b/src/client/app/types/redux/groups.ts index 344c4f73c..1e97cc5d9 100644 --- a/src/client/app/types/redux/groups.ts +++ b/src/client/app/types/redux/groups.ts @@ -43,10 +43,7 @@ export interface StatefulEditable { dirty: boolean; submitted?: boolean; } -export interface GroupDataByID { - [groupID: number]: GroupData; - -} +export interface GroupDataByID extends Record {} export interface GroupsState { byGroupID: GroupDataByID selectedGroups: number[]; diff --git a/src/client/app/types/redux/map.ts b/src/client/app/types/redux/map.ts index 55caaaf9f..d1700c035 100644 --- a/src/client/app/types/redux/map.ts +++ b/src/client/app/types/redux/map.ts @@ -2,8 +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/. */ -import {ActionType} from './actions'; -import {CalibratedPoint, CalibrationResult, CartesianPoint, GPSPoint} from '../../utils/calibration'; +import { ActionType } from './actions'; +import { CalibratedPoint, CalibrationResult, CartesianPoint, GPSPoint } from '../../utils/calibration'; /** * 'initiate', 'calibrate' or 'unavailable' @@ -126,7 +126,7 @@ export type MapsAction = * @param opposite * @param mapSource */ -export interface MapData{ +export interface MapData { id: number; name: string; displayable: boolean; @@ -173,9 +173,8 @@ export interface CalibrationSettings { /** * @param mapID <= -1 means it's a new map; */ -interface MapMetadataByID { - [mapID: number]: MapMetadata; -} +interface MapMetadataByID extends Record { } + export interface MapState { isLoading: boolean; diff --git a/src/client/app/types/redux/meters.ts b/src/client/app/types/redux/meters.ts index 46b8ae93f..055ab907f 100644 --- a/src/client/app/types/redux/meters.ts +++ b/src/client/app/types/redux/meters.ts @@ -106,9 +106,7 @@ export interface MeterData { disableChecks: boolean; } -export interface MeterDataByID { - [meterID: number]: MeterData; -} +export interface MeterDataByID extends Record { } export interface MetersState { hasBeenFetchedOnce: boolean; diff --git a/src/client/app/types/redux/units.ts b/src/client/app/types/redux/units.ts index 5793a1bb6..9f2b0e997 100644 --- a/src/client/app/types/redux/units.ts +++ b/src/client/app/types/redux/units.ts @@ -48,10 +48,10 @@ export interface UnitEditData { note: string; } -export interface UnitDataById { - [unitId: number]: UnitData; -} - +// export interface UnitDataById { +// [unitId: number]: UnitData; +// } +export interface UnitDataById extends Record { } export interface UnitsState { hasBeenFetchedOnce: boolean, isFetching: boolean; From 042e9e748339ff946355d4e83066466452f26cbc Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Wed, 8 Nov 2023 16:29:48 +0000 Subject: [PATCH 037/131] Sweep for bugs Add TODOS --- src/client/app/components/AppLayout.tsx | 3 +- .../app/components/HeaderButtonsComponent.tsx | 48 +++++++++---------- src/client/app/components/HeaderComponent.tsx | 7 ++- .../app/components/MenuModalComponent.tsx | 5 +- .../app/components/RouteComponentWIP.tsx | 7 ++- .../meters/MetersDetailComponentWIP.tsx | 14 +++--- src/client/app/initScript.ts | 1 + src/client/app/reducers/currentUser.ts | 10 ++-- src/client/app/redux/api/authApi.ts | 8 ++++ .../app/redux/selectors/adminSelectors.ts | 2 - src/client/app/utils/getPage.ts | 12 ----- 11 files changed, 56 insertions(+), 61 deletions(-) delete mode 100644 src/client/app/utils/getPage.ts diff --git a/src/client/app/components/AppLayout.tsx b/src/client/app/components/AppLayout.tsx index 51fb62fa9..40f789659 100644 --- a/src/client/app/components/AppLayout.tsx +++ b/src/client/app/components/AppLayout.tsx @@ -5,14 +5,13 @@ import HeaderComponent from './HeaderComponent' import { Slide, ToastContainer } from 'react-toastify' import 'react-toastify/dist/ReactToastify.css'; /** - * @returns The OED Application, with the current route as an outlet + * @returns The OED Application Layout, header, and footer, with the current route as the outlet. */ export default function AppLayout() { return ( <> - diff --git a/src/client/app/components/HeaderButtonsComponent.tsx b/src/client/app/components/HeaderButtonsComponent.tsx index 1bfcee1a8..b99633c69 100644 --- a/src/client/app/components/HeaderButtonsComponent.tsx +++ b/src/client/app/components/HeaderButtonsComponent.tsx @@ -3,33 +3,33 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { useState, useEffect } from 'react'; -import { Link } from 'react-router-dom-v5-compat'; +import { useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import getPage from '../utils/getPage'; -import translate from '../utils/translate'; +import { useSelector } from 'react-redux'; +import { Link, useLocation } from 'react-router-dom-v5-compat'; +import { DropdownItem, DropdownMenu, DropdownToggle, Nav, NavLink, Navbar, UncontrolledDropdown } from 'reactstrap'; +import { toggleOptionsVisibility } from '../actions/graph'; +import TooltipHelpContainer from '../containers/TooltipHelpContainer'; +import { currentUserSlice } from '../reducers/currentUser'; +import { unsavedWarningSlice } from '../reducers/unsavedWarning'; +import { useAppDispatch } from '../redux/hooks'; import { UserRole } from '../types/items'; +import { State } from '../types/redux/state'; import { hasPermissions, isRoleAdmin } from '../utils/hasPermissions'; import { deleteToken } from '../utils/token'; -import { State } from '../types/redux/state'; -import { useDispatch, useSelector } from 'react-redux'; -import { Navbar, Nav, NavLink, UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap'; +import translate from '../utils/translate'; import LanguageSelectorComponent from './LanguageSelectorComponent'; -import { toggleOptionsVisibility } from '../actions/graph'; import { BASE_URL } from './TooltipHelpComponent'; -import { currentUserSlice } from '../reducers/currentUser'; -import { unsavedWarningSlice } from '../reducers/unsavedWarning'; import TooltipMarkerComponent from './TooltipMarkerComponent'; -import TooltipHelpContainer from '../containers/TooltipHelpContainer'; /** * React Component that defines the header buttons at the top of a page * @returns Header buttons element */ export default function HeaderButtonsComponent() { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); // Get the current page so know which one should not be shown in menu. - const currentPage = getPage(); + const { pathname } = useLocation(); // OED version is needed for help redirect const version = useSelector((state: State) => state.version.version); @@ -94,16 +94,16 @@ export default function HeaderButtonsComponent() { useEffect(() => { setState(prevState => ({ ...prevState, - shouldHomeButtonDisabled: currentPage === '', - shouldAdminButtonDisabled: currentPage === 'admin', - shouldGroupsButtonDisabled: currentPage === 'groups', - shouldMetersButtonDisabled: currentPage === 'meters', - shouldMapsButtonDisabled: currentPage === 'maps', - shouldCSVButtonDisabled: currentPage === 'csv', - shouldUnitsButtonDisabled: currentPage === 'units', - shouldConversionsButtonDisabled: currentPage === 'conversions' + shouldHomeButtonDisabled: pathname === '/', + shouldAdminButtonDisabled: pathname === '/admin', + shouldGroupsButtonDisabled: pathname === '/groups', + shouldMetersButtonDisabled: pathname === '/meters', + shouldMapsButtonDisabled: pathname === '/maps', + shouldCSVButtonDisabled: pathname === '/csv', + shouldUnitsButtonDisabled: pathname === '/units', + shouldConversionsButtonDisabled: pathname === '/conversions' })); - }, [currentPage]); + }, [pathname]); // This updates which items are hidden based on the login status. useEffect(() => { @@ -146,7 +146,7 @@ export default function HeaderButtonsComponent() { display: !renderLoginButton ? 'block' : 'none' }; const currentShowOptionsStyle = { - display: currentPage === '' ? 'block' : 'none' + display: pathname === '/' ? 'block' : 'none' } // Admin help or regular user page const neededPage = loggedInAsAdmin ? '/adminPageChoices.html' : '/pageChoices.html'; @@ -162,7 +162,7 @@ export default function HeaderButtonsComponent() { pageChoicesHelp: currentPageChoicesHelp, showOptionsStyle: currentShowOptionsStyle })); - }, [currentPage, currentUser, helpUrl]); + }, [pathname, currentUser, helpUrl]); // Handle actions on logout. const handleLogOut = () => { diff --git a/src/client/app/components/HeaderComponent.tsx b/src/client/app/components/HeaderComponent.tsx index 43989f40c..a9739a73c 100644 --- a/src/client/app/components/HeaderComponent.tsx +++ b/src/client/app/components/HeaderComponent.tsx @@ -4,9 +4,8 @@ import * as React from 'react'; import { useSelector } from 'react-redux'; -import { Link } from 'react-router-dom-v5-compat'; +import { Link, useLocation } from 'react-router-dom-v5-compat'; import { State } from '../types/redux/state'; -import getPage from '../utils/getPage'; import HeaderButtonsComponent from './HeaderButtonsComponent'; import LogoComponent from './LogoComponent'; import MenuModalComponent from './MenuModalComponent'; @@ -18,7 +17,7 @@ import MenuModalComponent from './MenuModalComponent'; export default function HeaderComponent() { const siteTitle = useSelector((state: State) => state.admin.displayTitle); const showOptions = useSelector((state: State) => state.graph.optionsVisibility); - + const { pathname } = useLocation() const divStyle = { marginTop: '5px', paddingBottom: '5px' @@ -55,7 +54,7 @@ export default function HeaderComponent() {
{/* collapse menu if optionsVisibility is false */} - {getPage() === '' && !showOptions ? + {pathname === '/' && !showOptions ? : } diff --git a/src/client/app/components/MenuModalComponent.tsx b/src/client/app/components/MenuModalComponent.tsx index 98f3e63ba..77ee56706 100644 --- a/src/client/app/components/MenuModalComponent.tsx +++ b/src/client/app/components/MenuModalComponent.tsx @@ -7,9 +7,9 @@ import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; import ReactTooltip from 'react-tooltip'; import { Button, Modal, ModalBody, ModalHeader } from 'reactstrap'; -import getPage from '../utils/getPage'; import HeaderButtonsComponent from './HeaderButtonsComponent'; import UIOptionsComponent from './UIOptionsComponent'; +import { useLocation } from 'react-router-dom-v5-compat'; /** * React component to define the collapsed menu modal @@ -18,6 +18,7 @@ import UIOptionsComponent from './UIOptionsComponent'; export default function MenuModalComponent() { const [showModal, setShowModal] = useState(false); const toggleModal = () => { setShowModal(!showModal); } + const { pathname } = useLocation() const inlineStyle: React.CSSProperties = { display: 'inline', @@ -33,7 +34,7 @@ export default function MenuModalComponent() { {/* Only render graph options if on the graph page */} - {getPage() === '' && + {pathname === '/' && } diff --git a/src/client/app/components/RouteComponentWIP.tsx b/src/client/app/components/RouteComponentWIP.tsx index 3e4a74cf0..15b792cd3 100644 --- a/src/client/app/components/RouteComponentWIP.tsx +++ b/src/client/app/components/RouteComponentWIP.tsx @@ -50,8 +50,7 @@ const useWaitForInit = () => { const waitForInit = async () => { await Promise.all(dispatch(baseApi.util.getRunningQueriesThunk())) setInitComplete(true) - // TODO Fix crashing in components on startup if data has yet to be returned, for now readyToNav works. - // This Could be avoided if these components were written to handle such cases upon startup| undefined data + // TODO Startup Crashing fixed, authen } waitForInit(); @@ -76,7 +75,7 @@ export const AdminOutlet = () => { return // For now this functionality is disabled. - // If no longer desired can remove this and close PR + // If no longer desired can remove this and close Issue #817 // No other cases means user doesn't have the permissions. // return @@ -97,7 +96,7 @@ export const RoleOutlet = ({ UserRole }: { UserRole: UserRole }) => { if (currentUser.profile?.role === UserRole) { return } - // If no longer desired can remove this and close PR + // If no longer desired can remove this and close Issue #817 // For now this functionality is disabled. // return return diff --git a/src/client/app/components/meters/MetersDetailComponentWIP.tsx b/src/client/app/components/meters/MetersDetailComponentWIP.tsx index d049a7b00..c5dcaa371 100644 --- a/src/client/app/components/meters/MetersDetailComponentWIP.tsx +++ b/src/client/app/components/meters/MetersDetailComponentWIP.tsx @@ -5,12 +5,10 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; -import { metersApi } from '../../redux/api/metersApi'; import { useAppSelector } from '../../redux/hooks'; import { selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; import { selectVisibleMetersGroupsDataByID } from '../../redux/selectors/dataSelectors'; import '../../styles/card-page.css'; -import { MeterData } from '../../types/redux/meters'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateMeterModalComponentWIP from './CreateMeterModalComponentWIP'; import MeterViewComponentWIP from './MeterViewComponentWIP'; @@ -26,7 +24,6 @@ export default function MetersDetailComponent() { // We only want displayable meters if non-admins because they still have // non-displayable in state. const { visibleMeters } = useAppSelector(selectVisibleMetersGroupsDataByID); - const { isFetching } = metersApi.useGetMetersQuery() return (
@@ -47,9 +44,14 @@ export default function MetersDetailComponent() { {
{/* Create a MeterViewComponent for each MeterData in Meters State after sorting by identifier */} - {!isFetching && Object.values(visibleMeters) - .sort((MeterA: MeterData, MeterB: MeterData) => (MeterA.identifier.toLowerCase() > MeterB.identifier.toLowerCase()) ? 1 : - ((MeterB.identifier.toLowerCase() > MeterA.identifier.toLowerCase()) ? -1 : 0)) + {/* Optional Chaining to prevent from crashing upon startup race conditions*/} + {Object.values(visibleMeters) + .sort((MeterA, MeterB) => + (MeterA.identifier?.toLowerCase() > MeterB.identifier?.toLowerCase()) + ? 1 + : ((MeterB.identifier?.toLowerCase() > MeterA.identifier?.toLowerCase()) + ? -1 + : 0)) .map(MeterData => ( { // If user is an admin, they receive additional meter details. // To avoid sending duplicate requests upon startup, verify user then fetch + // TODO Not working as expected, still pings for meters and groups twice, due to onQueryStarted async call on verify Token if (hasToken()) { // User has a session token verify before requesting meter/group details await store.dispatch(authApi.endpoints.verifyToken.initiate(getToken())) diff --git a/src/client/app/reducers/currentUser.ts b/src/client/app/reducers/currentUser.ts index 9dfb9f476..e12a12b7c 100644 --- a/src/client/app/reducers/currentUser.ts +++ b/src/client/app/reducers/currentUser.ts @@ -41,14 +41,14 @@ export const currentUserSlice = createSlice({ // Extra Reducers that listen for actions or endpoints and execute accordingly to update this slice's state. builder .addMatcher(userApi.endpoints.getUserDetails.matchFulfilled, - (state, api) => { - state.profile = api.payload + (state, { payload }) => { + state.profile = payload }) .addMatcher(authApi.endpoints.login.matchFulfilled, - (state, api) => { + (state, { payload }) => { // User has logged in update state, and write to local storage - state.profile = { email: api.payload.email, role: api.payload.role } - state.token = api.payload.token + state.profile = { email: payload.email, role: payload.role } + state.token = payload.token setToken(state.token) }) }, diff --git a/src/client/app/redux/api/authApi.ts b/src/client/app/redux/api/authApi.ts index f47784618..8617a9fa7 100644 --- a/src/client/app/redux/api/authApi.ts +++ b/src/client/app/redux/api/authApi.ts @@ -61,6 +61,14 @@ export const authApi = baseApi.injectEndpoints({ deleteToken() }) } + }), + logout: builder.mutation({ + queryFn: (_, { dispatch }) => { + // Opt to use a RTK mutation instead of manually writing a thunk to take advantage mutation invalidations + dispatch(currentUserSlice.actions.clearCurrentUser()) + return { data: null } + }, + invalidatesTags: ['MeterData', 'GroupData'] }) }) }) \ No newline at end of file diff --git a/src/client/app/redux/selectors/adminSelectors.ts b/src/client/app/redux/selectors/adminSelectors.ts index 6c522fb39..ba0233a10 100644 --- a/src/client/app/redux/selectors/adminSelectors.ts +++ b/src/client/app/redux/selectors/adminSelectors.ts @@ -215,8 +215,6 @@ export const selectIsValidConversion = createSelector( Cannot mix unit represent TODO Some of these can go away when we make the menus dynamic. */ - console.log('running again!') - // The destination cannot be a meter unit. if (destinationId !== -999 && unitDataById[destinationId].typeOfUnit === UnitType.meter) { // notifyUser(translate('conversion.create.destination.meter')); diff --git a/src/client/app/utils/getPage.ts b/src/client/app/utils/getPage.ts deleted file mode 100644 index 689154daf..000000000 --- a/src/client/app/utils/getPage.ts +++ /dev/null @@ -1,12 +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/. */ - -/** - * Returns the current page route - * @returns current page name as a string - */ -export default function getPage(): string { - const urlArr = window.location.href.split('/'); - return urlArr[urlArr.length - 1]; -} From e481080b841ccbab55b65594cc3684639e4dd8d7 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Wed, 8 Nov 2023 19:47:51 +0000 Subject: [PATCH 038/131] Api Selector Refactor - Selectors now wrapped to avoid destructuring assignment across the app --- src/client/app/actions/version.ts | 44 ----- src/client/app/components/AppLayout.tsx | 8 +- .../components/AreaUnitSelectComponent.tsx | 2 +- .../app/components/BarChartComponent.tsx | 16 +- src/client/app/components/ExportComponent.tsx | 42 ++-- src/client/app/components/FooterComponent.tsx | 92 +++++---- .../components/GraphicRateMenuComponent.tsx | 2 +- .../app/components/HeaderButtonsComponent.tsx | 26 ++- src/client/app/components/HomeComponent.tsx | 5 +- .../components/LanguageSelectorComponent.tsx | 13 +- .../app/components/LineChartComponent.tsx | 16 +- .../MultiCompareChartComponentWIP.tsx | 12 +- src/client/app/components/ThreeDComponent.tsx | 6 +- .../app/components/ThreeDPillComponent.tsx | 4 +- .../app/components/TooltipHelpComponent.tsx | 181 +++++++++--------- .../app/components/UnitSelectComponent.tsx | 2 +- .../app/components/admin/AdminComponent.tsx | 4 +- .../admin/CreateUserComponentWIP.tsx | 4 - .../components/admin/UsersDetailComponent.tsx | 4 +- .../admin/UsersDetailComponentWIP.tsx | 4 +- .../conversion/ConversionViewComponentWIP.tsx | 2 +- .../conversion/ConversionsDetailComponent.tsx | 8 +- .../ConversionsDetailComponentWIP.tsx | 4 +- .../CreateConversionModalComponent.tsx | 4 +- .../CreateConversionModalComponentWIP.tsx | 6 +- .../EditConversionModalComponent.tsx | 4 +- .../EditConversionModalComponentWIP.tsx | 6 +- .../groups/CreateGroupModalComponent.tsx | 4 +- .../groups/CreateGroupModalComponentWIP.tsx | 12 +- .../groups/EditGroupModalComponent.tsx | 4 +- .../groups/EditGroupModalComponentWIP.tsx | 28 +-- .../groups/GroupViewComponentWIP.tsx | 2 +- .../groups/GroupsDetailComponent.tsx | 10 +- .../groups/GroupsDetailComponentWIP.tsx | 4 +- .../components/maps/MapsDetailComponent.tsx | 4 +- .../meters/CreateMeterModalComponent.tsx | 4 +- .../meters/CreateMeterModalComponentWIP.tsx | 4 +- .../meters/EditMeterModalComponent.tsx | 4 +- .../meters/EditMeterModalComponentWIP.tsx | 6 +- .../meters/MetersDetailComponent.tsx | 10 +- .../meters/MetersDetailComponentWIP.tsx | 4 +- .../unit/CreateUnitModalComponent.tsx | 4 +- .../unit/EditUnitModalComponent.tsx | 10 +- .../components/unit/UnitsDetailComponent.tsx | 8 +- .../app/containers/CompareChartContainer.ts | 6 +- src/client/app/containers/FooterContainer.ts | 26 --- .../app/containers/TooltipHelpContainer.ts | 26 --- .../containers/admin/UsersDetailContainer.tsx | 2 - .../app/containers/csv/UploadCSVContainer.tsx | 18 +- src/client/app/initScript.ts | 2 + src/client/app/reducers/graph.ts | 22 +-- src/client/app/reducers/index.ts | 4 - src/client/app/reducers/version.ts | 60 ------ src/client/app/redux/api/authApi.ts | 1 + src/client/app/redux/api/conversionsApi.ts | 18 +- src/client/app/redux/api/groupsApi.ts | 11 +- src/client/app/redux/api/metersApi.ts | 11 +- src/client/app/redux/api/unitsApi.ts | 26 ++- src/client/app/redux/api/versionApi.ts | 18 ++ .../app/redux/middleware/graphHistory.ts | 8 +- .../app/redux/selectors/adminSelectors.ts | 12 +- .../app/redux/selectors/dataSelectors.ts | 12 +- .../app/redux/selectors/threeDSelectors.ts | 2 +- src/client/app/redux/selectors/uiSelectors.ts | 14 +- src/client/app/types/redux/meters.ts | 47 ----- .../app/utils/determineCompatibleUnits.ts | 26 +-- 66 files changed, 420 insertions(+), 595 deletions(-) delete mode 100644 src/client/app/actions/version.ts delete mode 100644 src/client/app/containers/FooterContainer.ts delete mode 100644 src/client/app/containers/TooltipHelpContainer.ts delete mode 100644 src/client/app/reducers/version.ts create mode 100644 src/client/app/redux/api/versionApi.ts diff --git a/src/client/app/actions/version.ts b/src/client/app/actions/version.ts deleted file mode 100644 index fb7df3252..000000000 --- a/src/client/app/actions/version.ts +++ /dev/null @@ -1,44 +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 { versionApi } from '../utils/api'; -import { Thunk, Dispatch, GetState } from '../types/redux/actions'; -import { State } from '../types/redux/state'; -import { versionSlice } from '../reducers/version'; - - -/** - * @param state The redux state. - * @returns Whether preferences are fetching - */ -function shouldFetchVersion(state: State): boolean { - return !state.version.isFetching; -} - -/** - * Dispatches version fetch actions - */ -export function fetchVersion(): Thunk { - return async (dispatch: Dispatch) => { - dispatch(versionSlice.actions.requestVersion()); - // Returns the version string - const version = await versionApi.getVersion(); - return dispatch(versionSlice.actions.receiveVersion(version)); - }; -} - -/** - * Function that performs the API call to retrieve the current version of the app, - * and dispatches the corresponding action types. - * This function will be called on component initialization. - */ -export function fetchVersionIfNeeded(): Thunk { - return (dispatch: Dispatch, getState: GetState) => { - if (shouldFetchVersion(getState())) { - return dispatch(fetchVersion()); - } - return Promise.resolve(); - }; -} - diff --git a/src/client/app/components/AppLayout.tsx b/src/client/app/components/AppLayout.tsx index 40f789659..07ed7aa20 100644 --- a/src/client/app/components/AppLayout.tsx +++ b/src/client/app/components/AppLayout.tsx @@ -1,9 +1,9 @@ import * as React from 'react' import { Outlet } from 'react-router-dom-v5-compat' -import FooterContainer from '../containers/FooterContainer' -import HeaderComponent from './HeaderComponent' import { Slide, ToastContainer } from 'react-toastify' -import 'react-toastify/dist/ReactToastify.css'; +import 'react-toastify/dist/ReactToastify.css' +import FooterComponent from './FooterComponent' +import HeaderComponent from './HeaderComponent' /** * @returns The OED Application Layout, header, and footer, with the current route as the outlet. */ @@ -13,7 +13,7 @@ export default function AppLayout() { - + ) } \ No newline at end of file diff --git a/src/client/app/components/AreaUnitSelectComponent.tsx b/src/client/app/components/AreaUnitSelectComponent.tsx index af2c87b5a..4ea374ca5 100644 --- a/src/client/app/components/AreaUnitSelectComponent.tsx +++ b/src/client/app/components/AreaUnitSelectComponent.tsx @@ -23,7 +23,7 @@ export default function AreaUnitSelectComponent() { const dispatch = useDispatch(); const graphState = useAppSelector(state => state.graph); - const { data: unitDataById = {} } = useAppSelector(selectUnitDataById); + const unitDataById = useAppSelector(selectUnitDataById); // Array of select options created from the area unit enum const unitOptions: StringSelectOption[] = []; diff --git a/src/client/app/components/BarChartComponent.tsx b/src/client/app/components/BarChartComponent.tsx index bd60741d7..0dfa4624c 100644 --- a/src/client/app/components/BarChartComponent.tsx +++ b/src/client/app/components/BarChartComponent.tsx @@ -38,14 +38,14 @@ export default function BarChartComponent(props: ChartMultiQueryProps state.graph.selectedUnit); // The unit label depends on the unit which is in selectUnit state. const graphingUnit = useAppSelector(state => state.graph.selectedUnit); - const { data: unitDataByID = {} } = useAppSelector(selectUnitDataById); + const unitDataById = useAppSelector(selectUnitDataById); const selectedAreaNormalization = useAppSelector(state => state.graph.areaNormalization); const selectedAreaUnit = useAppSelector(state => state.graph.selectedAreaUnit); const selectedMeters = useAppSelector(selectSelectedMeters); const selectedGroups = useAppSelector(selectSelectedGroups); - const { data: meterDataByID = {} } = useAppSelector(selectMeterDataById); - const { data: groupDataByID = {} } = useAppSelector(selectGroupDataById); + const meterDataByID = useAppSelector(selectMeterDataById); + const groupDataById = useAppSelector(selectGroupDataById); // useQueryHooks for data fetching const { data: meterReadings, isLoading: meterIsFetching } = readingsApi.useBarQuery(meterArgs, { skip: meterSkipQuery }); @@ -60,7 +60,7 @@ export default function BarChartComponent(props: ChartMultiQueryProps 0 && groupDataByID[groupID].areaUnit != AreaUnitType.none)) { + let groupArea = groupDataById[groupID].area; + if (!selectedAreaNormalization || (groupArea > 0 && groupDataById[groupID].areaUnit != AreaUnitType.none)) { if (selectedAreaNormalization) { // convert the meter area into the proper unit, if needed - groupArea *= getAreaUnitConversion(groupDataByID[groupID].areaUnit, selectedAreaUnit); + groupArea *= getAreaUnitConversion(groupDataById[groupID].areaUnit, selectedAreaUnit); } const readingsData = groupData[groupID]; if (readingsData && !groupIsFetching) { - const label = groupDataByID[groupID].name; + const label = groupDataById[groupID].name; const colorID = groupID; if (!readingsData) { throw new Error('Unacceptable condition: readingsData.readings is undefined.'); diff --git a/src/client/app/components/ExportComponent.tsx b/src/client/app/components/ExportComponent.tsx index d3b7395de..80424ad84 100644 --- a/src/client/app/components/ExportComponent.tsx +++ b/src/client/app/components/ExportComponent.tsx @@ -30,13 +30,13 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; */ export default function ExportComponent() { // Meters state - const { data: metersDataById = {} } = useAppSelector(selectMeterDataById); + const meterDataById = useAppSelector(selectMeterDataById); // Groups state - const { data: groupsDataById = {} } = useAppSelector(selectGroupDataById); + const groupDataById = useAppSelector(selectGroupDataById); // Units state - const { data: unitsDataById = {} } = useAppSelector(selectUnitDataById); + const unitsDataById = useAppSelector(selectUnitDataById); // Conversion state - const { data: conversionState = [] } = useAppSelector(selectConversionsDetails); + const conversionState = useAppSelector(selectConversionsDetails); // graph state const graphState = useAppSelector(state => state.graph); // admin state @@ -70,16 +70,16 @@ export default function ExportComponent() { const rateScaling = returned.needsRateScaling ? graphState.lineGraphRate.rate : 1; // Loop over the displayed meters and export one-by-one. Does nothing if no meters selected. for (const meterId of graphState.selectedMeters) { - const meterArea = metersDataById[meterId].area; + const meterArea = meterDataById[meterId].area; // export if area normalization is off or the meter can be normalized - if (!graphState.areaNormalization || (meterArea > 0 && metersDataById[meterId].areaUnit !== AreaUnitType.none)) { + if (!graphState.areaNormalization || (meterArea > 0 && meterDataById[meterId].areaUnit !== AreaUnitType.none)) { // Line readings data for this meter. // Get the readings for the time range and unit graphed const readingsData = lineMeterReadings[meterId]; // Make sure it exists in case state is not there yet. // Convert the meter area into the proper unit if normalizing by area or use 1 if not so won't change reading values. const areaScaling = graphState.areaNormalization ? - meterArea * getAreaUnitConversion(metersDataById[meterId].areaUnit, graphState.selectedAreaUnit) : 1; + meterArea * getAreaUnitConversion(meterDataById[meterId].areaUnit, graphState.selectedAreaUnit) : 1; // Divide areaScaling into the rate so have complete scaling factor for readings. const scaling = rateScaling / areaScaling; // Make sure they are there and not being fetched. @@ -89,7 +89,7 @@ export default function ExportComponent() { // Sort by start timestamp. const sortedReadings = _.sortBy(readings, item => item.startTimestamp, 'asc'); // Identifier for current meter. - const meterIdentifier = metersDataById[meterId].identifier; + const meterIdentifier = meterDataById[meterId].identifier; graphExport(sortedReadings, meterIdentifier, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.meters, errorBarState); } else { throw new Error(`Unacceptable condition: readingsData.readings is undefined for meter ${meterId}.`); @@ -98,12 +98,12 @@ export default function ExportComponent() { } // Loop over the displayed groups and export one-by-one. Does nothing if no groups selected. for (const groupId of graphState.selectedGroups) { - const groupArea = groupsDataById[groupId].area; + const groupArea = groupDataById[groupId].area; // export if area normalization is off or the group can be normalized - if (!graphState.areaNormalization || (groupArea > 0 && groupsDataById[groupId].areaUnit !== AreaUnitType.none)) { + if (!graphState.areaNormalization || (groupArea > 0 && groupDataById[groupId].areaUnit !== AreaUnitType.none)) { // Convert the group area into the proper unit if normalizing by area or use 1 if not so won't change reading values. const areaScaling = graphState.areaNormalization ? - groupArea * getAreaUnitConversion(groupsDataById[groupId].areaUnit, graphState.selectedAreaUnit) : 1; + groupArea * getAreaUnitConversion(groupDataById[groupId].areaUnit, graphState.selectedAreaUnit) : 1; // Divide areaScaling into the rate so have complete scaling factor for readings. const scaling = rateScaling / areaScaling; @@ -117,7 +117,7 @@ export default function ExportComponent() { // Sort by start timestamp. const sortedReadings = _.sortBy(readings, item => item.startTimestamp, 'asc'); // Identifier for current group. - const groupName = groupsDataById[groupId].name; + const groupName = groupDataById[groupId].name; graphExport(sortedReadings, groupName, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.groups); } else { throw new Error(`Unacceptable condition: readingsData.readings is undefined for group ${groupId}.`); @@ -131,12 +131,12 @@ export default function ExportComponent() { // Loop over the displayed meters and export one-by-one. Does nothing if no meters selected. for (const meterId of graphState.selectedMeters) { // export if area normalization is off or the meter can be normalized - if (!graphState.areaNormalization || (metersDataById[meterId].area > 0 && metersDataById[meterId].areaUnit !== AreaUnitType.none)) { + if (!graphState.areaNormalization || (meterDataById[meterId].area > 0 && meterDataById[meterId].areaUnit !== AreaUnitType.none)) { // No scaling if areaNormalization is not enabled let scaling = 1; if (graphState.areaNormalization) { // convert the meter area into the proper unit, if needed - scaling *= getAreaUnitConversion(metersDataById[meterId].areaUnit, graphState.selectedAreaUnit); + scaling *= getAreaUnitConversion(meterDataById[meterId].areaUnit, graphState.selectedAreaUnit); } // Get the readings for the time range and unit graphed const readingsData = barMeterReadings[meterId]; @@ -148,7 +148,7 @@ export default function ExportComponent() { // Sort by start timestamp. const sortedReadings = _.sortBy(readings, item => item.startTimestamp, 'asc'); // Identifier for current meter. - const meterIdentifier = metersDataById[meterId].identifier; + const meterIdentifier = meterDataById[meterId].identifier; graphExport(sortedReadings, meterIdentifier, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.meters); } else if (!readingsData && !barMeterIsFetching) { throw new Error(`Unacceptable condition: readingsData.readings is undefined for meter ${meterId}.`); @@ -158,13 +158,13 @@ export default function ExportComponent() { // Loop over the displayed groups and export one-by-one. Does nothing if no groups selected. for (const groupId of graphState.selectedGroups) { // export if area normalization is off or the group can be normalized - if (!graphState.areaNormalization || (groupsDataById[groupId].area > 0 && groupsDataById[groupId].areaUnit !== AreaUnitType.none)) { + if (!graphState.areaNormalization || (groupDataById[groupId].area > 0 && groupDataById[groupId].areaUnit !== AreaUnitType.none)) { // Bar readings data for this group. // No scaling if areaNormalization is not enabled let scaling = 1; if (graphState.areaNormalization) { // convert the meter area into the proper unit, if needed - scaling *= getAreaUnitConversion(groupsDataById[groupId].areaUnit, graphState.selectedAreaUnit); + scaling *= getAreaUnitConversion(groupDataById[groupId].areaUnit, graphState.selectedAreaUnit); } // Get the readings for the time range and unit graphed const readingsData = barGroupReadings[groupId]; @@ -176,7 +176,7 @@ export default function ExportComponent() { // Sort by start timestamp. const sortedReadings = _.sortBy(readings, item => item.startTimestamp, 'asc'); // Identifier for current group. - const groupName = groupsDataById[groupId].name; + const groupName = groupDataById[groupId].name; graphExport(sortedReadings, groupName, unitLabel, unitIdentifier, chartName, scaling, MeterOrGroup.groups); } else if (!readingsData && !barGroupIsFetching) { throw new Error(`Unacceptable condition: readingsData.readings is undefined for group ${groupId}.`); @@ -232,13 +232,13 @@ export default function ExportComponent() { // Loop over each selected meter in graphic. Does nothing if no meters selected. for (const meterId of graphState.selectedMeters) { // export if area normalization is off or the meter can be normalized - if (!graphState.areaNormalization || (metersDataById[meterId].area > 0 && metersDataById[meterId].areaUnit !== AreaUnitType.none)) { + if (!graphState.areaNormalization || (meterDataById[meterId].area > 0 && meterDataById[meterId].areaUnit !== AreaUnitType.none)) { // Which selected meter being processed. // const currentMeter = graphState.selectedMeters[i]; // Identifier for current meter. - const currentMeterIdentifier = metersDataById[meterId].identifier; + const currentMeterIdentifier = meterDataById[meterId].identifier; // The unit of the currentMeter. - const meterUnitId = metersDataById[meterId].unitId; + const meterUnitId = meterDataById[meterId].unitId; // Note that each meter can have a different unit so look up for each one. let unitIdentifier; // A complication is that a unit associated with a meter is not the one the user diff --git a/src/client/app/components/FooterComponent.tsx b/src/client/app/components/FooterComponent.tsx index d9f2ad7a2..d9cf3fa9d 100644 --- a/src/client/app/components/FooterComponent.tsx +++ b/src/client/app/components/FooterComponent.tsx @@ -4,56 +4,50 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; +import { selectOEDVersion } from '../redux/api/versionApi'; +import { useAppSelector } from '../redux/hooks'; -interface FooterProps { - version: string; - fetchVersionIfNeeded(): Promise; +/** + * + * @returns Footer loaded at the bottom of every webpage, which loads the site version from the redux store + */ +export default function FooterComponent() { + const version = useAppSelector(selectOEDVersion) + return ( +
+
+ +
+ ) } -/* -* Footer loaded at the bottom of every webpage, which loads the site version from the redux store -*/ -export default class FooterComponent extends React.Component { - constructor(props: FooterProps) { - super(props); - this.props.fetchVersionIfNeeded(); - } - public render() { - const footerStyle: React.CSSProperties = { - position: 'absolute', - bottom: '60px', - height: '10px', - lineHeight: '20px', - paddingTop: '20px', - borderTop: '1px #e1e4e8 solid', - textAlign: 'center', - width: '100%' - }; - const phantomStyle: React.CSSProperties = { - display: 'block', - height: '100px', - width: '100%' - }; - return ( -
-
-
- - - - - - - - - - - - - {this.props.version} -
-
- ); - } -} +const footerStyle: React.CSSProperties = { + position: 'absolute', + bottom: '60px', + height: '10px', + lineHeight: '20px', + paddingTop: '20px', + borderTop: '1px #e1e4e8 solid', + textAlign: 'center', + width: '100%' +}; +const phantomStyle: React.CSSProperties = { + display: 'block', + height: '100px', + width: '100%' +}; \ No newline at end of file diff --git a/src/client/app/components/GraphicRateMenuComponent.tsx b/src/client/app/components/GraphicRateMenuComponent.tsx index 1fc0f2393..d46408271 100644 --- a/src/client/app/components/GraphicRateMenuComponent.tsx +++ b/src/client/app/components/GraphicRateMenuComponent.tsx @@ -26,7 +26,7 @@ export default function GraphicRateMenuComponent() { const graphState = useAppSelector(state => state.graph); // Unit state - const { data: unitDataById = {} } = useAppSelector(selectUnitDataById); + const unitDataById = useAppSelector(selectUnitDataById); // Unit data by Id const selectedUnitData = unitDataById[graphState.selectedUnit]; diff --git a/src/client/app/components/HeaderButtonsComponent.tsx b/src/client/app/components/HeaderButtonsComponent.tsx index b99633c69..c218b80cd 100644 --- a/src/client/app/components/HeaderButtonsComponent.tsx +++ b/src/client/app/components/HeaderButtonsComponent.tsx @@ -5,18 +5,16 @@ import * as React from 'react'; import { useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useSelector } from 'react-redux'; import { Link, useLocation } from 'react-router-dom-v5-compat'; import { DropdownItem, DropdownMenu, DropdownToggle, Nav, NavLink, Navbar, UncontrolledDropdown } from 'reactstrap'; +import { selectOEDVersion } from '../redux/api/versionApi'; import { toggleOptionsVisibility } from '../actions/graph'; -import TooltipHelpContainer from '../containers/TooltipHelpContainer'; -import { currentUserSlice } from '../reducers/currentUser'; +import TooltipHelpComponent from '../components/TooltipHelpComponent'; import { unsavedWarningSlice } from '../reducers/unsavedWarning'; -import { useAppDispatch } from '../redux/hooks'; +import { authApi } from '../redux/api/authApi'; +import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { UserRole } from '../types/items'; -import { State } from '../types/redux/state'; import { hasPermissions, isRoleAdmin } from '../utils/hasPermissions'; -import { deleteToken } from '../utils/token'; import translate from '../utils/translate'; import LanguageSelectorComponent from './LanguageSelectorComponent'; import { BASE_URL } from './TooltipHelpComponent'; @@ -27,12 +25,13 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; * @returns Header buttons element */ export default function HeaderButtonsComponent() { + const [logout] = authApi.useLogoutMutation() const dispatch = useAppDispatch(); // Get the current page so know which one should not be shown in menu. const { pathname } = useLocation(); // OED version is needed for help redirect - const version = useSelector((state: State) => state.version.version); + const version = useAppSelector(selectOEDVersion); // Help URL location const helpUrl = BASE_URL + version; // options help @@ -76,11 +75,11 @@ export default function HeaderButtonsComponent() { // Local state for rendering. const [state, setState] = useState(defaultState); // Information on the current user. - const currentUser = useSelector((state: State) => state.currentUser.profile); + const currentUser = useAppSelector(state => state.currentUser.profile); // Tracks unsaved changes. - const unsavedChangesState = useSelector((state: State) => state.unsavedWarning.hasUnsavedChanges); + const unsavedChangesState = useAppSelector(state => state.unsavedWarning.hasUnsavedChanges); // whether to collapse options when on graphs page - const optionsVisibility = useSelector((state: State) => state.graph.optionsVisibility); + const optionsVisibility = useAppSelector(state => state.graph.optionsVisibility); // Must update in case the version was not set when the page was loaded. useEffect(() => { @@ -170,17 +169,14 @@ export default function HeaderButtonsComponent() { // Unsaved changes so deal with them and then it takes care of logout. dispatch(unsavedWarningSlice.actions.flipLogOutState()); } else { - // Remove token so has no role. - deleteToken(); - // Clean up state since lost your role. - dispatch(currentUserSlice.actions.clearCurrentUser()); + logout() } }; return (
- +
); diff --git a/src/client/app/components/LanguageSelectorComponent.tsx b/src/client/app/components/LanguageSelectorComponent.tsx index bb744bd38..3a08eec42 100644 --- a/src/client/app/components/LanguageSelectorComponent.tsx +++ b/src/client/app/components/LanguageSelectorComponent.tsx @@ -3,12 +3,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { LanguageTypes } from '../types/redux/i18n'; import { FormattedMessage } from 'react-intl'; import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap'; -import { useDispatch, useSelector } from 'react-redux'; -import { State } from '../types/redux/state'; import { updateSelectedLanguage } from '../actions/options'; +import { selectSelectedLanguage } from '../reducers/options'; +import { selectOEDVersion } from '../redux/api/versionApi'; +import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { LanguageTypes } from '../types/redux/i18n'; import { BASE_URL } from './TooltipHelpComponent'; /** @@ -16,10 +17,10 @@ import { BASE_URL } from './TooltipHelpComponent'; * @returns Language selector element for navbar */ export default function LanguageSelectorComponent() { - const dispatch = useDispatch(); + const dispatch = useAppDispatch(); - const selectedLanguage = useSelector((state: State) => state.options.selectedLanguage); - const version = useSelector((state: State) => state.version.version); + const selectedLanguage = useAppSelector(selectSelectedLanguage); + const version = useAppSelector(selectOEDVersion); const HELP_URL = BASE_URL + version; diff --git a/src/client/app/components/LineChartComponent.tsx b/src/client/app/components/LineChartComponent.tsx index 62ef78edd..eb33627cd 100644 --- a/src/client/app/components/LineChartComponent.tsx +++ b/src/client/app/components/LineChartComponent.tsx @@ -38,13 +38,13 @@ export default function LineChartComponent(props: ChartMultiQueryProps state.graph.selectedUnit); // The current selected rate const currentSelectedRate = useAppSelector(selectLineGraphRate); - const { data: unitDataByID = {} } = useAppSelector(selectUnitDataById); + const unitDataById = useAppSelector(selectUnitDataById); const selectedAreaNormalization = useAppSelector(selectGraphAreaNormalization); const selectedAreaUnit = useAppSelector(selectAreaUnit); const selectedMeters = useAppSelector(selectSelectedMeters); const selectedGroups = useAppSelector(selectSelectedGroups); - const { data: meterDataByID = {} } = useAppSelector(selectMeterDataById); - const { data: groupDataByID = {} } = useAppSelector(selectGroupDataById); + const meterDataByID = useAppSelector(selectMeterDataById); + const groupDataById = useAppSelector(selectGroupDataById); // dataFetching Query Hooks @@ -68,7 +68,7 @@ export default function LineChartComponent(props: ChartMultiQueryProps 0 && groupDataByID[groupID].areaUnit != AreaUnitType.none)) { + if (!selectedAreaNormalization || (groupArea > 0 && groupDataById[groupID].areaUnit != AreaUnitType.none)) { // Convert the group area into the proper unit if normalizing by area or use 1 if not so won't change reading values. const areaScaling = selectedAreaNormalization ? - groupArea * getAreaUnitConversion(groupDataByID[groupID].areaUnit, selectedAreaUnit) : 1; + groupArea * getAreaUnitConversion(groupDataById[groupID].areaUnit, selectedAreaUnit) : 1; // Divide areaScaling into the rate so have complete scaling factor for readings. const scaling = rateScaling / areaScaling; const readingsData = byGroupID[groupID]; if (readingsData !== undefined && !groupIsLoading) { - const label = groupDataByID[groupID].name; + const label = groupDataById[groupID].name; const colorID = groupID; if (readingsData === undefined) { throw new Error('Unacceptable condition: readingsData.readings is undefined.'); diff --git a/src/client/app/components/MultiCompareChartComponentWIP.tsx b/src/client/app/components/MultiCompareChartComponentWIP.tsx index 1837cffd1..6bef12081 100644 --- a/src/client/app/components/MultiCompareChartComponentWIP.tsx +++ b/src/client/app/components/MultiCompareChartComponentWIP.tsx @@ -7,8 +7,8 @@ import { FormattedMessage } from 'react-intl'; import { UncontrolledAlert } from 'reactstrap'; import CompareChartContainer, { CompareEntity } from '../containers/CompareChartContainer'; import { selectGraphAreaNormalization, selectSelectedGroups, selectSelectedMeters, selectSortingOrder } from '../reducers/graph'; -import { groupsApi } from '../redux/api/groupsApi'; -import { metersApi } from '../redux/api/metersApi'; +import { selectGroupDataById } from '../redux/api/groupsApi'; +import { selectMeterDataById } from '../redux/api/metersApi'; import { readingsApi } from '../redux/api/readingsApi'; import { useAppSelector } from '../redux/hooks'; import { selectChartQueryArgs } from '../redux/selectors/dataSelectors'; @@ -33,8 +33,8 @@ export default function MultiCompareChartComponentWIP() { const { data: meterReadings = {} } = readingsApi.useCompareQuery(meterArgs, { skip: meterSkipQuery }) const { data: groupReadings = {} } = readingsApi.useCompareQuery(groupsArgs, { skip: groupSkipQuery }) - const { data: meterDataByID = {} } = metersApi.useGetMetersQuery() - const { data: groupDataByID = {} } = groupsApi.useGetGroupsQuery() + const meterDataByID = useAppSelector(selectMeterDataById) + const groupDataById = useAppSelector(selectGroupDataById) // TODO SEEMS UNUSED, kept due to uncertainty when migrating to RTK VERIFY BEHAVIOR const errorEntities: string[] = []; @@ -60,8 +60,8 @@ export default function MultiCompareChartComponentWIP() { } }) Object.entries(groupReadings).forEach(([key, value]) => { - const identifier = groupDataByID[Number(key)].name - const areaNormValid = (!areaNormalization || (groupDataByID[Number(key)].area > 0 && groupDataByID[Number(key)].areaUnit !== AreaUnitType.none)) + const identifier = groupDataById[Number(key)].name + const areaNormValid = (!areaNormalization || (groupDataById[Number(key)].area > 0 && groupDataById[Number(key)].areaUnit !== AreaUnitType.none)) if (areaNormValid && selectedGroups.includes(Number(key))) { const change = calculateChange(value.curr_use, value.prev_use); const entity: CompareEntity = { diff --git a/src/client/app/components/ThreeDComponent.tsx b/src/client/app/components/ThreeDComponent.tsx index 9f3100961..fae59a88a 100644 --- a/src/client/app/components/ThreeDComponent.tsx +++ b/src/client/app/components/ThreeDComponent.tsx @@ -33,9 +33,9 @@ import ThreeDPillComponent from './ThreeDPillComponent'; export default function ThreeDComponent(props: ChartSingleQueryProps) { const { args, skipQuery } = props.queryArgs; const { data, isFetching } = readingsApi.endpoints.threeD.useQuery(args, { skip: skipQuery }); - const { data: meterDataById = {} } = useAppSelector(selectMeterDataById); - const { data: groupDataById = {} } = useAppSelector(selectGroupDataById); - const { data: unitDataById = {} } = useAppSelector(selectUnitDataById); + const meterDataById = useAppSelector(selectMeterDataById); + const groupDataById = useAppSelector(selectGroupDataById); + const unitDataById = useAppSelector(selectUnitDataById); const graphState = useAppSelector(selectGraphState); const { meterOrGroupID, meterOrGroupName, isAreaCompatible } = useAppSelector(selectThreeDComponentInfo); diff --git a/src/client/app/components/ThreeDPillComponent.tsx b/src/client/app/components/ThreeDPillComponent.tsx index f5eb9ad72..598347e05 100644 --- a/src/client/app/components/ThreeDPillComponent.tsx +++ b/src/client/app/components/ThreeDPillComponent.tsx @@ -17,8 +17,8 @@ import { AreaUnitType } from '../utils/getAreaUnitConversion'; */ export default function ThreeDPillComponent() { const dispatch = useAppDispatch(); - const { data: meterDataById = {} } = useAppSelector(selectMeterDataById); - const { data: groupDataById = {} } = useAppSelector(selectGroupDataById); + const meterDataById = useAppSelector(selectMeterDataById); + const groupDataById = useAppSelector(selectGroupDataById); const threeDState = useAppSelector(state => state.graph.threeD); const graphState = useAppSelector(state => state.graph); diff --git a/src/client/app/components/TooltipHelpComponent.tsx b/src/client/app/components/TooltipHelpComponent.tsx index d15869d38..e467996ab 100644 --- a/src/client/app/components/TooltipHelpComponent.tsx +++ b/src/client/app/components/TooltipHelpComponent.tsx @@ -7,11 +7,11 @@ import { FormattedMessage } from 'react-intl'; import ReactTooltip from 'react-tooltip'; import '../styles/tooltip.css'; import translate from '../utils/translate'; +import { useAppSelector } from '../redux/hooks'; +import { selectOEDVersion } from '../redux/api/versionApi'; interface TooltipHelpProps { page: string; // Specifies which page the tip is in. - version: string; - fetchVersionIfNeeded(): Promise; } // Normal/live URL for OED help pages @@ -21,102 +21,101 @@ export const BASE_URL = 'https://openenergydashboard.github.io/help/' // This works if you have a fork of the web pages and setup your GitHub account to serve them up. // export const BASE_URL = 'https://xxx.github.io/OpenEnergyDashboard.github.io/help/'; -export default class TooltipHelpComponent extends React.Component { - constructor(props: TooltipHelpProps) { - super(props); - this.props.fetchVersionIfNeeded(); - } +/** + * @param props // Specifies which page the tip is in. + * @returns ToolTipHelpComponent + */ +export default function TooltipHelpComponent(props: TooltipHelpProps) { /** * @returns JSX to create the help icons with links */ - public render() { - const divStyle = { - display: 'inline-block' - }; - const version = this.props.version - const HELP_URL = BASE_URL + version; + const version = useAppSelector(selectOEDVersion) - const helpLinks: Record> = { - 'help.admin.conversioncreate': { link: `${HELP_URL}/adminConversionCreating.html` }, - 'help.admin.conversionedit': { link: `${HELP_URL}/adminConversionEditing.html` }, - 'help.admin.conversionview': { link: `${HELP_URL}/adminConversionViewing.html` }, - 'help.admin.groupcreate': { link: `${HELP_URL}/adminGroupCreating.html` }, - 'help.admin.groupedit': { link: `${HELP_URL}/adminGroupEditing.html` }, - 'help.admin.groupview': { link: `${HELP_URL}/adminGroupViewing.html` }, - 'help.admin.header': { link: `${HELP_URL}/adminPreferences.html` }, - 'help.admin.mapview': { link: `${HELP_URL}/adminMapViewing.html` }, - 'help.admin.metercreate': { link: `${HELP_URL}/adminMeterCreating.html` }, - 'help.admin.meteredit': { link: `${HELP_URL}/adminMeterEditing.html` }, - 'help.admin.meterview': { link: `${HELP_URL}/adminMeterViewing.html` }, - 'help.admin.unitcreate': { link: `${HELP_URL}/adminUnitCreating.html` }, - 'help.admin.unitedit': { link: `${HELP_URL}/adminUnitEditing.html` }, - 'help.admin.unitview': { link: `${HELP_URL}/adminUnitViewing.html` }, - 'help.admin.user': { link: `${HELP_URL}/adminUser.html` }, - 'help.csv.header': { link: `${HELP_URL}/adminDataAcquisition.html` }, - 'help.home.area.normalize': { link: `${HELP_URL}/areaNormalization.html` }, - 'help.home.bar.custom.slider.tip': { link: `${HELP_URL}/barGraphic.html#usage` }, - 'help.home.bar.interval.tip': { link: `${HELP_URL}/barGraphic.html#usage` }, - 'help.home.bar.stacking.tip': { link: `${HELP_URL}/barGraphic.html#barStacking` }, - 'help.home.chart.plotly.controls': { link: 'https://plotly.com/chart-studio-help/getting-to-know-the-plotly-modebar/' }, - 'help.home.chart.redraw.restore': { link: `${HELP_URL}/lineGraphic.html#redrawRestore` }, - 'help.home.chart.select': { link: `${HELP_URL}/graphType.html` }, - 'help.home.compare.interval.tip': { link: `${HELP_URL}/compareGraphic.html#usage` }, - 'help.home.compare.sort.tip': { link: `${HELP_URL}/compareGraphic.html#usage` }, - 'help.home.error.bar': { link: `${HELP_URL}/errorBar.html#usage` }, - 'help.home.export.graph.data': { link: `${HELP_URL}/export.html` }, - 'help.home.map.interval.tip': { link: `${HELP_URL}/mapGraphic.html#usage` }, - 'help.home.navigation': { link: '' }, - 'help.home.select.groups': { link: `${HELP_URL}/graphingGroups.html` }, - 'help.home.select.maps': { link: `${HELP_URL}/mapGraphic.html` }, - 'help.home.select.meters': { link: `${HELP_URL}/graphingMeters.html` }, - 'help.home.select.rates': { link: `${HELP_URL}/graphingRates.html` }, - 'help.home.select.units': { link: `${HELP_URL}/graphingUnits.html` }, - 'help.home.readings.per.day': { link: `${HELP_URL}/readingsPerDay.html` }, - 'help.home.toggle.chart.link': { link: `${HELP_URL}/chartLink.html` }, - 'help.groups.groupdetails': { link: `${HELP_URL}/groupViewing.html#groupDetails` }, - 'help.groups.groupview': { link: `${HELP_URL}/groupViewing.html` }, - 'help.meters.meterview': { link: `${HELP_URL}/meterViewing.html` } - }; + const HELP_URL = BASE_URL + version; - return ( -
- { - if (dataTip === null) { - return; - } - // Create links - const values = helpLinks[dataTip] || {}; // This is in case the help tip does not have any links. - const links: Record = {}; - Object.keys(values).forEach(key => { - const link = values[key]; - links[key] = version ? ( - {translate('here')} - - ) : <>...; - // TODO: Provide default link when there are issues fetching version number - }); - return ( -
- -
- ); - }} - /> + const helpLinks: Record> = { + 'help.admin.conversioncreate': { link: `${HELP_URL}/adminConversionCreating.html` }, + 'help.admin.conversionedit': { link: `${HELP_URL}/adminConversionEditing.html` }, + 'help.admin.conversionview': { link: `${HELP_URL}/adminConversionViewing.html` }, + 'help.admin.groupcreate': { link: `${HELP_URL}/adminGroupCreating.html` }, + 'help.admin.groupedit': { link: `${HELP_URL}/adminGroupEditing.html` }, + 'help.admin.groupview': { link: `${HELP_URL}/adminGroupViewing.html` }, + 'help.admin.header': { link: `${HELP_URL}/adminPreferences.html` }, + 'help.admin.mapview': { link: `${HELP_URL}/adminMapViewing.html` }, + 'help.admin.metercreate': { link: `${HELP_URL}/adminMeterCreating.html` }, + 'help.admin.meteredit': { link: `${HELP_URL}/adminMeterEditing.html` }, + 'help.admin.meterview': { link: `${HELP_URL}/adminMeterViewing.html` }, + 'help.admin.unitcreate': { link: `${HELP_URL}/adminUnitCreating.html` }, + 'help.admin.unitedit': { link: `${HELP_URL}/adminUnitEditing.html` }, + 'help.admin.unitview': { link: `${HELP_URL}/adminUnitViewing.html` }, + 'help.admin.user': { link: `${HELP_URL}/adminUser.html` }, + 'help.csv.header': { link: `${HELP_URL}/adminDataAcquisition.html` }, + 'help.home.area.normalize': { link: `${HELP_URL}/areaNormalization.html` }, + 'help.home.bar.custom.slider.tip': { link: `${HELP_URL}/barGraphic.html#usage` }, + 'help.home.bar.interval.tip': { link: `${HELP_URL}/barGraphic.html#usage` }, + 'help.home.bar.stacking.tip': { link: `${HELP_URL}/barGraphic.html#barStacking` }, + 'help.home.chart.plotly.controls': { link: 'https://plotly.com/chart-studio-help/getting-to-know-the-plotly-modebar/' }, + 'help.home.chart.redraw.restore': { link: `${HELP_URL}/lineGraphic.html#redrawRestore` }, + 'help.home.chart.select': { link: `${HELP_URL}/graphType.html` }, + 'help.home.compare.interval.tip': { link: `${HELP_URL}/compareGraphic.html#usage` }, + 'help.home.compare.sort.tip': { link: `${HELP_URL}/compareGraphic.html#usage` }, + 'help.home.error.bar': { link: `${HELP_URL}/errorBar.html#usage` }, + 'help.home.export.graph.data': { link: `${HELP_URL}/export.html` }, + 'help.home.map.interval.tip': { link: `${HELP_URL}/mapGraphic.html#usage` }, + 'help.home.navigation': { link: '' }, + 'help.home.select.groups': { link: `${HELP_URL}/graphingGroups.html` }, + 'help.home.select.maps': { link: `${HELP_URL}/mapGraphic.html` }, + 'help.home.select.meters': { link: `${HELP_URL}/graphingMeters.html` }, + 'help.home.select.rates': { link: `${HELP_URL}/graphingRates.html` }, + 'help.home.select.units': { link: `${HELP_URL}/graphingUnits.html` }, + 'help.home.readings.per.day': { link: `${HELP_URL}/readingsPerDay.html` }, + 'help.home.toggle.chart.link': { link: `${HELP_URL}/chartLink.html` }, + 'help.groups.groupdetails': { link: `${HELP_URL}/groupViewing.html#groupDetails` }, + 'help.groups.groupview': { link: `${HELP_URL}/groupViewing.html` }, + 'help.meters.meterview': { link: `${HELP_URL}/meterViewing.html` } + }; -
- ); - } + return ( +
+ { + if (dataTip === null) { + return; + } + // Create links + const values = helpLinks[dataTip] || {}; // This is in case the help tip does not have any links. + const links: Record = {}; + Object.keys(values).forEach(key => { + const link = values[key]; + links[key] = version ? ( + {translate('here')} + + ) : <>...; + // TODO: Provide default link when there are issues fetching version number + }); + return ( +
+ +
+ ); + }} + /> + +
+ ); } +const divStyle = { + display: 'inline-block' +}; \ No newline at end of file diff --git a/src/client/app/components/UnitSelectComponent.tsx b/src/client/app/components/UnitSelectComponent.tsx index 9d774152a..d7b11555b 100644 --- a/src/client/app/components/UnitSelectComponent.tsx +++ b/src/client/app/components/UnitSelectComponent.tsx @@ -24,7 +24,7 @@ export default function UnitSelectComponent() { const dispatch = useAppDispatch(); const unitSelectOptions = useAppSelector(state => selectUnitSelectData(state)); const selectedUnitID = useAppSelector(state => state.graph.selectedUnit); - const { data: unitsByID = {} } = useAppSelector(selectUnitDataById); + const unitsByID = useAppSelector(selectUnitDataById); const { endpointsFetchingData } = getFetchingStates(); diff --git a/src/client/app/components/admin/AdminComponent.tsx b/src/client/app/components/admin/AdminComponent.tsx index 8bb30511f..0fbcc854c 100644 --- a/src/client/app/components/admin/AdminComponent.tsx +++ b/src/client/app/components/admin/AdminComponent.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; // import PreferencesContainer from '../../containers/admin/PreferencesContainer'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import PreferencesComponentWIP from './PreferencesComponentWIP'; @@ -34,7 +34,7 @@ export default function AdminComponent() { }; return (
- +

diff --git a/src/client/app/components/admin/CreateUserComponentWIP.tsx b/src/client/app/components/admin/CreateUserComponentWIP.tsx index 1dff21ee5..9b3bbb78a 100644 --- a/src/client/app/components/admin/CreateUserComponentWIP.tsx +++ b/src/client/app/components/admin/CreateUserComponentWIP.tsx @@ -5,8 +5,6 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { Button, Input } from 'reactstrap'; -import HeaderComponent from '../../components/HeaderComponent'; -import FooterContainer from '../../containers/FooterContainer'; import { userApi } from '../../redux/api/userApi'; import { NewUser, UserRole } from '../../types/items'; import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; @@ -40,7 +38,6 @@ export default function CreateUserComponentWIP() { } return (
-

@@ -71,7 +68,6 @@ export default function CreateUserComponentWIP() {
-
) diff --git a/src/client/app/components/admin/UsersDetailComponent.tsx b/src/client/app/components/admin/UsersDetailComponent.tsx index 5d8991bf8..97a76f169 100644 --- a/src/client/app/components/admin/UsersDetailComponent.tsx +++ b/src/client/app/components/admin/UsersDetailComponent.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { User, UserRole } from '../../types/items'; import { Button, Input, Table } from 'reactstrap'; import CreateUserLinkButtonComponent from './users/CreateUserLinkButtonComponent'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import { FormattedMessage } from 'react-intl'; import UnsavedWarningContainer from '../../containers/UnsavedWarningContainer'; @@ -73,7 +73,7 @@ export default function UserDetailComponent(props: UserDisplayComponentProps) { return (
- +

diff --git a/src/client/app/components/admin/UsersDetailComponentWIP.tsx b/src/client/app/components/admin/UsersDetailComponentWIP.tsx index 61dbeeda0..9e53254b7 100644 --- a/src/client/app/components/admin/UsersDetailComponentWIP.tsx +++ b/src/client/app/components/admin/UsersDetailComponentWIP.tsx @@ -6,7 +6,7 @@ import * as _ from 'lodash'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { Button, Input, Table } from 'reactstrap'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { userApi } from '../../redux/api/userApi'; import { User, UserRole } from '../../types/items'; import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; @@ -63,7 +63,7 @@ export default function UserDetailComponentWIP() { successMessage='users.successfully.edit.users' failureMessage='failed.to.submit.changes' /> - +

diff --git a/src/client/app/components/conversion/ConversionViewComponentWIP.tsx b/src/client/app/components/conversion/ConversionViewComponentWIP.tsx index def042f61..7e7d25331 100644 --- a/src/client/app/components/conversion/ConversionViewComponentWIP.tsx +++ b/src/client/app/components/conversion/ConversionViewComponentWIP.tsx @@ -28,7 +28,7 @@ export default function ConversionViewComponent(props: ConversionViewComponentPr // Edit Modal Show const [showEditModal, setShowEditModal] = useState(false); - const { data: unitDataById = {} } = useAppSelector(selectUnitDataById) + const unitDataById = useAppSelector(selectUnitDataById) const handleShow = () => { setShowEditModal(true); diff --git a/src/client/app/components/conversion/ConversionsDetailComponent.tsx b/src/client/app/components/conversion/ConversionsDetailComponent.tsx index 56a6209a7..cbb90d835 100644 --- a/src/client/app/components/conversion/ConversionsDetailComponent.tsx +++ b/src/client/app/components/conversion/ConversionsDetailComponent.tsx @@ -10,8 +10,7 @@ import { ConversionData } from 'types/redux/conversions'; import { fetchConversionsDetailsIfNeeded } from '../../actions/conversions'; import HeaderComponent from '../../components/HeaderComponent'; import SpinnerComponent from '../../components/SpinnerComponent'; -import FooterContainer from '../../containers/FooterContainer'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { selectConversionsDetails } from '../../redux/api/conversionsApi'; import { useAppDispatch, useAppSelector } from '../../redux/hooks'; import { State } from '../../types/redux/state'; @@ -34,7 +33,7 @@ export default function ConversionsDetailComponent() { }, [dispatch]); // Conversions state - const { data: conversionsState = [] } = useAppSelector(selectConversionsDetails); + const conversionsState = useAppSelector(selectConversionsDetails); const isUpdatingCikAndDBViews = useSelector((state: State) => state.admin.isUpdatingCikAndDBViews); @@ -66,7 +65,7 @@ export default function ConversionsDetailComponent() { ) : (
- +

@@ -95,7 +94,6 @@ export default function ConversionsDetailComponent() { units={unitsState} />))}

-

)}

diff --git a/src/client/app/components/conversion/ConversionsDetailComponentWIP.tsx b/src/client/app/components/conversion/ConversionsDetailComponentWIP.tsx index e34306748..3b06f615b 100644 --- a/src/client/app/components/conversion/ConversionsDetailComponentWIP.tsx +++ b/src/client/app/components/conversion/ConversionsDetailComponentWIP.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import SpinnerComponent from '../../components/SpinnerComponent'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { conversionsApi } from '../../redux/api/conversionsApi'; import { unitsApi } from '../../redux/api/unitsApi'; import { ConversionData } from '../../types/redux/conversions'; @@ -52,7 +52,7 @@ export default function ConversionsDetailComponent() {
) : (
- +

diff --git a/src/client/app/components/conversion/CreateConversionModalComponent.tsx b/src/client/app/components/conversion/CreateConversionModalComponent.tsx index 3586b37f3..359fbd948 100644 --- a/src/client/app/components/conversion/CreateConversionModalComponent.tsx +++ b/src/client/app/components/conversion/CreateConversionModalComponent.tsx @@ -11,7 +11,7 @@ import translate from '../../utils/translate'; import '../../styles/modal.css'; import { TrueFalseType } from '../../types/items'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { addConversion } from '../../actions/conversions'; import { UnitData, UnitDataById } from 'types/redux/units'; import { ConversionData } from 'types/redux/conversions'; @@ -204,7 +204,7 @@ export default function CreateConversionModalComponent(props: CreateConversionMo - +
diff --git a/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx b/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx index a4f54bbb3..ea78309ba 100644 --- a/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx +++ b/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx @@ -7,7 +7,7 @@ import * as React from 'react'; import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { conversionsApi } from '../../redux/api/conversionsApi'; import { selectUnitDataById } from '../../redux/api/unitsApi'; import { useAppSelector } from '../../redux/hooks'; @@ -26,7 +26,7 @@ import TooltipMarkerComponent from '../TooltipMarkerComponent'; */ export default function CreateConversionModalComponent() { const [addConversionMutation] = conversionsApi.useAddConversionMutation() - const { data: unitDataById = {} } = useAppSelector(selectUnitDataById) + const unitDataById = useAppSelector(selectUnitDataById) // Want units in sorted order by identifier regardless of case. const unitsSorted = _.sortBy(Object.values(unitDataById), unit => unit.identifier.toLowerCase(), 'asc'); @@ -127,7 +127,7 @@ export default function CreateConversionModalComponent() { - +
diff --git a/src/client/app/components/conversion/EditConversionModalComponent.tsx b/src/client/app/components/conversion/EditConversionModalComponent.tsx index 896139015..a999b4136 100644 --- a/src/client/app/components/conversion/EditConversionModalComponent.tsx +++ b/src/client/app/components/conversion/EditConversionModalComponent.tsx @@ -9,7 +9,7 @@ import { Button, Col, Container, FormGroup, Input, Label, Modal, ModalBody, Moda import { FormattedMessage } from 'react-intl'; import translate from '../../utils/translate'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import '../../styles/modal.css'; import { submitEditedConversion, deleteConversion } from '../../actions/conversions'; import { TrueFalseType } from '../../types/items'; @@ -153,7 +153,7 @@ export default function EditConversionModalComponent(props: EditConversionModalC - +
diff --git a/src/client/app/components/conversion/EditConversionModalComponentWIP.tsx b/src/client/app/components/conversion/EditConversionModalComponentWIP.tsx index 394917b03..4ff03a296 100644 --- a/src/client/app/components/conversion/EditConversionModalComponentWIP.tsx +++ b/src/client/app/components/conversion/EditConversionModalComponentWIP.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { Button, Col, Container, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { conversionsApi } from '../../redux/api/conversionsApi'; import { selectUnitDataById } from '../../redux/api/unitsApi'; import { useAppSelector } from '../../redux/hooks'; @@ -37,7 +37,7 @@ interface EditConversionModalComponentProps { export default function EditConversionModalComponent(props: EditConversionModalComponentProps) { const [editConversion] = conversionsApi.useEditConversionMutation() const [deleteConversion] = conversionsApi.useDeleteConversionMutation() - const { data: unitDataById = {} } = useAppSelector(selectUnitDataById) + const unitDataById = useAppSelector(selectUnitDataById) // Set existing conversion values const values = { ...props.conversion } @@ -148,7 +148,7 @@ export default function EditConversionModalComponent(props: EditConversionModalC - +
diff --git a/src/client/app/components/groups/CreateGroupModalComponent.tsx b/src/client/app/components/groups/CreateGroupModalComponent.tsx index aabdd213f..229fa8938 100644 --- a/src/client/app/components/groups/CreateGroupModalComponent.tsx +++ b/src/client/app/components/groups/CreateGroupModalComponent.tsx @@ -11,7 +11,7 @@ import { Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; import { State } from 'types/redux/state'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import '../../styles/modal.css'; import { tooltipBaseStyle } from '../../styles/modalStyle'; import { ConversionArray } from '../../types/conversionArray'; @@ -300,7 +300,7 @@ export default function CreateGroupModalComponent(props: CreateGroupModalCompone - +
diff --git a/src/client/app/components/groups/CreateGroupModalComponentWIP.tsx b/src/client/app/components/groups/CreateGroupModalComponentWIP.tsx index 9c77ee573..5b0e4ce4f 100644 --- a/src/client/app/components/groups/CreateGroupModalComponentWIP.tsx +++ b/src/client/app/components/groups/CreateGroupModalComponentWIP.tsx @@ -10,7 +10,7 @@ import { Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; import { GroupData } from 'types/redux/groups'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { groupsApi, selectGroupDataById } from '../../redux/api/groupsApi'; import { selectMeterDataById } from '../../redux/api/metersApi'; import { selectUnitDataById } from '../../redux/api/unitsApi'; @@ -42,11 +42,11 @@ export default function CreateGroupModalComponentWIP() { const [createGroup] = groupsApi.useCreateGroupMutation() // Meters state - const { data: metersDataById = {} } = useAppSelector(selectMeterDataById); + const metersDataById = useAppSelector(selectMeterDataById); // Groups state - const { data: groupsDataById = {} } = useAppSelector(selectGroupDataById); + const groupDataById = useAppSelector(selectGroupDataById); // Units state - const { data: unitsDataById = {} } = useAppSelector(selectUnitDataById); + const unitsDataById = useAppSelector(selectUnitDataById); // Check for admin status const possibleGraphicUnits = useAppSelector(selectPossibleGraphicUnits) @@ -296,7 +296,7 @@ export default function CreateGroupModalComponentWIP() { - +
@@ -539,7 +539,7 @@ export default function CreateGroupModalComponentWIP() { state.childGroups.forEach(groupId => { selectedGroupsUnsorted.push({ value: groupId, - label: groupsDataById[groupId].name + label: groupDataById[groupId].name // isDisabled not needed since only used for selected and not display. } as SelectOption ); diff --git a/src/client/app/components/groups/EditGroupModalComponent.tsx b/src/client/app/components/groups/EditGroupModalComponent.tsx index 3a5eddad1..dbcbe091a 100644 --- a/src/client/app/components/groups/EditGroupModalComponent.tsx +++ b/src/client/app/components/groups/EditGroupModalComponent.tsx @@ -13,7 +13,7 @@ import { Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; import { State } from 'types/redux/state'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import '../../styles/card-page.css'; import '../../styles/modal.css'; import { tooltipBaseStyle } from '../../styles/modalStyle'; @@ -413,7 +413,7 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr {/* In a number of the items that follow, what is shown varies on whether you are an admin. */} - +
diff --git a/src/client/app/components/groups/EditGroupModalComponentWIP.tsx b/src/client/app/components/groups/EditGroupModalComponentWIP.tsx index 371f746cf..37041246c 100644 --- a/src/client/app/components/groups/EditGroupModalComponentWIP.tsx +++ b/src/client/app/components/groups/EditGroupModalComponentWIP.tsx @@ -11,7 +11,7 @@ import { Button, Col, Container, FormFeedback, FormGroup, Input, InputGroup, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { groupsApi, selectGroupDataById } from '../../redux/api/groupsApi'; import { selectMeterDataById } from '../../redux/api/metersApi'; import { useAppSelector } from '../../redux/hooks'; @@ -59,13 +59,13 @@ export default function EditGroupModalComponentWIP(props: EditGroupModalComponen const [submitGroupEdits] = groupsApi.useEditGroupMutation() const [deleteGroup] = groupsApi.useDeleteGroupMutation() // Meter state - const { data: metersState = {} } = useAppSelector(selectMeterDataById); + const meterDataById = useAppSelector(selectMeterDataById); // Group state used on other pages - const { data: globalGroupsState = {} } = useAppSelector(selectGroupDataById); + const groupDataById = useAppSelector(selectGroupDataById); // Make a local copy of the group data so we can update during the edit process. // When the group is saved the values will be synced again with the global state. // This needs to be a deep clone so the changes are only local. - const [editGroupsState, setEditGroupsState] = useState(_.cloneDeep(globalGroupsState)); + const [editGroupsState, setEditGroupsState] = useState(_.cloneDeep(groupDataById)); const possibleGraphicUnits = useAppSelector(selectPossibleGraphicUnits) // The current groups state of group being edited of the local copy. It should always be valid. @@ -180,7 +180,7 @@ export default function EditGroupModalComponentWIP(props: EditGroupModalComponen let areaSum = 0; let notifyMsg = ''; groupState.deepMeters.forEach(meterID => { - const meter = metersState[meterID]; + const meter = meterDataById[meterID]; if (meter.area > 0) { if (meter.areaUnit != AreaUnitType.none) { areaSum += meter.area * getAreaUnitConversion(meter.areaUnit, groupState.areaUnit); @@ -222,7 +222,7 @@ export default function EditGroupModalComponentWIP(props: EditGroupModalComponen // Failure to edit groups will not trigger a re-render, as no state has changed. Therefore, we must manually reset the values const resetState = () => { // Set back to the global group values for this group. As before, need a deep copy. - setEditGroupsState(_.cloneDeep(globalGroupsState)); + setEditGroupsState(_.cloneDeep(groupDataById)); // Set back to the default values for the menus. setGroupChildrenState(groupChildrenDefaults); setGraphicUnitsState(graphicUnitsStateDefaults); @@ -253,7 +253,7 @@ export default function EditGroupModalComponentWIP(props: EditGroupModalComponen // Check for changes by comparing the original, global state to edited state. // This is the unedited state of the group being edited to compare to for changes. - const originalGroupState = globalGroupsState[groupState.id]; + const originalGroupState = groupDataById[groupState.id]; // Check children separately since lists. const childMeterChanges = !_.isEqual(originalGroupState.childMeters, groupState.childMeters); const childGroupChanges = !_.isEqual(originalGroupState.childGroups, groupState.childGroups); @@ -304,7 +304,7 @@ export default function EditGroupModalComponentWIP(props: EditGroupModalComponen // been made in the edit state. const groupsChanged: number[] = []; Object.values(editGroupsState).forEach(group => { - if (group.defaultGraphicUnit !== globalGroupsState[group.id].defaultGraphicUnit) { + if (group.defaultGraphicUnit !== groupDataById[group.id].defaultGraphicUnit) { groupsChanged.push(group.id); } }); @@ -414,7 +414,7 @@ export default function EditGroupModalComponentWIP(props: EditGroupModalComponen {/* In a number of the items that follow, what is shown varies on whether you are an admin. */} - +
@@ -607,7 +607,7 @@ export default function EditGroupModalComponentWIP(props: EditGroupModalComponen // The new child meter removal was rejected so put it back. Should only be one item so no need to sort. newSelectedMeterOptions.push({ value: removedMeterId, - label: metersState[removedMeterId].identifier + label: meterDataById[removedMeterId].identifier // isDisabled not needed since only used for selected and not display. } as SelectOption ); @@ -920,7 +920,7 @@ export default function EditGroupModalComponentWIP(props: EditGroupModalComponen groupState.childMeters.forEach(groupId => { selectedMetersUnsorted.push({ value: groupId, - label: metersState[groupId].identifier + label: meterDataById[groupId].identifier // isDisabled not needed since only used for selected and not display. } as SelectOption ); @@ -941,7 +941,7 @@ export default function EditGroupModalComponentWIP(props: EditGroupModalComponen value: groupId, // Use globalGroupsState so see edits in other groups. You would miss an update // in this group but it cannot be on the menu so that is okay. - label: globalGroupsState[groupId].name + label: groupDataById[groupId].name // isDisabled not needed since only used for selected and not display. } as SelectOption ); @@ -961,7 +961,7 @@ export default function EditGroupModalComponentWIP(props: EditGroupModalComponen // Tells if any meter is not visible to user. let hasHidden = false; groupState.childMeters.forEach(meterId => { - const meterIdentifier = metersState[meterId].identifier; + const meterIdentifier = meterDataById[meterId].identifier; // The identifier is null if the meter is not visible to this user. If hidden then do // not list and otherwise label. if (meterIdentifier === null) { @@ -1020,7 +1020,7 @@ export default function EditGroupModalComponentWIP(props: EditGroupModalComponen const listedDeepMeters: string[] = []; let hasHidden = false; groupState.deepMeters.forEach(meterId => { - const meterIdentifier = metersState[meterId].identifier; + const meterIdentifier = meterDataById[meterId].identifier; if (meterIdentifier === null) { // The identifier is null if the meter is not visible to this user. hasHidden = true; diff --git a/src/client/app/components/groups/GroupViewComponentWIP.tsx b/src/client/app/components/groups/GroupViewComponentWIP.tsx index abec803d6..296d30d40 100644 --- a/src/client/app/components/groups/GroupViewComponentWIP.tsx +++ b/src/client/app/components/groups/GroupViewComponentWIP.tsx @@ -45,7 +45,7 @@ export default function GroupViewComponentWIP(props: GroupViewComponentProps) { // Set up to display the units associated with the group as the unit identifier. // unit state - const { data: unitDataById = {} } = useAppSelector(selectUnitDataById); + const unitDataById = useAppSelector(selectUnitDataById); return ( diff --git a/src/client/app/components/groups/GroupsDetailComponent.tsx b/src/client/app/components/groups/GroupsDetailComponent.tsx index 5c90c5ef3..66434d94f 100644 --- a/src/client/app/components/groups/GroupsDetailComponent.tsx +++ b/src/client/app/components/groups/GroupsDetailComponent.tsx @@ -4,9 +4,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import HeaderComponent from '../../components/HeaderComponent'; -import FooterContainer from '../../containers/FooterContainer'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { useAppSelector } from '../../redux/hooks'; import { selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; @@ -31,7 +29,7 @@ export default function GroupsDetailComponent() { const { visibleGroups } = useAppSelector(state => selectVisibleMetersGroupsDataByID(state)); // Units state - const { data: unitDataById = {} } = useAppSelector(selectUnitDataById); + const unitDataById = useAppSelector(selectUnitDataById); // Possible graphic units to use @@ -51,8 +49,7 @@ export default function GroupsDetailComponent() { return (
- - +

@@ -84,7 +81,6 @@ export default function GroupsDetailComponent() {

}
-

); diff --git a/src/client/app/components/groups/GroupsDetailComponentWIP.tsx b/src/client/app/components/groups/GroupsDetailComponentWIP.tsx index a6fb8edb0..1b6130402 100644 --- a/src/client/app/components/groups/GroupsDetailComponentWIP.tsx +++ b/src/client/app/components/groups/GroupsDetailComponentWIP.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { useAppSelector } from '../../redux/hooks'; import { selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; import { selectVisibleMetersGroupsDataByID } from '../../redux/selectors/dataSelectors'; @@ -40,7 +40,7 @@ export default function GroupsDetailComponentWIP() { return (
- +

diff --git a/src/client/app/components/maps/MapsDetailComponent.tsx b/src/client/app/components/maps/MapsDetailComponent.tsx index fe7a8bfbc..320d0bbf6 100644 --- a/src/client/app/components/maps/MapsDetailComponent.tsx +++ b/src/client/app/components/maps/MapsDetailComponent.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router-dom-v5-compat'; import { Button, Table } from 'reactstrap'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import UnsavedWarningContainer from '../../containers/UnsavedWarningContainer'; import MapViewContainer from '../../containers/maps/MapViewContainer'; import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; @@ -59,7 +59,7 @@ export default class MapsDetailComponent extends React.Component - +

diff --git a/src/client/app/components/meters/CreateMeterModalComponent.tsx b/src/client/app/components/meters/CreateMeterModalComponent.tsx index dd04ae3e3..0b418ef27 100644 --- a/src/client/app/components/meters/CreateMeterModalComponent.tsx +++ b/src/client/app/components/meters/CreateMeterModalComponent.tsx @@ -11,7 +11,7 @@ import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, M import { Dispatch } from 'types/redux/actions'; import { State } from 'types/redux/state'; import { addMeter } from '../../actions/meters'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import '../../styles/modal.css'; import { tooltipBaseStyle } from '../../styles/modalStyle'; import { ConversionArray } from '../../types/conversionArray'; @@ -358,7 +358,7 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone - +
diff --git a/src/client/app/components/meters/CreateMeterModalComponentWIP.tsx b/src/client/app/components/meters/CreateMeterModalComponentWIP.tsx index b187cbb02..b8b2f09ea 100644 --- a/src/client/app/components/meters/CreateMeterModalComponentWIP.tsx +++ b/src/client/app/components/meters/CreateMeterModalComponentWIP.tsx @@ -7,7 +7,7 @@ import * as React from 'react'; import { useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { metersApi } from '../../redux/api/metersApi'; import { useAppSelector } from '../../redux/hooks'; import { makeSelectGraphicUnitCompatibility } from '../../redux/selectors/adminSelectors'; @@ -287,7 +287,7 @@ export default function CreateMeterModalComponent() { - +
diff --git a/src/client/app/components/meters/EditMeterModalComponent.tsx b/src/client/app/components/meters/EditMeterModalComponent.tsx index 7b0e1363c..ae102e264 100644 --- a/src/client/app/components/meters/EditMeterModalComponent.tsx +++ b/src/client/app/components/meters/EditMeterModalComponent.tsx @@ -13,7 +13,7 @@ import '../../styles/modal.css'; import { MeterData, MeterTimeSortType, MeterType } from '../../types/redux/meters'; import { submitEditedMeter } from '../../actions/meters'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { TrueFalseType } from '../../types/items'; import TimeZoneSelect from '../TimeZoneSelect'; import { GPSPoint, isValidGPSInput } from '../../utils/calibration'; @@ -398,7 +398,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr - +
diff --git a/src/client/app/components/meters/EditMeterModalComponentWIP.tsx b/src/client/app/components/meters/EditMeterModalComponentWIP.tsx index 0a777a2cc..52f2b667d 100644 --- a/src/client/app/components/meters/EditMeterModalComponentWIP.tsx +++ b/src/client/app/components/meters/EditMeterModalComponentWIP.tsx @@ -8,7 +8,7 @@ import * as React from 'react'; import { useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; import { metersApi, selectMeterDataWithID } from '../../redux/api/metersApi'; import { selectUnitDataById } from '../../redux/api/unitsApi'; @@ -56,7 +56,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr useEffect(() => { setLocalMeterEdits(_.cloneDeep(meterState)) }, [meterState]) /* State */ // unit state - const { data: unitDataById = {} } = useAppSelector(selectUnitDataById); + const unitDataById = useAppSelector(selectUnitDataById); const [validMeter, setValidMeter] = useState(isValidMeter(localMeterEdits)); @@ -171,7 +171,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr - +
diff --git a/src/client/app/components/meters/MetersDetailComponent.tsx b/src/client/app/components/meters/MetersDetailComponent.tsx index fdaecb25b..c30585759 100644 --- a/src/client/app/components/meters/MetersDetailComponent.tsx +++ b/src/client/app/components/meters/MetersDetailComponent.tsx @@ -5,9 +5,7 @@ import * as _ from 'lodash'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import HeaderComponent from '../../components/HeaderComponent'; -import FooterContainer from '../../containers/FooterContainer'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { useAppSelector } from '../../redux/hooks'; import { selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; import { selectVisibleMetersGroupsDataByID } from '../../redux/selectors/dataSelectors'; @@ -38,7 +36,7 @@ export default function MetersDetailComponent() { const { visibleMeters } = useAppSelector(state => selectVisibleMetersGroupsDataByID(state)); // Units state - const { data: unitDataById = {} } = useAppSelector(selectUnitDataById); + const unitDataById = useAppSelector(selectUnitDataById); // TODO? Convert into Selector? // Possible Meter Units to use @@ -70,8 +68,7 @@ export default function MetersDetailComponent() { return (
- - +

@@ -105,7 +102,6 @@ export default function MetersDetailComponent() {

}
-

); } diff --git a/src/client/app/components/meters/MetersDetailComponentWIP.tsx b/src/client/app/components/meters/MetersDetailComponentWIP.tsx index c5dcaa371..7847d034f 100644 --- a/src/client/app/components/meters/MetersDetailComponentWIP.tsx +++ b/src/client/app/components/meters/MetersDetailComponentWIP.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { useAppSelector } from '../../redux/hooks'; import { selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; import { selectVisibleMetersGroupsDataByID } from '../../redux/selectors/dataSelectors'; @@ -27,7 +27,7 @@ export default function MetersDetailComponent() { return (
- +

diff --git a/src/client/app/components/unit/CreateUnitModalComponent.tsx b/src/client/app/components/unit/CreateUnitModalComponent.tsx index 5a10c9c1d..e9be283af 100644 --- a/src/client/app/components/unit/CreateUnitModalComponent.tsx +++ b/src/client/app/components/unit/CreateUnitModalComponent.tsx @@ -10,7 +10,7 @@ import translate from '../../utils/translate'; import '../../styles/modal.css'; import { TrueFalseType } from '../../types/items'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { UnitRepresentType, DisplayableType, UnitType } from '../../types/redux/units'; import { addUnit } from '../../actions/units'; import { tooltipBaseStyle } from '../../styles/modalStyle'; @@ -113,7 +113,7 @@ export default function CreateUnitModalComponent() { - +
diff --git a/src/client/app/components/unit/EditUnitModalComponent.tsx b/src/client/app/components/unit/EditUnitModalComponent.tsx index 00bd6b126..3a954cd3a 100644 --- a/src/client/app/components/unit/EditUnitModalComponent.tsx +++ b/src/client/app/components/unit/EditUnitModalComponent.tsx @@ -10,7 +10,7 @@ import { useDispatch } from 'react-redux'; import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; import { Dispatch } from 'types/redux/actions'; import { submitEditedUnit } from '../../actions/units'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; import { selectConversionsDetails } from '../../redux/api/conversionsApi'; import { selectMeterDataById } from '../../redux/api/metersApi'; @@ -56,7 +56,7 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp /* State */ // Handlers for each type of input change const [state, setState] = useState(values); - const { data: globalConversionsState = [] } = useAppSelector(selectConversionsDetails); + const conversionData = useAppSelector(selectConversionsDetails); const handleStringChange = (e: React.ChangeEvent) => { @@ -103,7 +103,7 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp const shouldUpdateUnit = (): boolean => { // true if inputted values are okay and there are changes. let inputOk = true; - const { data: meterDataByID = {} } = selectMeterDataById(store.getState()) + const meterDataByID = selectMeterDataById(store.getState()) // Check for case 1 if (props.unit.typeOfUnit === UnitType.meter && state.typeOfUnit !== UnitType.meter) { @@ -172,7 +172,7 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp // 1. If the unit is used, the Unit Represent cannot be changed. // 2. Otherwise, the Unit Represent can be changed. const inConversions = () => { - for (const conversion of globalConversionsState) { + for (const conversion of conversionData) { if (conversion.sourceId === state.id || conversion.destinationId === state.id) { return true; } @@ -190,7 +190,7 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp - +
diff --git a/src/client/app/components/unit/UnitsDetailComponent.tsx b/src/client/app/components/unit/UnitsDetailComponent.tsx index c1a958931..a3bfb79f2 100644 --- a/src/client/app/components/unit/UnitsDetailComponent.tsx +++ b/src/client/app/components/unit/UnitsDetailComponent.tsx @@ -5,8 +5,8 @@ import { QueryStatus } from '@reduxjs/toolkit/query'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import SpinnerComponent from '../../components/SpinnerComponent'; -import TooltipHelpContainer from '../../containers/TooltipHelpContainer'; -import { selectUnitDataById } from '../../redux/api/unitsApi'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; +import { selectUnitDataByIdQueryState } from '../../redux/api/unitsApi'; import { useAppSelector } from '../../redux/hooks'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateUnitModalComponent from './CreateUnitModalComponent'; @@ -20,7 +20,7 @@ export default function UnitsDetailComponent() { // The route stops you from getting to this page if not an admin. //Units state - const { data: unitDataById = {}, status } = useAppSelector(selectUnitDataById); + const { data: unitDataById = {}, status } = useAppSelector(selectUnitDataByIdQueryState); return ( @@ -32,7 +32,7 @@ export default function UnitsDetailComponent() {

) : (
- +

diff --git a/src/client/app/containers/CompareChartContainer.ts b/src/client/app/containers/CompareChartContainer.ts index 2335cf7a8..15ef45513 100644 --- a/src/client/app/containers/CompareChartContainer.ts +++ b/src/client/app/containers/CompareChartContainer.ts @@ -47,9 +47,9 @@ function mapStateToProps(state: RootState, ownProps: CompareChartContainerProps) // Also need to determine if raw. const graphingUnit = state.graph.selectedUnit; // This container is not called if there is no data of there are not units so this is safe. - const { data: unitDataById = {} } = selectUnitDataById(state) - const { data: meterDataById = {} } = selectMeterDataById(state) - const { data: groupDataById = {} } = selectGroupDataById(state) + const unitDataById = selectUnitDataById(state) + const meterDataById = selectMeterDataById(state) + const groupDataById = selectGroupDataById(state) const selectUnitState = unitDataById[graphingUnit]; let unitLabel: string = ''; // If graphingUnit is -99 then none selected and nothing to graph so label is empty. diff --git a/src/client/app/containers/FooterContainer.ts b/src/client/app/containers/FooterContainer.ts deleted file mode 100644 index fef1142fe..000000000 --- a/src/client/app/containers/FooterContainer.ts +++ /dev/null @@ -1,26 +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 { fetchVersionIfNeeded } from '../actions/version' -import FooterComponent from '../components/FooterComponent'; -import { State } from '../types/redux/state'; -import { Dispatch } from '../types/redux/actions'; - -/** - * A container that does data fetching for FooterComponent and connects it to the redux store. - */ -function mapStateToProps(state: State) { - return { - version: state.version.version - }; -} - -function mapDispatchToProps(dispatch: Dispatch) { - return { - fetchVersionIfNeeded: () => dispatch(fetchVersionIfNeeded()) - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(FooterComponent); diff --git a/src/client/app/containers/TooltipHelpContainer.ts b/src/client/app/containers/TooltipHelpContainer.ts deleted file mode 100644 index 488d3440f..000000000 --- a/src/client/app/containers/TooltipHelpContainer.ts +++ /dev/null @@ -1,26 +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 { fetchVersionIfNeeded } from '../actions/version' -import TooltipHelpComponent from '../components/TooltipHelpComponent'; -import { State } from '../types/redux/state'; -import { Dispatch } from '../types/redux/actions'; - -/** - * A container that does data fetching for TooltipHelpComponent and connects it to the redux store. - */ -function mapStateToProps(state: State) { - return { - version: state.version.version - }; -} - -function mapDispatchToProps(dispatch: Dispatch) { - return { - fetchVersionIfNeeded: () => dispatch(fetchVersionIfNeeded()) - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(TooltipHelpComponent); diff --git a/src/client/app/containers/admin/UsersDetailContainer.tsx b/src/client/app/containers/admin/UsersDetailContainer.tsx index c7d211cbb..50c765927 100644 --- a/src/client/app/containers/admin/UsersDetailContainer.tsx +++ b/src/client/app/containers/admin/UsersDetailContainer.tsx @@ -10,7 +10,6 @@ import { User, UserRole } from '../../types/items'; import { usersApi } from '../../utils/api'; import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; import translate from '../../utils/translate'; -import FooterContainer from '../FooterContainer'; interface UsersDisplayContainerProps { fetchUsers: () => User[]; @@ -90,7 +89,6 @@ export default class UsersDetailContainer extends React.Component -

) } diff --git a/src/client/app/containers/csv/UploadCSVContainer.tsx b/src/client/app/containers/csv/UploadCSVContainer.tsx index 7307691af..be44fe5df 100644 --- a/src/client/app/containers/csv/UploadCSVContainer.tsx +++ b/src/client/app/containers/csv/UploadCSVContainer.tsx @@ -3,17 +3,15 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Nav, NavItem, NavLink, TabContent, TabPane } from 'reactstrap'; +import TooltipMarkerComponent from '../../components/TooltipMarkerComponent'; import MetersCSVUploadComponent from '../../components/csv/MetersCSVUploadComponent'; import ReadingsCSVUploadComponent from '../../components/csv/ReadingsCSVUploadComponent'; -import FooterContainer from '../FooterContainer'; +import { BooleanMeterTypes, MetersCSVUploadPreferencesItem, ReadingsCSVUploadPreferencesItem, TimeSortTypes } from '../../types/csvUploadForm'; import { uploadCSVApi } from '../../utils/api'; -import { ReadingsCSVUploadPreferencesItem, MetersCSVUploadPreferencesItem, TimeSortTypes, BooleanMeterTypes } from '../../types/csvUploadForm'; -import { ReadingsCSVUploadDefaults, MetersCSVUploadDefaults } from '../../utils/csvUploadDefaults'; -import { TabContent, TabPane, Nav, NavItem, NavLink } from 'reactstrap'; -import { FormattedMessage } from 'react-intl'; -import TooltipHelpContainer from '../TooltipHelpContainer'; -import TooltipMarkerComponent from '../../components/TooltipMarkerComponent'; -import HeaderComponent from '../../components/HeaderComponent'; +import { MetersCSVUploadDefaults, ReadingsCSVUploadDefaults } from '../../utils/csvUploadDefaults'; +import TooltipHelpComponent from '../../components/TooltipHelpComponent'; export const enum MODE { meters = 'meters', @@ -269,8 +267,7 @@ export default class UploadCSVContainer extends React.Component<{}, UploadCSVCon } return (
- - +
) } diff --git a/src/client/app/initScript.ts b/src/client/app/initScript.ts index 9a2b5f632..906c8807a 100644 --- a/src/client/app/initScript.ts +++ b/src/client/app/initScript.ts @@ -2,6 +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 { versionApi } from './redux/api/versionApi'; import { authApi } from './redux/api/authApi'; import { conversionsApi } from './redux/api/conversionsApi'; import { groupsApi } from './redux/api/groupsApi'; @@ -20,6 +21,7 @@ export const initializeApp = async () => { // 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. + store.dispatch(versionApi.endpoints.getVersion.initiate()) store.dispatch(preferencesApi.endpoints.getPreferences.initiate()) store.dispatch(unitsApi.endpoints.getUnitsDetails.initiate()) store.dispatch(conversionsApi.endpoints.getConversionsDetails.initiate()) diff --git a/src/client/app/reducers/graph.ts b/src/client/app/reducers/graph.ts index 739265737..45946cdef 100644 --- a/src/client/app/reducers/graph.ts +++ b/src/client/app/reducers/graph.ts @@ -3,13 +3,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { PayloadAction, createSlice } from '@reduxjs/toolkit'; +import * as _ from 'lodash'; import * as moment from 'moment'; -import * as _ from 'lodash' import { ActionMeta } from 'react-select'; import { TimeInterval } from '../../../common/TimeInterval'; import { preferencesApi } from '../redux/api/preferencesApi'; import { SelectOption } from '../types/items'; -import { ChartTypes, GraphState, GraphStateHistory, LineGraphRate, MeterOrGroup, ReadingInterval } from '../types/redux/graph'; +import { ChartTypes, GraphState, LineGraphRate, MeterOrGroup, ReadingInterval } from '../types/redux/graph'; import { ComparePeriod, SortingOrder, calculateCompareTimeInterval } from '../utils/calculateCompare'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; @@ -215,9 +215,14 @@ export const graphSlice = createSlice({ } }, - updateHistory: (state, action: PayloadAction) => { - state.backHistoryStack.push(action.payload) - // reset forward history on new visit + resetTimeInterval: state => { + if (!state.queryTimeInterval.equals(TimeInterval.unbounded())) { + state.queryTimeInterval = TimeInterval.unbounded() + } + }, + updateHistory: (state, action: PayloadAction) => { + state.backHistoryStack.push(_.omit(action.payload, ['backHistoryStack', 'forwardHistoryStack'])) + // reset forward history on new 'visit' state.forwardHistoryStack = [] }, prevHistory: state => { @@ -231,13 +236,6 @@ export const graphSlice = createSlice({ state.backHistoryStack.push(state.forwardHistoryStack.pop()!) Object.assign(state, state.backHistoryStack[state.backHistoryStack.length - 1]) } - - - }, - resetTimeInterval: state => { - if (!state.queryTimeInterval.equals(TimeInterval.unbounded())) { - state.queryTimeInterval = TimeInterval.unbounded() - } } }, extraReducers: builder => { diff --git a/src/client/app/reducers/index.ts b/src/client/app/reducers/index.ts index 9b753b34d..0197381d3 100644 --- a/src/client/app/reducers/index.ts +++ b/src/client/app/reducers/index.ts @@ -5,10 +5,8 @@ import { combineReducers } from 'redux'; import maps from './maps'; import { adminSlice } from './admin'; -import { versionSlice } from './version'; import { currentUserSlice } from './currentUser'; import { unsavedWarningSlice } from './unsavedWarning'; -import { conversionsSlice } from './conversions'; import { optionsSlice } from './options'; import { baseApi } from '../redux/api/baseApi'; import { graphSlice } from './graph'; @@ -17,10 +15,8 @@ export const rootReducer = combineReducers({ maps, graph: graphSlice.reducer, admin: adminSlice.reducer, - version: versionSlice.reducer, currentUser: currentUserSlice.reducer, unsavedWarning: unsavedWarningSlice.reducer, - conversions: conversionsSlice.reducer, options: optionsSlice.reducer, // RTK Query's Derived Reducers [baseApi.reducerPath]: baseApi.reducer diff --git a/src/client/app/reducers/version.ts b/src/client/app/reducers/version.ts deleted file mode 100644 index 191db9bef..000000000 --- a/src/client/app/reducers/version.ts +++ /dev/null @@ -1,60 +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 { VersionState } from '../types/redux/version'; -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; - -/* -* Defines store interactions when user profile related actions are dispatched to the store. -*/ -const defaultState: VersionState = { isFetching: false, version: '' }; -export const versionSlice = createSlice({ - name: 'version', - initialState: defaultState, - reducers: { - // case ActionType.RequestVersion: - // // When version is requested, indicate app is fetching data from API - // return { - // ...state, - // isFetching: true - // }; - requestVersion: state => { - state.isFetching = true; - }, - // case ActionType.ReceiveVersion: - // // When version is received, update the store with result from API - // return { - // ...state, - // isFetching: false, - // version: action.data - // }; - receiveVersion: (state, action: PayloadAction) => { - state.isFetching = false; - state.version = action.payload; - } - } -}); -// default: -// return state; -// } - -// export default function version(state = defaultState, action: VersionAction) { -// switch (action.type) { -// case ActionType.RequestVersion: -// // When version is requested, indicate app is fetching data from API -// return { -// ...state, -// isFetching: true -// }; -// case ActionType.ReceiveVersion: -// // When version is received, update the store with result from API -// return { -// ...state, -// isFetching: false, -// version: action.data -// }; -// default: -// return state; -// } -// } \ No newline at end of file diff --git a/src/client/app/redux/api/authApi.ts b/src/client/app/redux/api/authApi.ts index 8617a9fa7..336be93b4 100644 --- a/src/client/app/redux/api/authApi.ts +++ b/src/client/app/redux/api/authApi.ts @@ -65,6 +65,7 @@ export const authApi = baseApi.injectEndpoints({ logout: builder.mutation({ queryFn: (_, { dispatch }) => { // Opt to use a RTK mutation instead of manually writing a thunk to take advantage mutation invalidations + deleteToken() dispatch(currentUserSlice.actions.clearCurrentUser()) return { data: null } }, diff --git a/src/client/app/redux/api/conversionsApi.ts b/src/client/app/redux/api/conversionsApi.ts index 18e342ef4..2b5ecc6e1 100644 --- a/src/client/app/redux/api/conversionsApi.ts +++ b/src/client/app/redux/api/conversionsApi.ts @@ -1,3 +1,4 @@ +import { createSelector } from '@reduxjs/toolkit'; import { ConversionData } from '../../types/redux/conversions'; import { baseApi } from './baseApi'; @@ -88,5 +89,18 @@ export const conversionsApi = baseApi.injectEndpoints({ }) }) -export const selectPIK = conversionsApi.endpoints.getConversionArray.select() -export const selectConversionsDetails = conversionsApi.endpoints.getConversionsDetails.select() \ No newline at end of file +export const selectConversionsQueryState = conversionsApi.endpoints.getConversionsDetails.select() +export const selectConversionsDetails = createSelector( + selectConversionsQueryState, + ({ data: conversionData = [] }) => { + return conversionData + } +) + +export const selectPikQueryState = conversionsApi.endpoints.getConversionArray.select() +export const selectPik = createSelector( + selectPikQueryState, + ({ data: pik = [[]] }) => { + return pik + } +) \ No newline at end of file diff --git a/src/client/app/redux/api/groupsApi.ts b/src/client/app/redux/api/groupsApi.ts index ab4ab74a9..92bc49137 100644 --- a/src/client/app/redux/api/groupsApi.ts +++ b/src/client/app/redux/api/groupsApi.ts @@ -5,6 +5,7 @@ import { selectIsLoggedInAsAdmin } from '../selectors/authSelectors'; import { RootState } from '../../store'; import { CompareReadings } from 'types/readings'; import { TimeInterval } from '../../../../common/TimeInterval'; +import { createSelector } from '@reduxjs/toolkit'; export const groupsApi = baseApi.injectEndpoints({ endpoints: builder => ({ @@ -97,10 +98,16 @@ export const groupsApi = baseApi.injectEndpoints({ }) }) -export const selectGroupDataById = groupsApi.endpoints.getGroups.select(); +export const selectGroupDataByIdQueryState = groupsApi.endpoints.getGroups.select(); +export const selectGroupDataById = createSelector( + selectGroupDataByIdQueryState, + ({ data: groupDataById = {} }) => { + return groupDataById + } +) export const selectGroupDataWithID = (state: RootState, groupId: number): GroupData | undefined => { - const { data: groupDataById = {} } = selectGroupDataById(state) + const groupDataById = selectGroupDataById(state) return groupDataById[groupId] } diff --git a/src/client/app/redux/api/metersApi.ts b/src/client/app/redux/api/metersApi.ts index 1a34ecf39..51b407968 100644 --- a/src/client/app/redux/api/metersApi.ts +++ b/src/client/app/redux/api/metersApi.ts @@ -7,6 +7,7 @@ import { MeterData, MeterDataByID } from '../../types/redux/meters'; import { durationFormat } from '../../utils/durationFormat'; import { baseApi } from './baseApi'; import { conversionsApi } from './conversionsApi'; +import { createSelector } from '@reduxjs/toolkit'; export const metersApi = baseApi.injectEndpoints({ @@ -57,6 +58,7 @@ export const metersApi = baseApi.injectEndpoints({ }) }) +export const selectMeterDataByIdQueryState = metersApi.endpoints.getMeters.select() /** * Selects the meter data associated with a given meter ID from the Redux state. * @param {RootState} state - The current state of the Redux store. @@ -67,7 +69,12 @@ export const metersApi = baseApi.injectEndpoints({ * or * const { data: meterDataByID } = useAppSelector(state => selectMeterDataById(state)) */ -export const selectMeterDataById = metersApi.endpoints.getMeters.select() +export const selectMeterDataById = createSelector( + selectMeterDataByIdQueryState, + ({ data: meterDataById = {} }) => { + return meterDataById + } +) /** @@ -79,7 +86,7 @@ export const selectMeterDataById = metersApi.endpoints.getMeters.select() * const meterData = useAppSelector(state => selectMeterDataWithID(state, 42)) */ export const selectMeterDataWithID = (state: RootState, meterID: number): MeterData | undefined => { - const { data: meterDataByID = {} } = selectMeterDataById(state); + const meterDataByID = selectMeterDataById(state); return meterDataByID[meterID]; } diff --git a/src/client/app/redux/api/unitsApi.ts b/src/client/app/redux/api/unitsApi.ts index acf8a3310..348d35b5a 100644 --- a/src/client/app/redux/api/unitsApi.ts +++ b/src/client/app/redux/api/unitsApi.ts @@ -2,6 +2,7 @@ import * as _ from 'lodash'; import { RootState } from 'store'; import { UnitData, UnitDataById } from '../../types/redux/units'; import { baseApi } from './baseApi'; +import { createSelector } from '@reduxjs/toolkit'; export const unitsApi = baseApi.injectEndpoints({ endpoints: builder => ({ @@ -29,16 +30,33 @@ export const unitsApi = baseApi.injectEndpoints({ }) }) + +/** + * Selects the most recent query status + * @param state - The complete state of the redux store. + * @returns The unit data corresponding to the `unitID` if found, or undefined if not. + * @example + * + * const queryState = useAppSelector(state => selectUnitDataByIdQueryState(state)) + * const {data: unitDataById = {}} = useAppSelector(state => selectUnitDataById(state)) + */ +export const selectUnitDataByIdQueryState = unitsApi.endpoints.getUnitsDetails.select() + /** * Selects the most recent query status * @param state - The complete state of the redux store. * @returns The unit data corresponding to the `unitID` if found, or undefined if not. * @example * - * const { data: unitDataById = {} } = useAppSelector(state =>selectUnitDataById(state)) - * const { data: unitDataById = {} } = useAppSelector(selectUnitDataById) + * const unitDataById = useAppSelector(state =>selectUnitDataById(state)) + * const unitDataById = useAppSelector(selectUnitDataById) */ -export const selectUnitDataById = unitsApi.endpoints.getUnitsDetails.select() +export const selectUnitDataById = createSelector( + selectUnitDataByIdQueryState, + ({ data: unitDataById = {} }) => { + return unitDataById + } +) /** * Selects a unit from the state by its unique identifier. @@ -51,7 +69,7 @@ export const selectUnitDataById = unitsApi.endpoints.getUnitsDetails.select() * const unit = useAppSelector(state => selectUnitWithID(state, 1)) */ export const selectUnitWithID = (state: RootState, unitID: number) => { - const { data: unitDataById = {} } = selectUnitDataById(state) + const unitDataById = selectUnitDataById(state) return unitDataById[unitID] } diff --git a/src/client/app/redux/api/versionApi.ts b/src/client/app/redux/api/versionApi.ts new file mode 100644 index 000000000..2b12e70b2 --- /dev/null +++ b/src/client/app/redux/api/versionApi.ts @@ -0,0 +1,18 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { baseApi } from './baseApi'; + +export const versionApi = baseApi.injectEndpoints({ + endpoints: builder => ({ + getVersion: builder.query({ + query: () => '/api/version' + }) + }) +}) + +export const selectVersion = versionApi.endpoints.getVersion.select() +export const selectOEDVersion = createSelector( + selectVersion, + ({ data: version }) => { + return version ?? '' + } +) \ No newline at end of file diff --git a/src/client/app/redux/middleware/graphHistory.ts b/src/client/app/redux/middleware/graphHistory.ts index 4f100b1dc..bf91b2a59 100644 --- a/src/client/app/redux/middleware/graphHistory.ts +++ b/src/client/app/redux/middleware/graphHistory.ts @@ -1,6 +1,5 @@ // https://redux-toolkit.js.org/api/createListenerMiddleware#typescript-usage import { createListenerMiddleware, isAnyOf } from '@reduxjs/toolkit' -import * as _ from 'lodash' import { graphSlice, nextHistory, @@ -31,9 +30,8 @@ startHistoryListening({ ) ) ), - effect: (action, api) => { - const state = api.getState(); - const historyState = _.omit(state.graph, ['backHistoryStack', 'forwardHistoryStack']) - api.dispatch(updateHistory(historyState)) + effect: (_action, { dispatch, getState }) => { + const { graph } = getState(); + dispatch(updateHistory(graph)) } }) diff --git a/src/client/app/redux/selectors/adminSelectors.ts b/src/client/app/redux/selectors/adminSelectors.ts index ba0233a10..e206fb6e4 100644 --- a/src/client/app/redux/selectors/adminSelectors.ts +++ b/src/client/app/redux/selectors/adminSelectors.ts @@ -44,9 +44,7 @@ export const selectAdminPreferences = createSelector( */ export const selectPossibleGraphicUnits = createSelector( selectUnitDataById, - ({ data: unitDataById = {} }) => { - return potentialGraphicUnits(unitDataById) - } + unitDataById => potentialGraphicUnits(unitDataById) ) /** @@ -56,7 +54,7 @@ export const selectPossibleGraphicUnits = createSelector( */ export const selectPossibleMeterUnits = createSelector( selectUnitDataById, - ({ data: unitDataById = {} }) => { + unitDataById => { let possibleMeterUnits = new Set(); // The meter unit can be any unit of type meter. Object.values(unitDataById).forEach(unit => { @@ -87,7 +85,7 @@ export const selectUnitName = createSelector( // ThisSelector takes an argument, due to one or more of the selectors accepts an argument (selectUnitWithID selectMeterDataWithID) selectUnitDataById, selectMeterDataWithID, - ({ data: unitDataById = {} }, meterData) => { + (unitDataById, meterData) => { const unitName = (Object.keys(unitDataById).length === 0 || !meterData || meterData.unitId === -99) ? noUnitTranslated().identifier : unitDataById[meterData.defaultGraphicUnit].identifier return unitName @@ -108,7 +106,7 @@ export const selectGraphicName = createSelector( // notice that this selector is written with inline selectors for demonstration purposes selectUnitDataById, selectMeterDataWithID, - ({ data: unitDataById = {} }, meterData) => { + (unitDataById, meterData) => { const graphicName = (Object.keys(unitDataById).length === 0 || !meterData || meterData.defaultGraphicUnit === -99) ? noUnitTranslated().identifier : unitDataById[meterData.defaultGraphicUnit].identifier return graphicName @@ -204,7 +202,7 @@ export const selectIsValidConversion = createSelector( selectUnitDataById, selectConversionsDetails, (_state: RootState, conversionData: ConversionData) => conversionData, - ({ data: unitDataById = {} }, { data: conversionData = [] }, { sourceId, destinationId, bidirectional }): [boolean, string] => { + (unitDataById, conversionData, { sourceId, destinationId, bidirectional }): [boolean, string] => { /* Create Conversion Validation: Source equals destination: invalid conversion Conversion exists: invalid conversion diff --git a/src/client/app/redux/selectors/dataSelectors.ts b/src/client/app/redux/selectors/dataSelectors.ts index d23d3a6b6..1ac2204f2 100644 --- a/src/client/app/redux/selectors/dataSelectors.ts +++ b/src/client/app/redux/selectors/dataSelectors.ts @@ -56,7 +56,7 @@ export interface ChartQueryArgsMeta { } // Selector prepares the query args for ALL graph endpoints based on the current graph slice state -// TODO Break down into individual selectors? Verify if prop drilling is required +// TODO Break down into individual selectors? Verify if prop drilling is a better pattern vs useSelector in same sameComponent export const selectChartQueryArgs = createSelector( selectGraphState, graphState => { @@ -148,15 +148,15 @@ export const selectVisibleMetersGroupsDataByID = createSelector( selectMeterDataById, selectGroupDataById, selectIsLoggedInAsAdmin, - ({ data: meterDataByID = {} }, { data: groupDataByID = {} }, isAdmin) => { + (meterDataById, groupDataById, isAdmin) => { let visibleMeters; let visibleGroups; if (isAdmin) { - visibleMeters = meterDataByID - visibleGroups = groupDataByID; + visibleMeters = meterDataById + visibleGroups = groupDataById; } else { - visibleMeters = _.filter(meterDataByID, meter => meter.displayable); - visibleGroups = _.filter(groupDataByID, group => group.displayable); + visibleMeters = _.filter(meterDataById, meter => meter.displayable); + visibleGroups = _.filter(groupDataById, group => group.displayable); } return { visibleMeters, visibleGroups } diff --git a/src/client/app/redux/selectors/threeDSelectors.ts b/src/client/app/redux/selectors/threeDSelectors.ts index f46ef2b3e..ef63a9b1a 100644 --- a/src/client/app/redux/selectors/threeDSelectors.ts +++ b/src/client/app/redux/selectors/threeDSelectors.ts @@ -19,7 +19,7 @@ export const selectThreeDComponentInfo = createSelector( selectThreeDMeterOrGroup, selectMeterDataById, selectGroupDataById, - (id, meterOrGroup, { data: meterDataById = {} }, { data: groupDataById = {} }) => { + (id, meterOrGroup, meterDataById, groupDataById) => { //Default Values let meterOrGroupName = 'Unselected Meter or Group' let isAreaCompatible = true; diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts index 7eb413af5..130c437f3 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -31,7 +31,7 @@ export const selectVisibleMetersAndGroups = createSelector( selectMeterDataById, selectGroupDataById, selectCurrentUser, - ({ data: meterDataByID = {} }, { data: groupDataById = {} }, currentUser) => { + (meterDataByID, groupDataById, currentUser) => { // Holds all meters visible to the user const visibleMeters = new Set(); const visibleGroups = new Set(); @@ -68,7 +68,7 @@ export const selectCurrentUnitCompatibility = createSelector( selectMeterDataById, selectGroupDataById, selectGraphUnitID, - (visible, { data: meterDataById = {} }, { data: groupDataById = {} }, graphUnitID) => { + (visible, meterDataById, groupDataById, graphUnitID) => { // meters and groups that can graph const compatibleMeters = new Set(); const compatibleGroups = new Set(); @@ -145,7 +145,7 @@ export const selectCurrentAreaCompatibility = createSelector( selectMeterDataById, selectGroupDataById, selectUnitDataById, - (currentUnitCompatibility, areaNormalization, unitID, { data: meterDataById = {} }, { data: groupDataById = {} }, { data: unitDataById = {} }) => { + (currentUnitCompatibility, areaNormalization, unitID, meterDataById, groupDataById, unitDataById) => { // Deep Copy previous selector's values, and update as needed based on current Area Normalization setting const compatibleMeters = new Set(currentUnitCompatibility.compatibleMeters); const compatibleGroups = new Set(currentUnitCompatibility.compatibleGroups); @@ -192,7 +192,7 @@ export const selectChartTypeCompatibility = createSelector( selectMeterDataById, selectGroupDataById, selectMapState, - (areaCompat, chartToRender, { data: meterDataById = {} }, { data: groupDataById = {} }, mapState) => { + (areaCompat, chartToRender, meterDataById, groupDataById, mapState) => { // Deep Copy previous selector's values, and update as needed based on current ChartType(s) const compatibleMeters = new Set(Array.from(areaCompat.compatibleMeters)); const incompatibleMeters = new Set(Array.from(areaCompat.incompatibleMeters)); @@ -280,7 +280,7 @@ export const selectMeterGroupSelectData = createSelector( selectGroupDataById, selectSelectedMeters, selectSelectedGroups, - (chartTypeCompatibility, { data: meterDataById = {} }, { data: groupDataById = {} }, selectedMeters, selectedGroups) => { + (chartTypeCompatibility, meterDataById, groupDataById, selectedMeters, selectedGroups) => { // Destructure Previous Selectors's values const { compatibleMeters, incompatibleMeters, compatibleGroups, incompatibleGroups } = chartTypeCompatibility; @@ -337,7 +337,7 @@ export const selectMeterGroupSelectData = createSelector( export const selectVisibleUnitOrSuffixState = createSelector( selectUnitDataById, selectCurrentUser, - ({ data: unitDataById }, currentUser) => { + (unitDataById, currentUser) => { let visibleUnitsOrSuffixes; if (currentUser.profile?.role === 'admin') { // User is an admin, allow all units to be seen @@ -361,7 +361,7 @@ export const selectUnitSelectData = createSelector( selectSelectedMeters, selectSelectedGroups, selectGraphAreaNormalization, - ({ data: unitDataById = {} }, visibleUnitsOrSuffixes, selectedMeters, selectedGroups, areaNormalization) => { + (unitDataById, visibleUnitsOrSuffixes, selectedMeters, selectedGroups, areaNormalization) => { // Holds all units that are compatible with selected meters/groups const compatibleUnits = new Set(); // Holds all units that are not compatible with selected meters/groups diff --git a/src/client/app/types/redux/meters.ts b/src/client/app/types/redux/meters.ts index 055ab907f..686fc31d3 100644 --- a/src/client/app/types/redux/meters.ts +++ b/src/client/app/types/redux/meters.ts @@ -5,53 +5,6 @@ import { GPSPoint } from 'utils/calibration'; // import { ActionType } from './actions'; import { AreaUnitType } from 'utils/getAreaUnitConversion'; -// export interface RequestMetersDetailsAction { -// type: ActionType.RequestMetersDetails; -// } - -// export interface ReceiveMetersDetailsAction { -// type: ActionType.ReceiveMetersDetails; -// data: MeterData[]; -// } - -// export interface ChangeDisplayedMetersAction { -// type: ActionType.ChangeDisplayedMeters; -// selectedMeters: number[]; -// } - -// export interface ConfirmEditedMeterAction { -// type: ActionType.ConfirmEditedMeter; -// editedMeter: MeterEditData; -// } - -// export interface ConfirmAddMeterAction { -// type: ActionType.ConfirmAddMeter; -// addedMeter: MeterEditData; -// } - -// export interface DeleteSubmittedMeterAction { -// type: ActionType.DeleteSubmittedMeter; -// meterId: number; -// } - -// export interface SubmitEditedMeterAction { -// type: ActionType.SubmitEditedMeter; -// meterId: number; -// } - -// export interface ConfirmMetersFetchedOnceAction { -// type: ActionType.ConfirmMetersFetchedOnce; -// } - -// export type MetersAction = RequestMetersDetailsAction -// | ReceiveMetersDetailsAction -// | ChangeDisplayedMetersAction -// | ConfirmEditedMeterAction -// | ConfirmAddMeterAction -// | DeleteSubmittedMeterAction -// | SubmitEditedMeterAction -// | ConfirmMetersFetchedOnceAction; - // The relates to the JS object Meter.types for the same use in src/server/models/Meter.js. // They should be kept in sync. export enum MeterType { diff --git a/src/client/app/utils/determineCompatibleUnits.ts b/src/client/app/utils/determineCompatibleUnits.ts index 88abeef3f..32f8d0eb2 100644 --- a/src/client/app/utils/determineCompatibleUnits.ts +++ b/src/client/app/utils/determineCompatibleUnits.ts @@ -4,7 +4,7 @@ import * as _ from 'lodash'; import React from 'react'; -import { selectPIK } from '../redux/api/conversionsApi'; +import { selectPik } from '../redux/api/conversionsApi'; import { selectGroupDataById } from '../redux/api/groupsApi'; import { selectMeterDataById } from '../redux/api/metersApi'; import { selectUnitDataById } from '../redux/api/unitsApi'; @@ -31,7 +31,7 @@ export function setIntersect(setA: Set, setB: Set): Set * @returns Set of compatible unit ids. */ export function unitsCompatibleWithMeters(meters: Set): Set { - const { data: meterDataByID = {} } = selectMeterDataById(store.getState()) + const meterDataByID = selectMeterDataById(store.getState()) // The first meter processed is different since intersection with empty set is empty. let first = true; @@ -74,7 +74,7 @@ export function unitsCompatibleWithUnit(unitId: number): Set { // If unit was null in the database then -99. This means there is no unit // so nothing is compatible with it. Skip processing and return empty set at end. // Do same if pik is not yet available. - const { data: pik } = selectPIK(store.getState()) + const pik = selectPik(store.getState()) if (unitId != -99 && pik) { // Get the row index in Pik of this unit. const row = pRowFromUnit(unitId); @@ -97,7 +97,7 @@ export function unitsCompatibleWithUnit(unitId: number): Set { * @returns The row index in Pik for given meter unit. */ export function pRowFromUnit(unitId: number): number { - const { data: unitDataById = {} } = selectUnitDataById(store.getState()) + const unitDataById = selectUnitDataById(store.getState()) const unit = _.find(unitDataById, function (o: UnitData) { // Since this is the row index, type of unit must be meter. @@ -112,7 +112,7 @@ export function pRowFromUnit(unitId: number): number { * @returns The unit id given the row in Pik units. */ export function unitFromPRow(row: number): number { - const { data: unitDataById = {} } = selectUnitDataById(store.getState()) + const unitDataById = selectUnitDataById(store.getState()) const unit = _.find(unitDataById, function (o: UnitData) { // Since the given unitIndex is a row index, the unit type must be meter. @@ -127,7 +127,7 @@ export function unitFromPRow(row: number): number { * @returns The unit id given the column in Pik. */ export function unitFromPColumn(column: number): number { - const { data: unitDataById = {} } = selectUnitDataById(store.getState()) + const unitDataById = selectUnitDataById(store.getState()) const unit = _.find(unitDataById, function (o: UnitData) { // Since the given unitIndex is a column index, the unit type must be different from meter. @@ -146,7 +146,7 @@ export function metersInGroup(groupId: number): Set { const state = store.getState(); // Gets the group associated with groupId. // The deep children are automatically fetched with group state so should exist. - const { data: groupDataById = {} } = selectGroupDataById(state) + const groupDataById = selectGroupDataById(state) const group = _.get(groupDataById, groupId); // Create a set of the deep meters of this group and return it. return new Set(group.deepMeters); @@ -160,7 +160,7 @@ export function metersInGroup(groupId: number): Set { */ export function metersInChangedGroup(changedGroupState: GroupData): number[] { const state = store.getState(); - const { data: groupDataById = {} } = selectGroupDataById(state) + const groupDataById = selectGroupDataById(state) // deep meters starts with all the direct child meters of the group being changed. const deepMeters = new Set(changedGroupState.childMeters); @@ -197,12 +197,12 @@ export function getMeterMenuOptionsForGroup(defaultGraphicUnit: number, deepMete // Get the units that are compatible with this set of meters. const currentUnits = unitsCompatibleWithMeters(deepMetersSet); // Get all meters' state. - const { data: meters = {} } = selectMeterDataById(state) + const meterDataById = selectMeterDataById(state) // Options for the meter menu. const options: SelectOption[] = []; // For each meter, decide its compatibility for the menu - Object.values(meters).forEach(meter => { + Object.values(meterDataById).forEach(meter => { const option = { label: meter.identifier, value: meter.id, @@ -240,12 +240,12 @@ export function getGroupMenuOptionsForGroup(groupId: number, defaultGraphicUnit: // Get the currentGroup's compatible units. const currentUnits = unitsCompatibleWithMeters(deepMetersSet); // Get all groups' state. - const { data: groups = {} } = selectGroupDataById(store.getState()); + const groupDataById = selectGroupDataById(store.getState()); // Options for the group menu. const options: SelectOption[] = []; - Object.values(groups).forEach(group => { + Object.values(groupDataById).forEach(group => { // You cannot have yourself in the group so not an option. if (group.id !== groupId) { const option = { @@ -312,7 +312,7 @@ export function getCompatibilityChangeCase(currentUnits: Set, idToAdd: n function getCompatibleUnits(id: number, type: DataType, deepMeters: number[]): Set { if (type == DataType.Meter) { const state = store.getState(); - const { data: meterDataByID = {} } = selectMeterDataById(state) + const meterDataByID = selectMeterDataById(state) // Get the unit id of meter. const unitId = meterDataByID[id].unitId; // Returns all compatible units with this unit id. From 21e16ebc5965c759bffe8e8fdcd2de5ecf189ef2 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Thu, 9 Nov 2023 02:05:12 +0000 Subject: [PATCH 039/131] Refactor Query Arg Selectors --- src/client/app/actions/graph.ts | 59 --- src/client/app/components/AppLayout.tsx | 2 +- .../app/components/BarChartComponent.tsx | 18 +- .../app/components/DashboardComponent.tsx | 12 +- src/client/app/components/ExportComponent.tsx | 4 +- .../app/components/HeaderButtonsComponent.tsx | 4 +- .../app/components/LineChartComponent.tsx | 15 +- .../app/components/MapChartComponent.tsx | 380 ++++++++++++++++++ .../MeterAndGroupSelectComponent.tsx | 4 +- .../MultiCompareChartComponentWIP.tsx | 9 +- .../ReadingsPerDaySelectComponent.tsx | 28 +- src/client/app/components/ThreeDComponent.tsx | 11 +- .../app/components/UnitSelectComponent.tsx | 4 +- src/client/app/reducers/admin.ts | 8 + src/client/app/reducers/graph.ts | 12 +- src/client/app/redux/api/readingsApi.ts | 1 - src/client/app/redux/componentHooks.ts | 6 +- .../app/redux/selectors/dataSelectors.ts | 259 +++++++----- .../app/redux/selectors/threeDSelectors.ts | 32 +- 19 files changed, 601 insertions(+), 267 deletions(-) create mode 100644 src/client/app/components/MapChartComponent.tsx diff --git a/src/client/app/actions/graph.ts b/src/client/app/actions/graph.ts index 4328316d0..641c590c3 100644 --- a/src/client/app/actions/graph.ts +++ b/src/client/app/actions/graph.ts @@ -4,67 +4,8 @@ import * as moment from 'moment'; import { TimeInterval } from '../../../common/TimeInterval'; -import { graphSlice } from '../reducers/graph'; -import { Dispatch, GetState, Thunk } from '../types/redux/actions'; import * as t from '../types/redux/graph'; import { ComparePeriod, SortingOrder } from '../utils/calculateCompare'; -import { fetchNeededMapReadings } from './mapReadings'; - -export function setHotlinkedAsync(hotlinked: boolean): Thunk { - return (dispatch: Dispatch) => { - dispatch(graphSlice.actions.setHotlinked(hotlinked)); - return Promise.resolve(); - }; -} - -export function toggleOptionsVisibility() { - return graphSlice.actions.toggleOptionsVisibility(); -} - -export function changeCompareSortingOrder(compareSortingOrder: SortingOrder) { - return graphSlice.actions.changeCompareSortingOrder(compareSortingOrder); -} - -export function changeSelectedMeters(meterIDs: number[]): Thunk { - return (dispatch: Dispatch, getState: GetState) => { - dispatch(graphSlice.actions.updateSelectedMeters(meterIDs)); - // Nesting dispatches to preserve that updateSelectedMeters() is called before fetching readings - dispatch((dispatch2: Dispatch) => { - dispatch2(fetchNeededMapReadings(getState().graph.queryTimeInterval, getState().graph.selectedUnit)); - }); - return Promise.resolve(); - }; -} - -export function changeSelectedGroups(groupIDs: number[]): Thunk { - return (dispatch: Dispatch, getState: GetState) => { - dispatch(graphSlice.actions.updateSelectedGroups(groupIDs)); - // Nesting dispatches to preserve that updateSelectedGroups() is called before fetching readings - dispatch((dispatch2: Dispatch) => { - dispatch2(fetchNeededMapReadings(getState().graph.queryTimeInterval, getState().graph.selectedUnit)); - }); - return Promise.resolve(); - }; -} - -export function updateThreeDReadingInterval(readingInterval: t.ReadingInterval): Thunk { - return (dispatch: Dispatch) => { - dispatch(graphSlice.actions.updateThreeDReadingInterval(readingInterval)); - return Promise.resolve(); - }; -} - -export function updateThreeDMeterOrGroupInfo(meterOrGroupID: t.MeterOrGroupID | undefined, meterOrGroup: t.MeterOrGroup) { - return graphSlice.actions.updateThreeDMeterOrGroupInfo({ meterOrGroupID, meterOrGroup }); -} - -export function changeMeterOrGroupInfo(meterOrGroupID: t.MeterOrGroupID | undefined, meterOrGroup: t.MeterOrGroup = t.MeterOrGroup.meters): Thunk { - // Meter ID can be null, however meterOrGroup defaults to meters a null check on ID can be sufficient - return (dispatch: Dispatch) => { - dispatch(updateThreeDMeterOrGroupInfo(meterOrGroupID, meterOrGroup)); - return Promise.resolve(); - }; -} export interface LinkOptions { meterIDs?: number[]; diff --git a/src/client/app/components/AppLayout.tsx b/src/client/app/components/AppLayout.tsx index 07ed7aa20..e79aa27f7 100644 --- a/src/client/app/components/AppLayout.tsx +++ b/src/client/app/components/AppLayout.tsx @@ -5,7 +5,7 @@ import 'react-toastify/dist/ReactToastify.css' import FooterComponent from './FooterComponent' import HeaderComponent from './HeaderComponent' /** - * @returns The OED Application Layout, header, and footer, with the current route as the outlet. + * @returns The OED Application Layout. The current route as the outlet Wrapped in the header, and footer components */ export default function AppLayout() { return ( diff --git a/src/client/app/components/BarChartComponent.tsx b/src/client/app/components/BarChartComponent.tsx index 0dfa4624c..ec7a82e21 100644 --- a/src/client/app/components/BarChartComponent.tsx +++ b/src/client/app/components/BarChartComponent.tsx @@ -9,10 +9,12 @@ import * as React from 'react'; import Plot from 'react-plotly.js'; import { TimeInterval } from '../../../common/TimeInterval'; import { graphSlice, selectSelectedGroups, selectSelectedMeters } from '../reducers/graph'; +import { selectGroupDataById } from '../redux/api/groupsApi'; import { selectMeterDataById } from '../redux/api/metersApi'; import { readingsApi } from '../redux/api/readingsApi'; +import { selectUnitDataById } from '../redux/api/unitsApi'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; -import { BarReadingApiArgs, ChartMultiQueryProps } from '../redux/selectors/dataSelectors'; +import { selectBarChartQueryArgs } from '../redux/selectors/dataSelectors'; import { DataType } from '../types/Datasources'; import { UnitRepresentType } from '../types/redux/units'; import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; @@ -20,19 +22,19 @@ import getGraphColor from '../utils/getGraphColor'; import { barUnitLabel } from '../utils/graphics'; import translate from '../utils/translate'; import SpinnerComponent from './SpinnerComponent'; -import { selectGroupDataById } from '../redux/api/groupsApi'; -import { selectUnitDataById } from '../redux/api/unitsApi'; /** * Passes the current redux state of the barchart, and turns it into props for the React * component, which is what will be visible on the page. Makes it possible to access * your reducer state objects from within your React components. - * @param props query arguments to be used in the dataFetching Hooks. * @returns Plotly BarChart */ -export default function BarChartComponent(props: ChartMultiQueryProps) { - const { meterArgs, groupsArgs, meterSkipQuery, groupSkipQuery } = props.queryArgs; +export default function BarChartComponent() { const dispatch = useAppDispatch(); + const { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } = useAppSelector(selectBarChartQueryArgs) + const { data: meterReadings, isLoading: meterIsFetching } = readingsApi.useBarQuery(meterArgs, { skip: meterShouldSkip }); + const { data: groupData, isLoading: groupIsFetching } = readingsApi.useBarQuery(groupArgs, { skip: groupShouldSkip }); + const barDuration = useAppSelector(state => state.graph.barDuration); const barStacking = useAppSelector(state => state.graph.barStacking); const unitID = useAppSelector(state => state.graph.selectedUnit); @@ -44,12 +46,10 @@ export default function BarChartComponent(props: ChartMultiQueryProps state.graph.selectedAreaUnit); const selectedMeters = useAppSelector(selectSelectedMeters); const selectedGroups = useAppSelector(selectSelectedGroups); - const meterDataByID = useAppSelector(selectMeterDataById); + const meterDataByID = useAppSelector(selectMeterDataById); const groupDataById = useAppSelector(selectGroupDataById); // useQueryHooks for data fetching - const { data: meterReadings, isLoading: meterIsFetching } = readingsApi.useBarQuery(meterArgs, { skip: meterSkipQuery }); - const { data: groupData, isLoading: groupIsFetching } = readingsApi.useBarQuery(groupsArgs, { skip: groupSkipQuery }); const datasets = []; if (meterIsFetching || groupIsFetching) { diff --git a/src/client/app/components/DashboardComponent.tsx b/src/client/app/components/DashboardComponent.tsx index 881b62988..b33d76397 100644 --- a/src/client/app/components/DashboardComponent.tsx +++ b/src/client/app/components/DashboardComponent.tsx @@ -3,13 +3,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import MapChartContainer from '../containers/MapChartContainer'; import { useAppSelector } from '../redux/hooks'; -import { selectChartQueryArgs } from '../redux/selectors/dataSelectors'; import { ChartTypes } from '../types/redux/graph'; import BarChartComponent from './BarChartComponent'; import HistoryComponent from './HistoryComponent'; import LineChartComponent from './LineChartComponent'; +import MapChartComponent from './MapChartComponent'; import MultiCompareChartComponentWIP from './MultiCompareChartComponentWIP'; import ThreeDComponent from './ThreeDComponent'; import UIOptionsComponent from './UIOptionsComponent'; @@ -21,7 +20,6 @@ import UIOptionsComponent from './UIOptionsComponent'; export default function DashboardComponent() { const chartToRender = useAppSelector(state => state.graph.chartToRender); const optionsVisibility = useAppSelector(state => state.graph.optionsVisibility); - const queryArgs = useAppSelector(state => selectChartQueryArgs(state)) const optionsClassName = optionsVisibility ? 'col-2 d-none d-lg-block' : 'd-none'; const chartClassName = optionsVisibility ? 'col-12 col-lg-10' : 'col-12'; @@ -38,11 +36,11 @@ export default function DashboardComponent() {
- {chartToRender === ChartTypes.line && } - {chartToRender === ChartTypes.bar && } + {chartToRender === ChartTypes.line && } + {chartToRender === ChartTypes.bar && } {chartToRender === ChartTypes.compare && } - {chartToRender === ChartTypes.map && } - {chartToRender === ChartTypes.threeD && } + {chartToRender === ChartTypes.map && } + {chartToRender === ChartTypes.threeD && }
diff --git a/src/client/app/components/ExportComponent.tsx b/src/client/app/components/ExportComponent.tsx index 80424ad84..c179aefe6 100644 --- a/src/client/app/components/ExportComponent.tsx +++ b/src/client/app/components/ExportComponent.tsx @@ -49,9 +49,9 @@ export default function ExportComponent() { const queryArgs = useAppSelector(selectChartQueryArgs) const { data: lineMeterReadings = {}, isFetching: lineMeterIsFetching } = readingsApi.endpoints.line.useQueryState(queryArgs.line.meterArgs); - const { data: lineGroupReadings = {}, isFetching: groupMeterIsFetching } = readingsApi.endpoints.line.useQueryState(queryArgs.line.groupsArgs); + const { data: lineGroupReadings = {}, isFetching: groupMeterIsFetching } = readingsApi.endpoints.line.useQueryState(queryArgs.line.groupArgs); const { data: barMeterReadings = {}, isFetching: barMeterIsFetching } = readingsApi.endpoints.line.useQueryState(queryArgs.bar.meterArgs); - const { data: barGroupReadings = {}, isFetching: barGroupIsFetching } = readingsApi.endpoints.line.useQueryState(queryArgs.bar.groupsArgs); + const { data: barGroupReadings = {}, isFetching: barGroupIsFetching } = readingsApi.endpoints.line.useQueryState(queryArgs.bar.groupArgs); // Function to export the data in a graph. const exportGraphReading = () => { diff --git a/src/client/app/components/HeaderButtonsComponent.tsx b/src/client/app/components/HeaderButtonsComponent.tsx index c218b80cd..6d7f9b180 100644 --- a/src/client/app/components/HeaderButtonsComponent.tsx +++ b/src/client/app/components/HeaderButtonsComponent.tsx @@ -7,11 +7,11 @@ import { useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { Link, useLocation } from 'react-router-dom-v5-compat'; import { DropdownItem, DropdownMenu, DropdownToggle, Nav, NavLink, Navbar, UncontrolledDropdown } from 'reactstrap'; -import { selectOEDVersion } from '../redux/api/versionApi'; -import { toggleOptionsVisibility } from '../actions/graph'; import TooltipHelpComponent from '../components/TooltipHelpComponent'; +import { toggleOptionsVisibility } from '../reducers/graph'; import { unsavedWarningSlice } from '../reducers/unsavedWarning'; import { authApi } from '../redux/api/authApi'; +import { selectOEDVersion } from '../redux/api/versionApi'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { UserRole } from '../types/items'; import { hasPermissions, isRoleAdmin } from '../utils/hasPermissions'; diff --git a/src/client/app/components/LineChartComponent.tsx b/src/client/app/components/LineChartComponent.tsx index eb33627cd..a883faeb8 100644 --- a/src/client/app/components/LineChartComponent.tsx +++ b/src/client/app/components/LineChartComponent.tsx @@ -15,23 +15,24 @@ import { import { selectGroupDataById } from '../redux/api/groupsApi'; import { selectMeterDataById } from '../redux/api/metersApi'; import { readingsApi } from '../redux/api/readingsApi'; +import { selectUnitDataById } from '../redux/api/unitsApi'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; -import { ChartMultiQueryProps, LineReadingApiArgs } from '../redux/selectors/dataSelectors'; +import { selectLineChartQueryArgs } from '../redux/selectors/dataSelectors'; import { DataType } from '../types/Datasources'; import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; import getGraphColor from '../utils/getGraphColor'; import { lineUnitLabel } from '../utils/graphics'; import translate from '../utils/translate'; import LogoSpinner from './LogoSpinner'; -import { selectUnitDataById } from '../redux/api/unitsApi'; /** - * @param props qpi query * @returns plotlyLine graphic */ -export default function LineChartComponent(props: ChartMultiQueryProps) { - const { meterArgs, groupsArgs, meterSkipQuery, groupSkipQuery } = props.queryArgs; +export default function LineChartComponent() { const dispatch = useAppDispatch(); + const { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } = useAppSelector(selectLineChartQueryArgs) + const { data: meterReadings, isLoading: meterIsLoading } = readingsApi.useLineQuery(meterArgs, { skip: meterShouldSkip }); + const { data: groupData, isLoading: groupIsLoading } = readingsApi.useLineQuery(groupArgs, { skip: groupShouldSkip }); const selectedUnit = useAppSelector(state => state.graph.selectedUnit); // The unit label depends on the unit which is in selectUnit state. @@ -43,13 +44,11 @@ export default function LineChartComponent(props: ChartMultiQueryProps state.graph.selectedUnit); + 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 barDuration = useSelector((state: State) => state.graph.barDuration) + const areaNormalization = useSelector((state: State) => state.graph.areaNormalization) + const selectedAreaUnit = useSelector((state: State) => state.graph.selectedAreaUnit) + const selectedMeters = useSelector((state: State) => state.graph.selectedMeters) + const selectedGroups = useSelector((state: State) => state.graph.selectedGroups) + + const unitDataById = useAppSelector(selectUnitDataById) + const groupDataById = useAppSelector(selectGroupDataById) + const meterDataById = useAppSelector(selectMeterDataById) + if (meterIsFetching || groupIsFetching) { + return + } + + + // 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. + 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'; + } + if (areaNormalization) { + unitLabel += ' / ' + translate(`AreaUnitType.${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.'); + } + // 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 hover text. + hoverText.push(` ${timeReading}
${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.'); + } + // 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 hover text. + hoverText.push(` ${timeReading}
${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 : 'There\'s not an available map' + }, + 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: (image) ? image.src : '', + xref: 'x', + yref: 'y', + x: 0, + y: 0, + sizex: 500, + sizey: 500, + xanchor: 'left', + yanchor: 'bottom', + sizing: 'contain', + opacity: 1 + }] + }; + + /*** + * Usage: + * console.log(points, event)}> + */ + return ( + + ); +} \ No newline at end of file diff --git a/src/client/app/components/MeterAndGroupSelectComponent.tsx b/src/client/app/components/MeterAndGroupSelectComponent.tsx index 6e6cdb0fe..b58a3ce57 100644 --- a/src/client/app/components/MeterAndGroupSelectComponent.tsx +++ b/src/client/app/components/MeterAndGroupSelectComponent.tsx @@ -9,7 +9,7 @@ import { Badge } from 'reactstrap'; import { graphSlice } from '../reducers/graph'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { selectMeterGroupSelectData } from '../redux/selectors/uiSelectors'; -import { getFetchingStates } from '../redux/componentHooks'; +import { useFetchingStates } from '../redux/componentHooks'; import { GroupedOption, SelectOption } from '../types/items'; import { MeterOrGroup } from '../types/redux/graph'; import translate from '../utils/translate'; @@ -24,7 +24,7 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; export default function MeterAndGroupSelectComponent(props: MeterAndGroupSelectProps) { const dispatch = useAppDispatch(); const meterAndGroupSelectOptions = useAppSelector(selectMeterGroupSelectData); - const { somethingIsFetching } = getFetchingStates(); + const { somethingIsFetching } = useFetchingStates(); const { meterOrGroup } = props; // Set the current component's appropriate meter or group update from the graphSlice's Payload-Action Creator diff --git a/src/client/app/components/MultiCompareChartComponentWIP.tsx b/src/client/app/components/MultiCompareChartComponentWIP.tsx index 6bef12081..6b57969a7 100644 --- a/src/client/app/components/MultiCompareChartComponentWIP.tsx +++ b/src/client/app/components/MultiCompareChartComponentWIP.tsx @@ -11,7 +11,7 @@ import { selectGroupDataById } from '../redux/api/groupsApi'; import { selectMeterDataById } from '../redux/api/metersApi'; import { readingsApi } from '../redux/api/readingsApi'; import { useAppSelector } from '../redux/hooks'; -import { selectChartQueryArgs } from '../redux/selectors/dataSelectors'; +import { selectCompareChartQueryArgs } from '../redux/selectors/dataSelectors'; import { SortingOrder } from '../utils/calculateCompare'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; @@ -25,13 +25,14 @@ export interface MultiCompareChartProps { * @returns Multi Compare Chart element */ export default function MultiCompareChartComponentWIP() { + const { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } = useAppSelector(selectCompareChartQueryArgs) + const { data: meterReadings = {} } = readingsApi.useCompareQuery(meterArgs, { skip: meterShouldSkip }) + const { data: groupReadings = {} } = readingsApi.useCompareQuery(groupArgs, { skip: groupShouldSkip }) + const areaNormalization = useAppSelector(selectGraphAreaNormalization) const sortingOrder = useAppSelector(selectSortingOrder) const selectedMeters = useAppSelector(selectSelectedMeters) const selectedGroups = useAppSelector(selectSelectedGroups) - const { compare: { meterArgs, meterSkipQuery, groupSkipQuery, groupsArgs } } = useAppSelector(selectChartQueryArgs) - const { data: meterReadings = {} } = readingsApi.useCompareQuery(meterArgs, { skip: meterSkipQuery }) - const { data: groupReadings = {} } = readingsApi.useCompareQuery(groupsArgs, { skip: groupSkipQuery }) const meterDataByID = useAppSelector(selectMeterDataById) const groupDataById = useAppSelector(selectGroupDataById) diff --git a/src/client/app/components/ReadingsPerDaySelectComponent.tsx b/src/client/app/components/ReadingsPerDaySelectComponent.tsx index f8d5b2387..16372c6d2 100644 --- a/src/client/app/components/ReadingsPerDaySelectComponent.tsx +++ b/src/client/app/components/ReadingsPerDaySelectComponent.tsx @@ -2,34 +2,28 @@ * 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 Select from 'react-select'; -import { State } from '../types/redux/state'; -import { useDispatch, useSelector } from 'react-redux'; -import { useAppSelector } from '../redux/hooks'; +import { updateThreeDReadingInterval } from '../reducers/graph'; +import { readingsApi } from '../redux/api/readingsApi'; +import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { ChartTypes, ReadingInterval } from '../types/redux/graph'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; -import { ChartTypes, ReadingInterval } from '../types/redux/graph'; -import { Dispatch } from '../types/redux/actions'; -import { updateThreeDReadingInterval } from '../actions/graph'; -import { selectThreeDQueryArgs, selectThreeDSkip } from '../redux/selectors/threeDSelectors' -import { readingsApi } from '../redux/api/readingsApi' - - -import * as moment from 'moment'; +import { selectThreeDQueryArgs } from '../redux/selectors/dataSelectors'; /** * A component which allows users to select date ranges for the graphic * @returns A Select menu with Readings per day options. */ export default function ReadingsPerDaySelect() { - const dispatch: Dispatch = useDispatch(); - const graphState = useSelector((state: State) => state.graph); - const readingInterval = useSelector((state: State) => state.graph.threeD.readingInterval); - const queryArgs = useAppSelector(selectThreeDQueryArgs); - const shouldSkip = useAppSelector(selectThreeDSkip); + const dispatch = useAppDispatch(); + const graphState = useAppSelector(state => state.graph); + const readingInterval = useAppSelector(state => state.graph.threeD.readingInterval); + const { args, shouldSkipQuery } = useAppSelector(selectThreeDQueryArgs); - const { data, isFetching } = readingsApi.endpoints.threeD.useQuery(queryArgs, { skip: shouldSkip }); + const { data, isFetching } = readingsApi.endpoints.threeD.useQuery(args, { skip: shouldSkipQuery }); let actualReadingInterval = ReadingInterval.Hourly if (data && data.zData.length) { diff --git a/src/client/app/components/ThreeDComponent.tsx b/src/client/app/components/ThreeDComponent.tsx index fae59a88a..29716802e 100644 --- a/src/client/app/components/ThreeDComponent.tsx +++ b/src/client/app/components/ThreeDComponent.tsx @@ -11,7 +11,7 @@ import { selectMeterDataById } from '../redux/api/metersApi'; import { readingsApi } from '../redux/api/readingsApi'; import { selectUnitDataById } from '../redux/api/unitsApi'; import { useAppSelector } from '../redux/hooks'; -import { ChartSingleQueryProps, ThreeDReadingApiArgs } from '../redux/selectors/dataSelectors'; +import { selectThreeDQueryArgs } from '../redux/selectors/dataSelectors'; import { selectThreeDComponentInfo } from '../redux/selectors/threeDSelectors'; import { ThreeDReading } from '../types/readings'; import { GraphState, MeterOrGroup } from '../types/redux/graph'; @@ -27,13 +27,12 @@ import ThreeDPillComponent from './ThreeDPillComponent'; /** * Component used to render 3D graphics - * @param props query args for the useQueryDataFetching hooks * @returns 3D Plotly 3D Surface Graph */ -export default function ThreeDComponent(props: ChartSingleQueryProps) { - const { args, skipQuery } = props.queryArgs; - const { data, isFetching } = readingsApi.endpoints.threeD.useQuery(args, { skip: skipQuery }); - const meterDataById = useAppSelector(selectMeterDataById); +export default function ThreeDComponent() { + const { args, shouldSkipQuery } = useAppSelector(selectThreeDQueryArgs); + const { data, isFetching } = readingsApi.endpoints.threeD.useQuery(args, { skip: shouldSkipQuery }); + const meterDataById = useAppSelector(selectMeterDataById); const groupDataById = useAppSelector(selectGroupDataById); const unitDataById = useAppSelector(selectUnitDataById); const graphState = useAppSelector(selectGraphState); diff --git a/src/client/app/components/UnitSelectComponent.tsx b/src/client/app/components/UnitSelectComponent.tsx index d7b11555b..653056e44 100644 --- a/src/client/app/components/UnitSelectComponent.tsx +++ b/src/client/app/components/UnitSelectComponent.tsx @@ -13,7 +13,7 @@ import { Badge } from 'reactstrap'; import { graphSlice } from '../reducers/graph'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; -import { getFetchingStates } from '../redux/componentHooks'; +import { useFetchingStates } from '../redux/componentHooks'; import { selectUnitDataById } from '../redux/api/unitsApi'; @@ -26,7 +26,7 @@ export default function UnitSelectComponent() { const selectedUnitID = useAppSelector(state => state.graph.selectedUnit); const unitsByID = useAppSelector(selectUnitDataById); - const { endpointsFetchingData } = getFetchingStates(); + const { endpointsFetchingData } = useFetchingStates(); let selectedUnitOption: SelectOption | null = null; diff --git a/src/client/app/reducers/admin.ts b/src/client/app/reducers/admin.ts index 67c5b87d7..232331ab1 100644 --- a/src/client/app/reducers/admin.ts +++ b/src/client/app/reducers/admin.ts @@ -10,6 +10,7 @@ import { ChartTypes } from '../types/redux/graph'; import { LanguageTypes } from '../types/redux/i18n'; import { durationFormat } from '../utils/durationFormat'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; +import { preferencesApi } from '../redux/api/preferencesApi'; const defaultState: AdminState = { selectedMeter: null, @@ -132,6 +133,13 @@ export const adminSlice = createSlice({ state.submitted = false; } }, + extraReducers: builder => { + builder.addMatcher(preferencesApi.endpoints.getPreferences.matchFulfilled, (state, action) => ({ + ...state, + ...action.payload, + defaultMeterReadingFrequency: durationFormat(action.payload.defaultMeterReadingFrequency) + })) + }, selectors: { selectAdminState: state => state } diff --git a/src/client/app/reducers/graph.ts b/src/client/app/reducers/graph.ts index 45946cdef..6adf63584 100644 --- a/src/client/app/reducers/graph.ts +++ b/src/client/app/reducers/graph.ts @@ -253,9 +253,9 @@ export const graphSlice = createSlice({ }, // New Feature as of 2.0.0 Beta. selectors: { + selectGraphState: state => state, selectThreeDState: state => state.threeD, selectBarWidthDays: state => state.barDuration, - selectGraphState: state => state, selectSelectedMeters: state => state.selectedMeters, selectSelectedGroups: state => state.selectedGroups, selectQueryTimeInterval: state => state.queryTimeInterval, @@ -267,7 +267,10 @@ export const graphSlice = createSlice({ selectThreeDReadingInterval: state => state.threeD.readingInterval, selectLineGraphRate: state => state.lineGraphRate, selectAreaUnit: state => state.selectedAreaUnit, - selectSortingOrder: state => state.compareSortingOrder + selectSortingOrder: state => state.compareSortingOrder, + selectSelectedUnit: state => state.selectedUnit, + selectComparePeriod: state => state.comparePeriod, + selectCompareTimeInterval: state => state.compareTimeInterval } }) @@ -287,7 +290,10 @@ export const { selectThreeDReadingInterval, selectLineGraphRate, selectAreaUnit, - selectSortingOrder + selectSortingOrder, + selectSelectedUnit, + selectComparePeriod, + selectCompareTimeInterval } = graphSlice.selectors // actionCreators exports diff --git a/src/client/app/redux/api/readingsApi.ts b/src/client/app/redux/api/readingsApi.ts index 4ffb55df3..ad938e28e 100644 --- a/src/client/app/redux/api/readingsApi.ts +++ b/src/client/app/redux/api/readingsApi.ts @@ -142,7 +142,6 @@ export const readingsApi = baseApi.injectEndpoints({ const { data, error } = await baseQuery(URL) return error ? { error } : { data: data as CompareReadings } } - // } }) }) diff --git a/src/client/app/redux/componentHooks.ts b/src/client/app/redux/componentHooks.ts index c6ce4ffdb..4d0d956eb 100644 --- a/src/client/app/redux/componentHooks.ts +++ b/src/client/app/redux/componentHooks.ts @@ -7,12 +7,12 @@ import { selectChartQueryArgs } from './selectors/dataSelectors'; import { unitsApi } from './api/unitsApi'; // General purpose custom hook mostly useful for Select component loadingIndicators, and current graph loading state(s) -export const getFetchingStates = () => { +export const useFetchingStates = () => { const queryArgs = useAppSelector(state => selectChartQueryArgs(state)); const { isFetching: meterLineIsFetching, isLoading: meterLineIsLoading } = readingsApi.endpoints.line.useQueryState(queryArgs.line.meterArgs); - const { isFetching: groupLineIsFetching, isLoading: groupLineIsLoading } = readingsApi.endpoints.line.useQueryState(queryArgs.line.groupsArgs); + const { isFetching: groupLineIsFetching, isLoading: groupLineIsLoading } = readingsApi.endpoints.line.useQueryState(queryArgs.line.groupArgs); const { isFetching: meterBarIsFetching, isLoading: meterBarIsLoading } = readingsApi.endpoints.bar.useQueryState(queryArgs.bar.meterArgs); - const { isFetching: groupBarIsFetching, isLoading: groupBarIsLoading } = readingsApi.endpoints.bar.useQueryState(queryArgs.bar.groupsArgs); + const { isFetching: groupBarIsFetching, isLoading: groupBarIsLoading } = readingsApi.endpoints.bar.useQueryState(queryArgs.bar.groupArgs); const { isFetching: threeDIsFetching, isLoading: threeDIsLoading } = readingsApi.endpoints.threeD.useQueryState(queryArgs.threeD.args); const { isFetching: metersFetching, isLoading: metersLoading } = metersApi.endpoints.getMeters.useQueryState(); const { isFetching: groupsFetching, isLoading: groupsLoading } = groupsApi.endpoints.getGroups.useQueryState(); diff --git a/src/client/app/redux/selectors/dataSelectors.ts b/src/client/app/redux/selectors/dataSelectors.ts index 1ac2204f2..5c56186b9 100644 --- a/src/client/app/redux/selectors/dataSelectors.ts +++ b/src/client/app/redux/selectors/dataSelectors.ts @@ -1,26 +1,35 @@ import { createSelector } from '@reduxjs/toolkit'; import * as _ from 'lodash'; -import { selectGraphState } from '../../reducers/graph'; -import { selectGroupDataById } from '../api/groupsApi'; -import { selectMeterDataById } from '../api/metersApi'; -import { readingsApi } from '../api/readingsApi'; +import * as moment from 'moment'; +import { TimeInterval } from '../../../../common/TimeInterval'; +import { selectBarWidthDays, selectComparePeriod, selectCompareTimeInterval, selectGraphState, selectQueryTimeInterval } from '../../reducers/graph'; import { MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; +import { calculateCompareShift } from '../../utils/calculateCompare'; import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; +import { selectGroupDataById } from '../api/groupsApi'; +import { selectMeterDataById } from '../api/metersApi'; import { selectIsLoggedInAsAdmin } from './authSelectors'; -import { calculateCompareShift } from '../../utils/calculateCompare'; +import { RootState } from 'store'; -// Props that are passed to plotly components -export interface ChartMultiQueryProps { - queryArgs: ChartMultiQueryArgs -} +// TODO DUPLICATE SELECTOR? UI SELECTOR MAY CONTAIN SAME LOGIC, CONSOLIDATE IF POSSIBLE? +export const selectVisibleMetersGroupsDataByID = createSelector( + selectMeterDataById, + selectGroupDataById, + selectIsLoggedInAsAdmin, + (meterDataById, groupDataById, isAdmin) => { + let visibleMeters; + let visibleGroups; + if (isAdmin) { + visibleMeters = meterDataById + visibleGroups = groupDataById; + } else { + visibleMeters = _.filter(meterDataById, meter => meter.displayable); + visibleGroups = _.filter(groupDataById, group => group.displayable); + } -export interface ChartMultiQueryArgs { - meterArgs: T - groupsArgs: T - meterSkipQuery: boolean - groupSkipQuery: boolean - meta: ChartQueryArgsMeta -} + return { visibleMeters, visibleGroups } + } +) // query args that 'most' graphs share export interface commonArgsMultiID { @@ -34,6 +43,7 @@ export interface commonArgsSingleID extends Omit { id: // endpoint specific args export interface LineReadingApiArgs extends commonArgsMultiID { } export interface BarReadingApiArgs extends commonArgsMultiID { barWidthDays: number } + export interface ThreeDReadingApiArgs extends commonArgsSingleID { readingInterval: ReadingInterval } export interface CompareReadingApiArgs extends Omit { // compare breaks the timeInterval pattern query pattern therefore omit and add required for api. @@ -41,124 +51,151 @@ export interface CompareReadingApiArgs extends Omit { - queryArgs: ChartQuerySingleArgs -} +// Maps uses the Bar Endpoint so just use its args for simplicity, however barWidthDays should be durationDays +export interface MapReadingApiArgs extends BarReadingApiArgs { } -export interface ChartQuerySingleArgs { - args: T; - skipQuery: boolean; - meta: ChartQueryArgsMeta -} -export interface ChartQueryArgsMeta { - endpoint: string; -} -// Selector prepares the query args for ALL graph endpoints based on the current graph slice state -// TODO Break down into individual selectors? Verify if prop drilling is a better pattern vs useSelector in same sameComponent -export const selectChartQueryArgs = createSelector( +export const selectCommonQueryArgs = createSelector( selectGraphState, graphState => { - // args that all meters queries share - const baseMeterArgs: commonArgsMultiID = { + const queryTimeInterval = graphState.queryTimeInterval + // args that 'most' meters queries share + const commonMeterArgs: commonArgsMultiID = { ids: graphState.selectedMeters, - timeInterval: graphState.queryTimeInterval.toString(), + timeInterval: queryTimeInterval.toString(), unitID: graphState.selectedUnit, meterOrGroup: MeterOrGroup.meters } - // args that all groups queries share - const baseGroupArgs: commonArgsMultiID = { + // args that 'most' groups queries share + const commonGroupArgs: commonArgsMultiID = { ids: graphState.selectedGroups, - timeInterval: graphState.queryTimeInterval.toString(), + timeInterval: queryTimeInterval.toString(), unitID: graphState.selectedUnit, meterOrGroup: MeterOrGroup.groups } + return { commonMeterArgs, commonGroupArgs } + } +) + +export const selectLineChartQueryArgs = createSelector( + selectCommonQueryArgs, + ({ commonMeterArgs, commonGroupArgs }) => { // props to pass into the line chart component - const line: ChartMultiQueryArgs = { - meterArgs: baseMeterArgs, - groupsArgs: baseGroupArgs, - meterSkipQuery: !baseMeterArgs.ids.length, - groupSkipQuery: !baseGroupArgs.ids.length, - meta: { - endpoint: readingsApi.endpoints.line.name - } - } - // props to pass into the bar chart component - const bar: ChartMultiQueryArgs = { - meterArgs: { - ...baseMeterArgs, - barWidthDays: Math.round(graphState.barDuration.asDays()) - }, - groupsArgs: { - ...baseGroupArgs, - barWidthDays: Math.round(graphState.barDuration.asDays()) - }, - meterSkipQuery: !baseMeterArgs.ids.length, - groupSkipQuery: !baseGroupArgs.ids.length, - meta: { - endpoint: readingsApi.endpoints.bar.name - } + const meterArgs = commonMeterArgs; + const groupArgs = commonGroupArgs; + const meterShouldSkip = !commonMeterArgs.ids.length; + const groupShouldSkip = !commonGroupArgs.ids.length; + return { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } + } +) + +export const selectBarChartQueryArgs = createSelector( + selectCommonQueryArgs, + selectBarWidthDays, + ({ commonMeterArgs, commonGroupArgs }, barWidthDays) => { + // props to pass into the line chart component + + const meterArgs = { + ...commonMeterArgs, + barWidthDays: Math.round(barWidthDays.asDays()) + + }; + const groupArgs = { + ...commonGroupArgs, + barWidthDays: Math.round(barWidthDays.asDays()) + }; + const meterShouldSkip = !commonMeterArgs.ids.length; + const groupShouldSkip = !commonGroupArgs.ids.length; + return { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } + } +) + +export const selectCompareChartQueryArgs = createSelector( + selectCommonQueryArgs, + selectComparePeriod, + selectCompareTimeInterval, + ({ commonMeterArgs, commonGroupArgs }, comparePeriod, compareTimeInterval) => { + const meterArgs = { + ...commonMeterArgs, + shift: calculateCompareShift(comparePeriod).toISOString(), + curr_start: compareTimeInterval.getStartTimestamp()?.toISOString(), + curr_end: compareTimeInterval.getEndTimestamp()?.toISOString() } - // TODO; Make 2 types for multi-id and single-id request ARGS not idea, but works for now. - const threeD: ChartQuerySingleArgs = { - // Fix not null assertion(s) - args: { - id: graphState.threeD.meterOrGroupID!, - timeInterval: roundTimeIntervalForFetch(graphState.queryTimeInterval).toString(), - unitID: graphState.selectedUnit, - readingInterval: graphState.threeD.readingInterval, - meterOrGroup: graphState.threeD.meterOrGroup! - }, - skipQuery: !graphState.threeD.meterOrGroupID || !graphState.queryTimeInterval.getIsBounded(), - meta: { - endpoint: readingsApi.endpoints.threeD.name - } + const groupArgs = { + ...commonGroupArgs, + shift: calculateCompareShift(comparePeriod).toISOString(), + curr_start: compareTimeInterval.getStartTimestamp()?.toISOString(), + curr_end: compareTimeInterval.getEndTimestamp()?.toISOString() } + const meterShouldSkip = !commonMeterArgs.ids.length; + const groupShouldSkip = !commonGroupArgs.ids.length; + return { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } + } +) - const compare: ChartMultiQueryArgs = { - meterArgs: { - ...baseMeterArgs, - shift: calculateCompareShift(graphState.comparePeriod).toISOString(), - curr_start: graphState.compareTimeInterval.getStartTimestamp()?.toISOString(), - curr_end: graphState.compareTimeInterval.getEndTimestamp()?.toISOString() - }, - groupsArgs: { - ...baseGroupArgs, - shift: calculateCompareShift(graphState.comparePeriod).toISOString(), - curr_start: graphState.compareTimeInterval.getStartTimestamp()?.toISOString(), - curr_end: graphState.compareTimeInterval.getEndTimestamp()?.toISOString() - }, - meterSkipQuery: !baseMeterArgs.ids.length, - groupSkipQuery: !baseGroupArgs.ids.length, - meta: { - endpoint: readingsApi.endpoints.compare.name - } - } +export const selectMapChartQueryArgs = createSelector( + selectBarChartQueryArgs, + selectQueryTimeInterval, + (state: RootState) => state.maps, + (barChartArgs, queryTimeInterval, maps) => { - return { line, bar, threeD, compare } + const meterArgs = { + ...barChartArgs.meterArgs, + // Maps uses the Bar Endpoint so just use its args for simplicity, however barWidthDays should be durationDays + barWidthDays: Math.round(((queryTimeInterval.equals(TimeInterval.unbounded())) + ? moment.duration(4, 'weeks') + : moment.duration(queryTimeInterval.duration('days'), 'days')).asDays()) + } + const groupArgs = { + ...barChartArgs.groupArgs, + // Maps uses the Bar Endpoint so just use its args for simplicity, however barWidthDays should be durationDays + barWidthDays: Math.round(((queryTimeInterval.equals(TimeInterval.unbounded())) + ? moment.duration(4, 'weeks') + : moment.duration(queryTimeInterval.duration('days'), 'days')).asDays() + ) + } + const meterShouldSkip = barChartArgs.meterShouldSkip || maps.selectedMap === 0 + const groupShouldSkip = barChartArgs.groupShouldSkip || maps.selectedMap === 0 + console.log(meterShouldSkip, groupShouldSkip) + return { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } } + ) -// TODO DUPLICATE SELECTOR? UI SELECTOR MAY CONTAIN SAME LOGIC, CONSOLIDATE IF POSSIBLE? -export const selectVisibleMetersGroupsDataByID = createSelector( - selectMeterDataById, - selectGroupDataById, - selectIsLoggedInAsAdmin, - (meterDataById, groupDataById, isAdmin) => { - let visibleMeters; - let visibleGroups; - if (isAdmin) { - visibleMeters = meterDataById - visibleGroups = groupDataById; - } else { - visibleMeters = _.filter(meterDataById, meter => meter.displayable); - visibleGroups = _.filter(groupDataById, group => group.displayable); - } - return { visibleMeters, visibleGroups } +// Selector prepares the query args for ALL graph endpoints based on the current graph slice state +// TODO Break down into individual selectors? +// Verify if prop drilling is a better pattern vs useSelector in same sameComponent +export const selectThreeDQueryArgs = createSelector( + selectGraphState, + graphState => { + const queryTimeInterval = graphState.queryTimeInterval + const args = { + id: graphState.threeD.meterOrGroupID!, + timeInterval: roundTimeIntervalForFetch(queryTimeInterval).toString(), + unitID: graphState.selectedUnit, + readingInterval: graphState.threeD.readingInterval, + meterOrGroup: graphState.threeD.meterOrGroup! + } + const shouldSkipQuery = !graphState.threeD.meterOrGroupID || !queryTimeInterval.getIsBounded() + return { args, shouldSkipQuery } } +) + +export const selectChartQueryArgs = createSelector( + selectLineChartQueryArgs, + selectBarChartQueryArgs, + selectCompareChartQueryArgs, + selectMapChartQueryArgs, + selectThreeDQueryArgs, + (line, bar, compare, map, threeD) => ({ + line, + bar, + compare, + map, + threeD + }) ) \ No newline at end of file diff --git a/src/client/app/redux/selectors/threeDSelectors.ts b/src/client/app/redux/selectors/threeDSelectors.ts index ef63a9b1a..7b0e4b6d8 100644 --- a/src/client/app/redux/selectors/threeDSelectors.ts +++ b/src/client/app/redux/selectors/threeDSelectors.ts @@ -1,16 +1,11 @@ import { createSelector } from '@reduxjs/toolkit'; -import { selectMeterDataById } from '../../redux/api/metersApi'; import { - selectGraphUnitID, - selectQueryTimeInterval, - selectThreeDMeterOrGroup, selectThreeDMeterOrGroupID, - selectThreeDReadingInterval + selectThreeDMeterOrGroup, selectThreeDMeterOrGroupID } from '../../reducers/graph'; import { selectGroupDataById } from '../../redux/api/groupsApi'; +import { selectMeterDataById } from '../../redux/api/metersApi'; import { MeterOrGroup } from '../../types/redux/graph'; -import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; -import { ThreeDReadingApiArgs } from './dataSelectors'; // Memoized Selectors @@ -46,26 +41,3 @@ export const selectThreeDComponentInfo = createSelector( } ) - -export const selectThreeDQueryArgs = createSelector( - selectThreeDMeterOrGroupID, - selectQueryTimeInterval, - selectGraphUnitID, - selectThreeDReadingInterval, - selectThreeDMeterOrGroup, - (id, timeInterval, unitID, readingInterval, meterOrGroup) => { - return { - id: id, - timeInterval: roundTimeIntervalForFetch(timeInterval).toString(), - unitID: unitID, - readingInterval: readingInterval, - meterOrGroup: meterOrGroup - } as ThreeDReadingApiArgs - } -) - -export const selectThreeDSkip = createSelector( - selectThreeDMeterOrGroupID, - selectQueryTimeInterval, - (id, interval) => !id || !interval.getIsBounded() -) \ No newline at end of file From cbd6921cef907cfe489d0e55a974603c1c04400d Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Thu, 9 Nov 2023 02:05:12 +0000 Subject: [PATCH 040/131] Refactor Query Arg Selectors --- .../app/redux/selectors/dataSelectors.ts | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/client/app/redux/selectors/dataSelectors.ts b/src/client/app/redux/selectors/dataSelectors.ts index 5c56186b9..2eb3ee6eb 100644 --- a/src/client/app/redux/selectors/dataSelectors.ts +++ b/src/client/app/redux/selectors/dataSelectors.ts @@ -6,6 +6,12 @@ import { selectBarWidthDays, selectComparePeriod, selectCompareTimeInterval, sel import { MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; import { calculateCompareShift } from '../../utils/calculateCompare'; import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; +import * as moment from 'moment'; +import { TimeInterval } from '../../../../common/TimeInterval'; +import { selectBarWidthDays, selectComparePeriod, selectCompareTimeInterval, selectGraphState, selectQueryTimeInterval } from '../../reducers/graph'; +import { MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; +import { calculateCompareShift } from '../../utils/calculateCompare'; +import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; import { selectGroupDataById } from '../api/groupsApi'; import { selectMeterDataById } from '../api/metersApi'; import { selectIsLoggedInAsAdmin } from './authSelectors'; @@ -84,8 +90,8 @@ export const selectLineChartQueryArgs = createSelector( ({ commonMeterArgs, commonGroupArgs }) => { // props to pass into the line chart component - const meterArgs = commonMeterArgs; - const groupArgs = commonGroupArgs; + const meterArgs: LineReadingApiArgs = commonMeterArgs; + const groupArgs: LineReadingApiArgs = commonGroupArgs; const meterShouldSkip = !commonMeterArgs.ids.length; const groupShouldSkip = !commonGroupArgs.ids.length; return { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } @@ -98,12 +104,12 @@ export const selectBarChartQueryArgs = createSelector( ({ commonMeterArgs, commonGroupArgs }, barWidthDays) => { // props to pass into the line chart component - const meterArgs = { + const meterArgs: BarReadingApiArgs = { ...commonMeterArgs, barWidthDays: Math.round(barWidthDays.asDays()) }; - const groupArgs = { + const groupArgs: BarReadingApiArgs = { ...commonGroupArgs, barWidthDays: Math.round(barWidthDays.asDays()) }; @@ -118,13 +124,13 @@ export const selectCompareChartQueryArgs = createSelector( selectComparePeriod, selectCompareTimeInterval, ({ commonMeterArgs, commonGroupArgs }, comparePeriod, compareTimeInterval) => { - const meterArgs = { + const meterArgs: CompareReadingApiArgs = { ...commonMeterArgs, shift: calculateCompareShift(comparePeriod).toISOString(), curr_start: compareTimeInterval.getStartTimestamp()?.toISOString(), curr_end: compareTimeInterval.getEndTimestamp()?.toISOString() } - const groupArgs = { + const groupArgs: CompareReadingApiArgs = { ...commonGroupArgs, shift: calculateCompareShift(comparePeriod).toISOString(), curr_start: compareTimeInterval.getStartTimestamp()?.toISOString(), @@ -142,14 +148,14 @@ export const selectMapChartQueryArgs = createSelector( (state: RootState) => state.maps, (barChartArgs, queryTimeInterval, maps) => { - const meterArgs = { + const meterArgs: MapReadingApiArgs = { ...barChartArgs.meterArgs, // Maps uses the Bar Endpoint so just use its args for simplicity, however barWidthDays should be durationDays barWidthDays: Math.round(((queryTimeInterval.equals(TimeInterval.unbounded())) ? moment.duration(4, 'weeks') : moment.duration(queryTimeInterval.duration('days'), 'days')).asDays()) } - const groupArgs = { + const groupArgs: MapReadingApiArgs = { ...barChartArgs.groupArgs, // Maps uses the Bar Endpoint so just use its args for simplicity, however barWidthDays should be durationDays barWidthDays: Math.round(((queryTimeInterval.equals(TimeInterval.unbounded())) @@ -159,7 +165,6 @@ export const selectMapChartQueryArgs = createSelector( } const meterShouldSkip = barChartArgs.meterShouldSkip || maps.selectedMap === 0 const groupShouldSkip = barChartArgs.groupShouldSkip || maps.selectedMap === 0 - console.log(meterShouldSkip, groupShouldSkip) return { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } } @@ -173,7 +178,7 @@ export const selectThreeDQueryArgs = createSelector( selectGraphState, graphState => { const queryTimeInterval = graphState.queryTimeInterval - const args = { + const args: ThreeDReadingApiArgs = { id: graphState.threeD.meterOrGroupID!, timeInterval: roundTimeIntervalForFetch(queryTimeInterval).toString(), unitID: graphState.selectedUnit, From e6068e7d124e5e6c9ae91246eaa554d8f9e059dc Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Sun, 12 Nov 2023 19:24:18 +0000 Subject: [PATCH 041/131] Separate GraphHistory from Graph - Update Middleware to mediate between AppState, and Graph --- .../app/components/DashboardComponent.tsx | 1 - .../app/components/HistoryComponent.tsx | 18 +++-- src/client/app/initScript.ts | 33 ++++++-- src/client/app/reducers/appStateSlice.ts | 72 ++++++++++++++++++ src/client/app/reducers/graph.ts | 28 +------ src/client/app/reducers/index.ts | 6 +- src/client/app/redux/api/authApi.ts | 34 +-------- .../app/redux/middleware/graphHistory.ts | 76 ++++++++++--------- src/client/app/redux/middleware/middleware.ts | 12 ++- src/client/app/store.ts | 4 +- src/client/app/types/redux/graph.ts | 4 - 11 files changed, 171 insertions(+), 117 deletions(-) create mode 100644 src/client/app/reducers/appStateSlice.ts diff --git a/src/client/app/components/DashboardComponent.tsx b/src/client/app/components/DashboardComponent.tsx index b33d76397..92dc2d7bf 100644 --- a/src/client/app/components/DashboardComponent.tsx +++ b/src/client/app/components/DashboardComponent.tsx @@ -35,7 +35,6 @@ export default function DashboardComponent() {
- {chartToRender === ChartTypes.line && } {chartToRender === ChartTypes.bar && } {chartToRender === ChartTypes.compare && } diff --git a/src/client/app/components/HistoryComponent.tsx b/src/client/app/components/HistoryComponent.tsx index 4104004f9..81c937a1c 100644 --- a/src/client/app/components/HistoryComponent.tsx +++ b/src/client/app/components/HistoryComponent.tsx @@ -1,25 +1,31 @@ import * as React from 'react'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; -import { prevHistory, nextHistory } from '../reducers/graph'; +import { prevHistory, forwardHistory, selectBackHistoryStack, selectForwardHistoryStack } from '../reducers/appStateSlice'; /** * @returns Renders a history component with previous and next buttons. */ export default function HistoryComponent() { const dispatch = useAppDispatch(); - const back = useAppSelector(state => state.graph.backHistoryStack) - const forward = useAppSelector(state => state.graph.forwardHistoryStack) + const backStack = useAppSelector(selectBackHistoryStack) + const forwardStack = useAppSelector(selectForwardHistoryStack) return (
dispatch(prevHistory())} > dispatch(nextHistory())} + style={{ + visibility: forwardStack.length < 1 ? 'hidden' : 'visible', + cursor: 'pointer' + }} + onClick={() => dispatch(forwardHistory())} > diff --git a/src/client/app/initScript.ts b/src/client/app/initScript.ts index 906c8807a..32bb0d747 100644 --- a/src/client/app/initScript.ts +++ b/src/client/app/initScript.ts @@ -2,23 +2,22 @@ * 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 { versionApi } from './redux/api/versionApi'; +import { appStateSlice } from './reducers/appStateSlice'; +import { currentUserSlice } from './reducers/currentUser'; import { authApi } from './redux/api/authApi'; import { conversionsApi } from './redux/api/conversionsApi'; import { groupsApi } from './redux/api/groupsApi'; import { metersApi } from './redux/api/metersApi'; import { preferencesApi } from './redux/api/preferencesApi'; import { unitsApi } from './redux/api/unitsApi'; +import { userApi } from './redux/api/userApi'; +import { versionApi } from './redux/api/versionApi'; import { store } from './store'; -import { getToken, hasToken } from './utils/token'; +import { deleteToken, getToken, hasToken } from './utils/token'; // Method initiates many data fetching calls on startup before react begins to render export const initializeApp = async () => { - // There are two primary ways to fetch data with RTKQuery - // Redux Toolkit generates hooks for use in react components, and standalone initiate dispatches as seen below. - // https://redux-toolkit.js.org/rtk-query/usage/usage-without-react-hooks - // 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. store.dispatch(versionApi.endpoints.getVersion.initiate()) @@ -29,12 +28,30 @@ export const initializeApp = async () => { // If user is an admin, they receive additional meter details. // To avoid sending duplicate requests upon startup, verify user then fetch - // TODO Not working as expected, still pings for meters and groups twice, due to onQueryStarted async call on verify Token if (hasToken()) { // User has a session token verify before requesting meter/group details - await store.dispatch(authApi.endpoints.verifyToken.initiate(getToken())) + try { + await store.dispatch(authApi.endpoints.verifyToken.initiate(getToken())) + // Token is valid if not errored out by this point, + // Apis will now use the token in headers via baseAPI's Prepare Headers + store.dispatch(currentUserSlice.actions.setUserToken(getToken())) + // Get userDetails with verified token in headers + await store.dispatch(userApi.endpoints.getUserDetails.initiate(undefined, { subscribe: false })) + .unwrap() + .catch(e => { throw (e) }) + + } catch { + // User had a token that isn't valid or getUserDetails threw an error. + // Assume token is invalid. Delete if any + deleteToken() + } + } // Request meter/group/details store.dispatch(metersApi.endpoints.getMeters.initiate()) store.dispatch(groupsApi.endpoints.getGroups.initiate()) + await store.dispatch(preferencesApi.util.getRunningQueryThunk('getPreferences', undefined)) + store.dispatch(appStateSlice.actions.updateHistory(store.getState().graph)) + + store.dispatch(appStateSlice.actions.setInitComplete(true)) } diff --git a/src/client/app/reducers/appStateSlice.ts b/src/client/app/reducers/appStateSlice.ts new file mode 100644 index 000000000..9b86a5c21 --- /dev/null +++ b/src/client/app/reducers/appStateSlice.ts @@ -0,0 +1,72 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { GraphState } from '../types/redux/graph'; +interface appStateSlice { + initComplete: boolean; + backHistoryStack: GraphState[]; + forwardHistoryStack: GraphState[]; +} + +const defaultState: appStateSlice = { + initComplete: false, + backHistoryStack: [], + forwardHistoryStack: [] +} + +export const appStateSlice = createSlice({ + name: 'appState', + initialState: defaultState, + reducers: create => ({ + // New way of creating reducers as of RTK 2.0 + // Allows thunks inside of reducers, and prepareReducers with 'create' builder notation + setInitComplete: create.reducer((state, action) => { + state.initComplete = action.payload + }), + updateHistory: create.reducer((state, action) => { + state.backHistoryStack.push(action.payload) + // reset forward history on new 'visit' + state.forwardHistoryStack.length = 0 + + }), + prevHistory: create.reducer(state => { + if (state.backHistoryStack.length > 1) { + // prev and forward can safely use type assertion due to length check. pop() Will never be undefined + state.forwardHistoryStack.push(state.backHistoryStack.pop() as GraphState) + } + }), + forwardHistory: create.reducer(state => { + if (state.forwardHistoryStack.length) { + state.backHistoryStack.push(state.forwardHistoryStack.pop() as GraphState) + } + }), + clearHistory: create.reducer(state => { + // TODO Verify the behavior of clear before adding an onClick + state.forwardHistoryStack.length = 0 + state.backHistoryStack.splice(0, state.backHistoryStack.length - 1) + }) + }), + selectors: { + selectBackHistoryStack: state => state.backHistoryStack, + selectForwardHistoryStack: state => state.forwardHistoryStack, + // Explicit return value required when calling sameSlice's getSelectors, otherwise circular type inference breaks TS. + selectBackHistoryTop: (state): GraphState => { + const { selectBackHistoryStack } = appStateSlice.getSelectors() + const backHistory = selectBackHistoryStack(state) + const top = backHistory[backHistory.length - 1] + return top + } + } +}) + +export const { + updateHistory, + prevHistory, + forwardHistory, + setInitComplete, + clearHistory +} = appStateSlice.actions + +export const { + selectBackHistoryStack, + selectForwardHistoryStack, + selectBackHistoryTop +} = appStateSlice.selectors diff --git a/src/client/app/reducers/graph.ts b/src/client/app/reducers/graph.ts index 6adf63584..b79711ec0 100644 --- a/src/client/app/reducers/graph.ts +++ b/src/client/app/reducers/graph.ts @@ -3,7 +3,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { PayloadAction, createSlice } from '@reduxjs/toolkit'; -import * as _ from 'lodash'; import * as moment from 'moment'; import { ActionMeta } from 'react-select'; import { TimeInterval } from '../../../common/TimeInterval'; @@ -36,9 +35,7 @@ const defaultState: GraphState = { meterOrGroupID: undefined, meterOrGroup: undefined, readingInterval: ReadingInterval.Hourly - }, - backHistoryStack: [], - forwardHistoryStack: [] + } }; export const graphSlice = createSlice({ @@ -220,23 +217,7 @@ export const graphSlice = createSlice({ state.queryTimeInterval = TimeInterval.unbounded() } }, - updateHistory: (state, action: PayloadAction) => { - state.backHistoryStack.push(_.omit(action.payload, ['backHistoryStack', 'forwardHistoryStack'])) - // reset forward history on new 'visit' - state.forwardHistoryStack = [] - }, - prevHistory: state => { - if (state.backHistoryStack.length > 1) { - state.forwardHistoryStack.push(state.backHistoryStack.pop()!) - } - Object.assign(state, state.backHistoryStack[state.backHistoryStack.length - 1]); - }, - nextHistory: state => { - if (state.forwardHistoryStack.length) { - state.backHistoryStack.push(state.forwardHistoryStack.pop()!) - Object.assign(state, state.backHistoryStack[state.backHistoryStack.length - 1]) - } - } + setGraphState: (_state, action: PayloadAction) => action.payload }, extraReducers: builder => { builder.addMatcher(preferencesApi.endpoints.getPreferences.matchFulfilled, (state, action) => { @@ -248,7 +229,6 @@ export const graphSlice = createSlice({ state.barStacking = action.payload.defaultBarStacking state.areaNormalization = action.payload.defaultAreaNormalization } - state.backHistoryStack.push(_.omit(state, ['backHistoryStack', 'forwardHistoryStack'])) }) }, // New Feature as of 2.0.0 Beta. @@ -326,7 +306,5 @@ export const { updateThreeDMeterOrGroup, updateSelectedMetersOrGroups, resetTimeInterval, - updateHistory, - prevHistory, - nextHistory + setGraphState } = graphSlice.actions \ No newline at end of file diff --git a/src/client/app/reducers/index.ts b/src/client/app/reducers/index.ts index 0197381d3..3827e0121 100644 --- a/src/client/app/reducers/index.ts +++ b/src/client/app/reducers/index.ts @@ -10,14 +10,16 @@ import { unsavedWarningSlice } from './unsavedWarning'; import { optionsSlice } from './options'; import { baseApi } from '../redux/api/baseApi'; import { graphSlice } from './graph'; +import { appStateSlice } from './appStateSlice'; export const rootReducer = combineReducers({ - maps, + appState: appStateSlice.reducer, graph: graphSlice.reducer, admin: adminSlice.reducer, currentUser: currentUserSlice.reducer, unsavedWarning: unsavedWarningSlice.reducer, options: optionsSlice.reducer, // RTK Query's Derived Reducers - [baseApi.reducerPath]: baseApi.reducer + [baseApi.reducerPath]: baseApi.reducer, + maps }); \ No newline at end of file diff --git a/src/client/app/redux/api/authApi.ts b/src/client/app/redux/api/authApi.ts index 336be93b4..26f7b366c 100644 --- a/src/client/app/redux/api/authApi.ts +++ b/src/client/app/redux/api/authApi.ts @@ -2,7 +2,6 @@ import { currentUserSlice } from '../../reducers/currentUser'; import { User } from '../../types/items'; import { deleteToken } from '../../utils/token'; import { baseApi } from './baseApi'; -import { userApi } from './userApi'; type LoginResponse = User & { token: string @@ -29,38 +28,7 @@ export const authApi = baseApi.injectEndpoints({ url: 'api/verification', method: 'POST', body: { token: token } - }), - // Optional endpoint property that does additional logic when the query is initiated. - onQueryStarted: async (token, { dispatch, queryFulfilled }) => { - // wait for the initial query (verifyToken) to finish - await queryFulfilled - .then(async () => { - // 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(token)) - - // Get userDetails with verified token in headers - const response = dispatch(userApi.endpoints.getUserDetails.initiate()); - // Next time the endpoint is queried it should be should be re-fetched, not pulled from the cache - // Subscriptions are handled automatically by hooks, but not when called via 'dispatch(endpoint.initiate())' - // Manually unsubscribe from the cache via the returned promise's .unsubscribe() method - response.unsubscribe(); - // The returned response is the thunk's promise which internally handles the request's promise. - // Use unwrap to get the original request's promise. - await response.unwrap().catch(e => { throw (e) }) - - // if no error thrown user is now logged in and cache(s) may be out of date due to potential admin privileges etc. - // manually invalidate potentially out of date cache stores - dispatch(baseApi.util.invalidateTags(['MeterData', 'GroupData', 'Users'])) - // If subscriptions to these tagged endpoints exist, they will automatically re-fetch. - // Otherwise subsequent requests will bypass and overwrite cache - }) - .catch(() => { - // User had a token that isn't valid or getUserDetails threw an error. - // Assume token is invalid. Delete if any - deleteToken() - }) - } + }) }), logout: builder.mutation({ queryFn: (_, { dispatch }) => { diff --git a/src/client/app/redux/middleware/graphHistory.ts b/src/client/app/redux/middleware/graphHistory.ts index bf91b2a59..b18b3a36a 100644 --- a/src/client/app/redux/middleware/graphHistory.ts +++ b/src/client/app/redux/middleware/graphHistory.ts @@ -1,37 +1,45 @@ // https://redux-toolkit.js.org/api/createListenerMiddleware#typescript-usage -import { createListenerMiddleware, isAnyOf } from '@reduxjs/toolkit' -import { - graphSlice, - nextHistory, - prevHistory, - setHotlinked, - setOptionsVisibility, - toggleOptionsVisibility, - updateHistory -} from '../../reducers/graph' -import { AppStartListening } from './middleware' +import { isAnyOf } from '@reduxjs/toolkit'; +import { clearHistory, forwardHistory, prevHistory, selectBackHistoryTop, updateHistory } from '../../reducers/appStateSlice'; +import { graphSlice, setGraphState, setHotlinked, setOptionsVisibility, toggleOptionsVisibility } from '../../reducers/graph'; +import * as _ from 'lodash'; +import { AppStartListening } from './middleware'; -export const historyMiddleware = createListenerMiddleware() -// Typescript usage for middleware api -const startHistoryListening = historyMiddleware.startListening as AppStartListening +// This middleware acts as a mediator between two slices of state. AppState, and GraphState. +// graphSlice cannot 'see' the appStateSlice, the middleware can see both and transact between the two. +export const historyMiddleware = (startListening: AppStartListening) => { -startHistoryListening({ - matcher: isAnyOf( - // listen to all graphSlice actions, filter out the ones don't directly alter the graph, or ones which can cause infinite recursion - // we use updateHistory here, so listening for updateHistory would cause infinite loops etc. - ...Object.values(graphSlice.actions) - .filter(action => !( - action === nextHistory || - action === prevHistory || - action === updateHistory || - action === toggleOptionsVisibility || - action === setOptionsVisibility || - action === setHotlinked - ) - ) - ), - effect: (_action, { dispatch, getState }) => { - const { graph } = getState(); - dispatch(updateHistory(graph)) - } -}) + startListening({ + predicate: (action, currentState, previousState) => { + // deep compare of previous state added mostly due to potential state triggers from laying on backspace when deleting meters or groups. + return isHistoryTrigger(action) && !_.isEqual(currentState.graph, previousState.graph) + } + , + effect: (_action, { dispatch, getState }) => { + dispatch(updateHistory(getState().graph)) + } + }) + + // Listen for calls to traverse history forward or backwards + startListening({ + matcher: isAnyOf(forwardHistory, prevHistory, clearHistory), + effect: (_action, { dispatch, getState }) => { + // History Stack logic written such that after prev,or next, is executed, the history to set is the top of the backStack + const graphStateHistory = selectBackHistoryTop(getState()) + dispatch(setGraphState(graphStateHistory)) + } + }) +} + +// we use updateHistory here, so listening for updateHistory would cause infinite loops etc. +const isHistoryTrigger = isAnyOf( + // listen to all graphSlice actions + ...Object.values(graphSlice.actions) + .filter(action => !( + // filter out the ones don't directly alter the graph, or ones which can cause infinite recursion + toggleOptionsVisibility.match(action) || + setOptionsVisibility.match(action) || + setHotlinked.match(action) || + setGraphState.match(action) + )) +) \ No newline at end of file diff --git a/src/client/app/redux/middleware/middleware.ts b/src/client/app/redux/middleware/middleware.ts index ad2d9fe7f..a4f409ad9 100644 --- a/src/client/app/redux/middleware/middleware.ts +++ b/src/client/app/redux/middleware/middleware.ts @@ -1,6 +1,14 @@ // listenerMiddleware.ts // https://redux-toolkit.js.org/api/createListenerMiddleware#typescript-usage -import { type TypedStartListening, type TypedAddListener, addListener } from '@reduxjs/toolkit' +import { type TypedStartListening, type TypedAddListener, addListener, createListenerMiddleware } from '@reduxjs/toolkit' import type { RootState, AppDispatch } from '../../store' +import { historyMiddleware } from './graphHistory' + + export type AppStartListening = TypedStartListening -export const addAppListener = addListener as TypedAddListener \ No newline at end of file +export const addAppListener = addListener as TypedAddListener +export const listenerMiddleware = createListenerMiddleware() +// Typescript usage for middleware api +export const startListening = listenerMiddleware.startListening as AppStartListening + +historyMiddleware(startListening) \ No newline at end of file diff --git a/src/client/app/store.ts b/src/client/app/store.ts index 70a7258cf..0439fb869 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -5,8 +5,8 @@ import { configureStore } from '@reduxjs/toolkit' import { rootReducer } from './reducers'; import { baseApi } from './redux/api/baseApi'; -import { historyMiddleware } from './redux/middleware/graphHistory'; import { Dispatch } from './types/redux/actions'; +import { listenerMiddleware } from './redux/middleware/middleware'; export const store = configureStore({ @@ -15,7 +15,7 @@ export const store = configureStore({ // immutableCheck: false, serializableCheck: false }) - .prepend(historyMiddleware.middleware) + .prepend(listenerMiddleware.middleware) .concat(baseApi.middleware) }); diff --git a/src/client/app/types/redux/graph.ts b/src/client/app/types/redux/graph.ts index e6fb2923f..4c258c0da 100644 --- a/src/client/app/types/redux/graph.ts +++ b/src/client/app/types/redux/graph.ts @@ -75,8 +75,4 @@ export interface GraphState { showMinMax: boolean; threeD: ThreeDState; queryTimeInterval: TimeInterval; - backHistoryStack: GraphStateHistory[]; - forwardHistoryStack: GraphStateHistory[]; -} -export interface GraphStateHistory extends Omit { } \ No newline at end of file From 88dc79dec802e74bcaae8c9933f38b5852ba07b5 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Mon, 13 Nov 2023 21:40:19 +0000 Subject: [PATCH 042/131] Sweep Selectors for clarity/usage --- .../app/components/BarChartComponent.tsx | 2 +- src/client/app/components/ExportComponent.tsx | 4 +- .../app/components/LineChartComponent.tsx | 2 +- .../app/components/MapChartComponent.tsx | 2 +- .../MeterAndGroupSelectComponent.tsx | 14 +- .../MultiCompareChartComponentWIP.tsx | 2 +- .../ReadingsPerDaySelectComponent.tsx | 2 +- .../app/components/RouteComponentWIP.tsx | 5 +- src/client/app/components/ThreeDComponent.tsx | 2 +- .../CreateConversionModalComponent.tsx | 1 - .../CreateConversionModalComponentWIP.tsx | 11 +- .../groups/EditGroupModalComponentWIP.tsx | 4 +- .../groups/GroupViewComponentWIP.tsx | 4 +- .../groups/GroupsDetailComponent.tsx | 8 +- .../groups/GroupsDetailComponentWIP.tsx | 8 +- .../meters/CreateMeterModalComponentWIP.tsx | 126 +++------- .../meters/MeterViewComponentWIP.tsx | 4 +- .../meters/MetersDetailComponent.tsx | 9 +- .../meters/MetersDetailComponentWIP.tsx | 8 +- .../app/containers/CompareChartContainer.ts | 8 +- src/client/app/initScript.ts | 3 - src/client/app/reducers/currentUser.ts | 16 +- src/client/app/reducers/graph.ts | 20 +- src/client/app/redux/api/groupsApi.ts | 4 +- src/client/app/redux/api/readingsApi.ts | 2 +- src/client/app/redux/componentHooks.ts | 34 +-- .../app/redux/selectors/adminSelectors.ts | 118 ++++++--- .../app/redux/selectors/authSelectors.ts | 14 -- .../selectors/authVisibilitySelectors.ts | 52 ++++ .../redux/selectors/chartQuerySelectors.ts | 186 ++++++++++++++ .../app/redux/selectors/dataSelectors.ts | 206 ---------------- src/client/app/redux/selectors/selectors.ts | 8 + src/client/app/redux/selectors/uiSelectors.ts | 230 ++++++------------ src/client/app/store.ts | 8 +- 34 files changed, 528 insertions(+), 599 deletions(-) delete mode 100644 src/client/app/redux/selectors/authSelectors.ts create mode 100644 src/client/app/redux/selectors/authVisibilitySelectors.ts create mode 100644 src/client/app/redux/selectors/chartQuerySelectors.ts delete mode 100644 src/client/app/redux/selectors/dataSelectors.ts create mode 100644 src/client/app/redux/selectors/selectors.ts diff --git a/src/client/app/components/BarChartComponent.tsx b/src/client/app/components/BarChartComponent.tsx index ec7a82e21..e33da4452 100644 --- a/src/client/app/components/BarChartComponent.tsx +++ b/src/client/app/components/BarChartComponent.tsx @@ -14,7 +14,7 @@ import { selectMeterDataById } from '../redux/api/metersApi'; import { readingsApi } from '../redux/api/readingsApi'; import { selectUnitDataById } from '../redux/api/unitsApi'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; -import { selectBarChartQueryArgs } from '../redux/selectors/dataSelectors'; +import { selectBarChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; import { DataType } from '../types/Datasources'; import { UnitRepresentType } from '../types/redux/units'; import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; diff --git a/src/client/app/components/ExportComponent.tsx b/src/client/app/components/ExportComponent.tsx index c179aefe6..40fb5c86b 100644 --- a/src/client/app/components/ExportComponent.tsx +++ b/src/client/app/components/ExportComponent.tsx @@ -12,7 +12,7 @@ import { selectMeterDataById } from '../redux/api/metersApi'; import { readingsApi } from '../redux/api/readingsApi'; import { selectUnitDataById } from '../redux/api/unitsApi'; import { useAppSelector } from '../redux/hooks'; -import { selectChartQueryArgs } from '../redux/selectors/dataSelectors'; +import { selectAllChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; import { UserRole } from '../types/items'; import { ConversionData } from '../types/redux/conversions'; import { ChartTypes, MeterOrGroup } from '../types/redux/graph'; @@ -46,7 +46,7 @@ export default function ExportComponent() { // Time range of graphic const timeInterval = graphState.queryTimeInterval; - const queryArgs = useAppSelector(selectChartQueryArgs) + const queryArgs = useAppSelector(selectAllChartQueryArgs) const { data: lineMeterReadings = {}, isFetching: lineMeterIsFetching } = readingsApi.endpoints.line.useQueryState(queryArgs.line.meterArgs); const { data: lineGroupReadings = {}, isFetching: groupMeterIsFetching } = readingsApi.endpoints.line.useQueryState(queryArgs.line.groupArgs); diff --git a/src/client/app/components/LineChartComponent.tsx b/src/client/app/components/LineChartComponent.tsx index a883faeb8..3355d2867 100644 --- a/src/client/app/components/LineChartComponent.tsx +++ b/src/client/app/components/LineChartComponent.tsx @@ -17,7 +17,7 @@ import { selectMeterDataById } from '../redux/api/metersApi'; import { readingsApi } from '../redux/api/readingsApi'; import { selectUnitDataById } from '../redux/api/unitsApi'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; -import { selectLineChartQueryArgs } from '../redux/selectors/dataSelectors'; +import { selectLineChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; import { DataType } from '../types/Datasources'; import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; import getGraphColor from '../utils/getGraphColor'; diff --git a/src/client/app/components/MapChartComponent.tsx b/src/client/app/components/MapChartComponent.tsx index 09f84be63..ce9c2e26b 100644 --- a/src/client/app/components/MapChartComponent.tsx +++ b/src/client/app/components/MapChartComponent.tsx @@ -7,7 +7,7 @@ import * as moment from 'moment'; import * as React from 'react'; import Plot from 'react-plotly.js'; import { useSelector } from 'react-redux'; -import { selectMapChartQueryArgs } from '../redux/selectors/dataSelectors'; +import { selectMapChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; import { selectGroupDataById } from '../redux/api/groupsApi'; import { selectMeterDataById } from '../redux/api/metersApi'; import { readingsApi } from '../redux/api/readingsApi'; diff --git a/src/client/app/components/MeterAndGroupSelectComponent.tsx b/src/client/app/components/MeterAndGroupSelectComponent.tsx index b58a3ce57..b37bba7e8 100644 --- a/src/client/app/components/MeterAndGroupSelectComponent.tsx +++ b/src/client/app/components/MeterAndGroupSelectComponent.tsx @@ -23,25 +23,19 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; */ export default function MeterAndGroupSelectComponent(props: MeterAndGroupSelectProps) { const dispatch = useAppDispatch(); - const meterAndGroupSelectOptions = useAppSelector(selectMeterGroupSelectData); + const { meterGroupedOptions, groupsGroupedOptions, selectedMeterOptions, selectedGroupOptions } = useAppSelector(selectMeterGroupSelectData); const { somethingIsFetching } = useFetchingStates(); const { meterOrGroup } = props; // Set the current component's appropriate meter or group update from the graphSlice's Payload-Action Creator - const value = meterOrGroup === MeterOrGroup.meters ? - meterAndGroupSelectOptions.selectedMeterValues - : - meterAndGroupSelectOptions.selectedGroupValues + const value = meterOrGroup === MeterOrGroup.meters ? selectedMeterOptions.compatible : selectedGroupOptions.compatible; // Set the current component's appropriate meter or group SelectOption - const options = meterOrGroup === MeterOrGroup.meters ? - meterAndGroupSelectOptions.meterGroupedOptions - : - meterAndGroupSelectOptions.groupsGroupedOptions + const options = meterOrGroup === MeterOrGroup.meters ? meterGroupedOptions : groupsGroupedOptions; const onChange = (newValues: MultiValue, meta: ActionMeta) => { - const newMetersOrGroups = newValues.map((option: SelectOption) => option.value); + const newMetersOrGroups = newValues.map(option => option.value); dispatch(graphSlice.actions.updateSelectedMetersOrGroups({ newMetersOrGroups, meta })); } diff --git a/src/client/app/components/MultiCompareChartComponentWIP.tsx b/src/client/app/components/MultiCompareChartComponentWIP.tsx index 6b57969a7..8fa7e686c 100644 --- a/src/client/app/components/MultiCompareChartComponentWIP.tsx +++ b/src/client/app/components/MultiCompareChartComponentWIP.tsx @@ -11,7 +11,7 @@ import { selectGroupDataById } from '../redux/api/groupsApi'; import { selectMeterDataById } from '../redux/api/metersApi'; import { readingsApi } from '../redux/api/readingsApi'; import { useAppSelector } from '../redux/hooks'; -import { selectCompareChartQueryArgs } from '../redux/selectors/dataSelectors'; +import { selectCompareChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; import { SortingOrder } from '../utils/calculateCompare'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; diff --git a/src/client/app/components/ReadingsPerDaySelectComponent.tsx b/src/client/app/components/ReadingsPerDaySelectComponent.tsx index 16372c6d2..b651f82e2 100644 --- a/src/client/app/components/ReadingsPerDaySelectComponent.tsx +++ b/src/client/app/components/ReadingsPerDaySelectComponent.tsx @@ -11,7 +11,7 @@ import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { ChartTypes, ReadingInterval } from '../types/redux/graph'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; -import { selectThreeDQueryArgs } from '../redux/selectors/dataSelectors'; +import { selectThreeDQueryArgs } from '../redux/selectors/chartQuerySelectors'; /** * A component which allows users to select date ranges for the graphic diff --git a/src/client/app/components/RouteComponentWIP.tsx b/src/client/app/components/RouteComponentWIP.tsx index 15b792cd3..ed40ee1ff 100644 --- a/src/client/app/components/RouteComponentWIP.tsx +++ b/src/client/app/components/RouteComponentWIP.tsx @@ -12,11 +12,10 @@ import CreateUserContainer from '../containers/admin/CreateUserContainer'; import UploadCSVContainer from '../containers/csv/UploadCSVContainer'; import MapCalibrationContainer from '../containers/maps/MapCalibrationContainer'; import MapsDetailContainer from '../containers/maps/MapsDetailContainer'; -import { selectCurrentUser } from '../reducers/currentUser'; +import { selectCurrentUser, selectIsAdmin } from '../reducers/currentUser'; import { graphSlice } from '../reducers/graph'; import { baseApi } from '../redux/api/baseApi'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; -import { selectIsLoggedInAsAdmin } from '../redux/selectors/authSelectors'; import LocaleTranslationData from '../translations/data'; import { UserRole } from '../types/items'; import { ChartTypes, LineGraphRate, MeterOrGroup } from '../types/redux/graph'; @@ -40,7 +39,7 @@ import UnitsDetailComponent from './unit/UnitsDetailComponent'; const useWaitForInit = () => { const dispatch = useAppDispatch(); - const isAdmin = useAppSelector(state => selectIsLoggedInAsAdmin(state)); + const isAdmin = useAppSelector(selectIsAdmin); const currentUser = useAppSelector(state => selectCurrentUser(state)); const [initComplete, setInitComplete] = React.useState(false); diff --git a/src/client/app/components/ThreeDComponent.tsx b/src/client/app/components/ThreeDComponent.tsx index 29716802e..00755511f 100644 --- a/src/client/app/components/ThreeDComponent.tsx +++ b/src/client/app/components/ThreeDComponent.tsx @@ -11,7 +11,7 @@ import { selectMeterDataById } from '../redux/api/metersApi'; import { readingsApi } from '../redux/api/readingsApi'; import { selectUnitDataById } from '../redux/api/unitsApi'; import { useAppSelector } from '../redux/hooks'; -import { selectThreeDQueryArgs } from '../redux/selectors/dataSelectors'; +import { selectThreeDQueryArgs } from '../redux/selectors/chartQuerySelectors'; import { selectThreeDComponentInfo } from '../redux/selectors/threeDSelectors'; import { ThreeDReading } from '../types/readings'; import { GraphState, MeterOrGroup } from '../types/redux/graph'; diff --git a/src/client/app/components/conversion/CreateConversionModalComponent.tsx b/src/client/app/components/conversion/CreateConversionModalComponent.tsx index 359fbd948..72862ad94 100644 --- a/src/client/app/components/conversion/CreateConversionModalComponent.tsx +++ b/src/client/app/components/conversion/CreateConversionModalComponent.tsx @@ -163,7 +163,6 @@ export default function CreateConversionModalComponent(props: CreateConversionMo } // Inverse is not bidirectional else { - // Do not allow for a bidirectional conversion with an inverse that is not bidirectional if (bidirectional) { // The new conversion is bidirectional isValid = false; diff --git a/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx b/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx index ea78309ba..e6376e5ec 100644 --- a/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx +++ b/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx @@ -15,7 +15,6 @@ import { selectIsValidConversion } from '../../redux/selectors/adminSelectors'; import '../../styles/modal.css'; import { tooltipBaseStyle } from '../../styles/modalStyle'; import { TrueFalseType } from '../../types/items'; -import { UnitData } from '../../types/redux/units'; import { showErrorNotification } from '../../utils/notifications'; import translate from '../../utils/translate'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; @@ -28,15 +27,15 @@ export default function CreateConversionModalComponent() { const [addConversionMutation] = conversionsApi.useAddConversionMutation() const unitDataById = useAppSelector(selectUnitDataById) // Want units in sorted order by identifier regardless of case. - const unitsSorted = _.sortBy(Object.values(unitDataById), unit => unit.identifier.toLowerCase(), 'asc'); + const sortedUnitData = _.sortBy(Object.values(unitDataById), unit => unit.identifier.toLowerCase(), 'asc'); const defaultValues = { // Invalid source/destination ids arbitrarily set to -999. // Meter Units are not allowed to be a destination. sourceId: -999, - sourceOptions: unitsSorted as UnitData[], + sourceOptions: sortedUnitData, destinationId: -999, - destinationOptions: unitsSorted.filter(unit => unit.typeOfUnit !== 'meter') as UnitData[], + destinationOptions: sortedUnitData.filter(unit => unit.typeOfUnit !== 'meter'), bidirectional: true, slope: 0, intercept: 0, @@ -56,9 +55,7 @@ export default function CreateConversionModalComponent() { const [conversionState, setConversionState] = useState(defaultValues); // If the currently selected conversion is valid - const [validConversion, reason] = useAppSelector( - state => selectIsValidConversion(state, conversionState) - ) + const [validConversion, reason] = useAppSelector(state => selectIsValidConversion(state, conversionState)) const handleStringChange = (e: React.ChangeEvent) => { setConversionState({ ...conversionState, [e.target.name]: e.target.value }); diff --git a/src/client/app/components/groups/EditGroupModalComponentWIP.tsx b/src/client/app/components/groups/EditGroupModalComponentWIP.tsx index 37041246c..52eb07c45 100644 --- a/src/client/app/components/groups/EditGroupModalComponentWIP.tsx +++ b/src/client/app/components/groups/EditGroupModalComponentWIP.tsx @@ -16,7 +16,7 @@ import { groupsApi, selectGroupDataById } from '../../redux/api/groupsApi'; import { selectMeterDataById } from '../../redux/api/metersApi'; import { useAppSelector } from '../../redux/hooks'; import { selectPossibleGraphicUnits } from '../../redux/selectors/adminSelectors'; -import { selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; +import { selectIsAdmin } from '../../reducers/currentUser'; import { store } from '../../store'; import '../../styles/card-page.css'; import '../../styles/modal.css'; @@ -72,7 +72,7 @@ export default function EditGroupModalComponentWIP(props: EditGroupModalComponen const groupState = editGroupsState[props.groupId]; // Check for admin status - const loggedInAsAdmin = useAppSelector(selectIsLoggedInAsAdmin); + const loggedInAsAdmin = useAppSelector(selectIsAdmin); // The information on the allowed children of this group that can be selected in the menus. const groupChildrenDefaults = { diff --git a/src/client/app/components/groups/GroupViewComponentWIP.tsx b/src/client/app/components/groups/GroupViewComponentWIP.tsx index 296d30d40..2bb55e7ed 100644 --- a/src/client/app/components/groups/GroupViewComponentWIP.tsx +++ b/src/client/app/components/groups/GroupViewComponentWIP.tsx @@ -10,7 +10,7 @@ import { Button } from 'reactstrap'; import { GroupData } from 'types/redux/groups'; import { selectUnitDataById } from '../../redux/api/unitsApi'; import { useAppSelector } from '../../redux/hooks'; -import { selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; +import { selectIsAdmin } from '../../reducers/currentUser'; import '../../styles/card-page.css'; import { noUnitTranslated } from '../../utils/input'; import translate from '../../utils/translate'; @@ -41,7 +41,7 @@ export default function GroupViewComponentWIP(props: GroupViewComponentProps) { } // Check for admin status - const loggedInAsAdmin = useAppSelector(selectIsLoggedInAsAdmin); + const loggedInAsAdmin = useAppSelector(selectIsAdmin); // Set up to display the units associated with the group as the unit identifier. // unit state diff --git a/src/client/app/components/groups/GroupsDetailComponent.tsx b/src/client/app/components/groups/GroupsDetailComponent.tsx index 66434d94f..b6c31c797 100644 --- a/src/client/app/components/groups/GroupsDetailComponent.tsx +++ b/src/client/app/components/groups/GroupsDetailComponent.tsx @@ -7,14 +7,14 @@ import { FormattedMessage } from 'react-intl'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { useAppSelector } from '../../redux/hooks'; -import { selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; -import { selectVisibleMetersGroupsDataByID } from '../../redux/selectors/dataSelectors'; +import { selectVisibleMeterAndGroupDataByID } from '../../redux/selectors/adminSelectors'; import { potentialGraphicUnits } from '../../utils/input'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateGroupModalComponent from './CreateGroupModalComponent'; import GroupViewComponent from './GroupViewComponent'; import { GroupData } from 'types/redux/groups'; import { selectUnitDataById } from '../../redux/api/unitsApi'; +import { selectIsAdmin } from '../../reducers/currentUser'; /** * Defines the groups page card view @@ -23,10 +23,10 @@ import { selectUnitDataById } from '../../redux/api/unitsApi'; export default function GroupsDetailComponent() { // Check for admin status - const isAdmin = useAppSelector(state => selectIsLoggedInAsAdmin(state)); + const isAdmin = useAppSelector(state => selectIsAdmin(state)); // We only want displayable groups if non-admins because they still have non-displayable in state. - const { visibleGroups } = useAppSelector(state => selectVisibleMetersGroupsDataByID(state)); + const { visibleGroups } = useAppSelector(state => selectVisibleMeterAndGroupDataByID(state)); // Units state const unitDataById = useAppSelector(selectUnitDataById); diff --git a/src/client/app/components/groups/GroupsDetailComponentWIP.tsx b/src/client/app/components/groups/GroupsDetailComponentWIP.tsx index 1b6130402..3bbbb0969 100644 --- a/src/client/app/components/groups/GroupsDetailComponentWIP.tsx +++ b/src/client/app/components/groups/GroupsDetailComponentWIP.tsx @@ -6,8 +6,8 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { useAppSelector } from '../../redux/hooks'; -import { selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; -import { selectVisibleMetersGroupsDataByID } from '../../redux/selectors/dataSelectors'; +import { selectIsAdmin } from '../../reducers/currentUser'; +import { selectVisibleMeterAndGroupDataByID } from '../../redux/selectors/adminSelectors'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateGroupModalComponentWIP from './CreateGroupModalComponentWIP'; import GroupViewComponentWIP from './GroupViewComponentWIP'; @@ -19,10 +19,10 @@ import GroupViewComponentWIP from './GroupViewComponentWIP'; export default function GroupsDetailComponentWIP() { // Check for admin status - const isAdmin = useAppSelector(state => selectIsLoggedInAsAdmin(state)); + const isAdmin = useAppSelector(state => selectIsAdmin(state)); // We only want displayable groups if non-admins because they still have non-displayable in state. - const { visibleGroups } = useAppSelector(state => selectVisibleMetersGroupsDataByID(state)); + const { visibleGroups } = useAppSelector(state => selectVisibleMeterAndGroupDataByID(state)); diff --git a/src/client/app/components/meters/CreateMeterModalComponentWIP.tsx b/src/client/app/components/meters/CreateMeterModalComponentWIP.tsx index b8b2f09ea..2d3707de6 100644 --- a/src/client/app/components/meters/CreateMeterModalComponentWIP.tsx +++ b/src/client/app/components/meters/CreateMeterModalComponentWIP.tsx @@ -10,7 +10,7 @@ import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, M import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { metersApi } from '../../redux/api/metersApi'; import { useAppSelector } from '../../redux/hooks'; -import { makeSelectGraphicUnitCompatibility } from '../../redux/selectors/adminSelectors'; +import { makeSelectGraphicUnitCompatibility, selectDefaultCreateMeterValues } from '../../redux/selectors/adminSelectors'; import '../../styles/modal.css'; import { tooltipBaseStyle } from '../../styles/modalStyle'; import { TrueFalseType } from '../../types/items'; @@ -40,49 +40,9 @@ export default function CreateMeterModalComponent() { const [addMeter] = metersApi.endpoints.addMeter.useMutation() // Admin state so can get the default reading frequency. - const adminState = useAppSelector(state => state.admin) // Memo'd memoized selector const selectGraphicUnitCompatibility = React.useMemo(makeSelectGraphicUnitCompatibility, []) - - // TODO MAKE A SELECTOR? - const defaultValues = { - id: -99, - identifier: '', - name: '', - area: 0, - enabled: false, - displayable: false, - meterType: '', - url: '', - timeZone: '', - gps: '', - // Defaults of -999 (not to be confused with -99 which is no unit) - // Purely for allowing the default select to be "select a ..." - unitId: -99, - defaultGraphicUnit: -99, - note: '', - cumulative: false, - cumulativeReset: false, - cumulativeResetStart: '', - cumulativeResetEnd: '', - endOnlyTime: false, - readingGap: adminState.defaultMeterReadingGap, - readingVariation: 0, - readingDuplication: 1, - timeSort: MeterTimeSortType.increasing, - reading: 0.0, - startTimestamp: '', - endTimestamp: '', - previousEnd: '', - areaUnit: AreaUnitType.none, - readingFrequency: adminState.defaultMeterReadingFrequency, - minVal: adminState.defaultMeterMinimumValue, - maxVal: adminState.defaultMeterMaximumValue, - minDate: adminState.defaultMeterMinimumDate, - maxDate: adminState.defaultMeterMaximumDate, - maxError: adminState.defaultMeterMaximumErrors, - disableChecks: adminState.defaultMeterDisableChecks - } + const defaultValues = useAppSelector(selectDefaultCreateMeterValues) /* State */ // To make this consistent with EditUnitModalComponent, we don't pass show and close via props @@ -142,43 +102,9 @@ export default function CreateMeterModalComponent() { const [validMeter, setValidMeter] = useState(false); useEffect(() => { - setValidMeter( - meterDetails.name !== '' && - (meterDetails.area === 0 || (meterDetails.area > 0 && meterDetails.areaUnit !== AreaUnitType.none)) && - meterDetails.readingGap >= 0 && - meterDetails.readingVariation >= 0 && - (meterDetails.readingDuplication >= 1 && meterDetails.readingDuplication <= 9) && - meterDetails.readingFrequency !== '' && - meterDetails.unitId !== -99 && - meterDetails.defaultGraphicUnit !== -99 && - meterDetails.meterType !== '' && - meterDetails.minVal >= MIN_VAL && - meterDetails.minVal <= meterDetails.maxVal && - meterDetails.maxVal <= MAX_VAL && - moment(meterDetails.minDate).isValid() && - moment(meterDetails.maxDate).isValid() && - moment(meterDetails.minDate).isSameOrAfter(MIN_DATE_MOMENT) && - moment(meterDetails.minDate).isSameOrBefore(moment(meterDetails.maxDate)) && - moment(meterDetails.maxDate).isSameOrBefore(MAX_DATE_MOMENT) && - (meterDetails.maxError >= 0 && meterDetails.maxError <= MAX_ERRORS) - ); - }, [ - meterDetails.area, - meterDetails.name, - meterDetails.readingGap, - meterDetails.readingVariation, - meterDetails.readingDuplication, - meterDetails.areaUnit, - meterDetails.readingFrequency, - meterDetails.unitId, - meterDetails.defaultGraphicUnit, - meterDetails.meterType, - meterDetails.minVal, - meterDetails.maxVal, - meterDetails.minDate, - meterDetails.maxDate, - meterDetails.maxError - ]); + // Conflicting GPS point type so type assertions + setValidMeter(isValidCreateMeter(meterDetails as unknown as MeterData)); + }, [meterDetails]); /* End State */ // Reset the state to default values @@ -207,8 +133,6 @@ export default function CreateMeterModalComponent() { // TODO Maybe should do as a single popup? - // Set default identifier as name if left blank - meterDetails.identifier = (!meterDetails.identifier || meterDetails.identifier.length === 0) ? meterDetails.name : meterDetails.identifier; // Check GPS entered. // Validate GPS is okay and take from string to GPSPoint to submit. @@ -221,14 +145,13 @@ export default function CreateMeterModalComponent() { if (typeof gpsInput === 'string') { if (isValidGPSInput(gpsInput)) { // Clearly gpsInput is a string but TS complains about the split so cast. - const gpsValues = gpsInput.split(',').map((value: string) => parseFloat(value)); + const gpsValues = gpsInput.split(',').map(value => parseFloat(value)); // It is valid and needs to be in this format for routing. gps = { longitude: gpsValues[longitudeIndex], latitude: gpsValues[latitudeIndex] }; - // gpsInput must be of type string but TS does not think so so cast. - } else if ((gpsInput as string).length !== 0) { + } else if (gpsInput.length !== 0) { // GPS not okay. Only true if some input. // TODO isValidGPSInput currently pops up an alert so not doing it here, may change // so leaving code commented out. @@ -241,9 +164,14 @@ export default function CreateMeterModalComponent() { // The input passed validation. // The default value for timeZone is an empty string but that should be null for DB. // See below for usage of timeZoneValue. - const timeZoneValue = (meterDetails.timeZone == '' ? null : meterDetails.timeZone); // GPS may have been updated so create updated state to submit. - const submitState = { ...meterDetails, gps: gps, timeZone: timeZoneValue }; + const submitState = { + ...meterDetails, + gps: gps, + timeZone: (meterDetails.timeZone == '' ? null : meterDetails.timeZone), + // Set default identifier as name if left blank + identifier: !meterDetails.identifier || meterDetails.identifier.length === 0 ? meterDetails.name : meterDetails.identifier + }; // Submit new meter if checks where ok. // Attempt to add meter to database addMeter(submitState) @@ -841,10 +769,34 @@ export default function CreateMeterModalComponent() { ); } +const isValidCreateMeter = (meterDetails: MeterData) => { + return meterDetails.name !== '' && + (meterDetails.area === 0 || (meterDetails.area > 0 && meterDetails.areaUnit !== AreaUnitType.none)) && + meterDetails.readingGap >= 0 && + meterDetails.readingVariation >= 0 && + (meterDetails.readingDuplication >= 1 && meterDetails.readingDuplication <= 9) && + meterDetails.readingFrequency !== '' && + meterDetails.unitId !== -99 && + meterDetails.defaultGraphicUnit !== -99 && + meterDetails.meterType !== '' && + meterDetails.minVal >= MIN_VAL && + meterDetails.minVal <= meterDetails.maxVal && + meterDetails.maxVal <= MAX_VAL && + moment(meterDetails.minDate).isValid() && + moment(meterDetails.maxDate).isValid() && + moment(meterDetails.minDate).isSameOrAfter(MIN_DATE_MOMENT) && + moment(meterDetails.minDate).isSameOrBefore(moment(meterDetails.maxDate)) && + moment(meterDetails.maxDate).isSameOrBefore(MAX_DATE_MOMENT) && + (meterDetails.maxError >= 0 && meterDetails.maxError <= MAX_ERRORS) +} + + + + const MIN_VAL = Number.MIN_SAFE_INTEGER; const MAX_VAL = Number.MAX_SAFE_INTEGER; const MIN_DATE_MOMENT = moment(0).utc(); const MAX_DATE_MOMENT = moment(0).utc().add(5000, 'years'); const MIN_DATE = MIN_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); const MAX_DATE = MAX_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); -const MAX_ERRORS = 75; \ No newline at end of file +const MAX_ERRORS = 75; diff --git a/src/client/app/components/meters/MeterViewComponentWIP.tsx b/src/client/app/components/meters/MeterViewComponentWIP.tsx index 0cbf56664..c0154e6e0 100644 --- a/src/client/app/components/meters/MeterViewComponentWIP.tsx +++ b/src/client/app/components/meters/MeterViewComponentWIP.tsx @@ -9,10 +9,10 @@ import { Button } from 'reactstrap'; import { MeterData } from 'types/redux/meters'; import { useAppSelector } from '../../redux/hooks'; import { selectGraphicName, selectUnitName } from '../../redux/selectors/adminSelectors'; -import { selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; import '../../styles/card-page.css'; import translate from '../../utils/translate'; import EditMeterModalComponentWIP from './EditMeterModalComponentWIP'; +import { selectIsAdmin } from '../../reducers/currentUser'; interface MeterViewComponentProps { meter: MeterData; @@ -27,7 +27,7 @@ export default function MeterViewComponent(props: MeterViewComponentProps) { // Edit Modal Show const [showEditModal, setShowEditModal] = useState(false); // Check for admin status - const loggedInAsAdmin = useAppSelector(selectIsLoggedInAsAdmin); + const loggedInAsAdmin = useAppSelector(selectIsAdmin); // Set up to display the units associated with the meter as the unit identifier. diff --git a/src/client/app/components/meters/MetersDetailComponent.tsx b/src/client/app/components/meters/MetersDetailComponent.tsx index c30585759..5863a0c36 100644 --- a/src/client/app/components/meters/MetersDetailComponent.tsx +++ b/src/client/app/components/meters/MetersDetailComponent.tsx @@ -7,8 +7,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { useAppSelector } from '../../redux/hooks'; -import { selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; -import { selectVisibleMetersGroupsDataByID } from '../../redux/selectors/dataSelectors'; +import { selectVisibleMeterAndGroupDataByID } from '../../redux/selectors/adminSelectors'; import '../../styles/card-page.css'; import { MeterData } from '../../types/redux/meters'; import { UnitData, UnitType } from '../../types/redux/units'; @@ -17,7 +16,7 @@ import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateMeterModalComponent from './CreateMeterModalComponent'; import MeterViewComponent from './MeterViewComponent'; -import { selectCurrentUser } from '../../reducers/currentUser'; +import { selectCurrentUser, selectIsAdmin } from '../../reducers/currentUser'; import { selectUnitDataById } from '../../redux/api/unitsApi'; /** @@ -29,11 +28,11 @@ export default function MetersDetailComponent() { const currentUserState = useAppSelector(state => selectCurrentUser(state)); // Check for admin status - const isAdmin = useAppSelector(state => selectIsLoggedInAsAdmin(state)); + const isAdmin = useAppSelector(selectIsAdmin); // We only want displayable meters if non-admins because they still have // non-displayable in state. - const { visibleMeters } = useAppSelector(state => selectVisibleMetersGroupsDataByID(state)); + const { visibleMeters } = useAppSelector(state => selectVisibleMeterAndGroupDataByID(state)); // Units state const unitDataById = useAppSelector(selectUnitDataById); diff --git a/src/client/app/components/meters/MetersDetailComponentWIP.tsx b/src/client/app/components/meters/MetersDetailComponentWIP.tsx index 7847d034f..aeac7d081 100644 --- a/src/client/app/components/meters/MetersDetailComponentWIP.tsx +++ b/src/client/app/components/meters/MetersDetailComponentWIP.tsx @@ -6,8 +6,8 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { useAppSelector } from '../../redux/hooks'; -import { selectIsLoggedInAsAdmin } from '../../redux/selectors/authSelectors'; -import { selectVisibleMetersGroupsDataByID } from '../../redux/selectors/dataSelectors'; +import { selectIsAdmin } from '../../reducers/currentUser'; +import { selectVisibleMeterAndGroupDataByID } from '../../redux/selectors/adminSelectors'; import '../../styles/card-page.css'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateMeterModalComponentWIP from './CreateMeterModalComponentWIP'; @@ -20,10 +20,10 @@ import MeterViewComponentWIP from './MeterViewComponentWIP'; export default function MetersDetailComponent() { // Check for admin status - const isAdmin = useAppSelector(state => selectIsLoggedInAsAdmin(state)); + const isAdmin = useAppSelector(state => selectIsAdmin(state)); // We only want displayable meters if non-admins because they still have // non-displayable in state. - const { visibleMeters } = useAppSelector(selectVisibleMetersGroupsDataByID); + const { visibleMeters } = useAppSelector(selectVisibleMeterAndGroupDataByID); return (
diff --git a/src/client/app/containers/CompareChartContainer.ts b/src/client/app/containers/CompareChartContainer.ts index 15ef45513..6f118fe71 100644 --- a/src/client/app/containers/CompareChartContainer.ts +++ b/src/client/app/containers/CompareChartContainer.ts @@ -48,7 +48,7 @@ function mapStateToProps(state: RootState, ownProps: CompareChartContainerProps) const graphingUnit = state.graph.selectedUnit; // This container is not called if there is no data of there are not units so this is safe. const unitDataById = selectUnitDataById(state) - const meterDataById = selectMeterDataById(state) + const meterDataById = selectMeterDataById(state) const groupDataById = selectGroupDataById(state) const selectUnitState = unitDataById[graphingUnit]; let unitLabel: string = ''; @@ -197,13 +197,15 @@ function mapStateToProps(state: RootState, ownProps: CompareChartContainerProps) yaxis: { title: unitLabel, showgrid: true, - gridcolor: '#ddd' + gridcolor: '#ddd', + fixedrange: true }, xaxis: { title: `${xTitle}`, showgrid: false, gridcolor: '#ddd', - automargin: true + automargin: true, + fixedrange: true }, margin: { t: 20, diff --git a/src/client/app/initScript.ts b/src/client/app/initScript.ts index 32bb0d747..18b28c181 100644 --- a/src/client/app/initScript.ts +++ b/src/client/app/initScript.ts @@ -50,8 +50,5 @@ export const initializeApp = async () => { // Request meter/group/details store.dispatch(metersApi.endpoints.getMeters.initiate()) store.dispatch(groupsApi.endpoints.getGroups.initiate()) - await store.dispatch(preferencesApi.util.getRunningQueryThunk('getPreferences', undefined)) - store.dispatch(appStateSlice.actions.updateHistory(store.getState().graph)) - store.dispatch(appStateSlice.actions.setInitComplete(true)) } diff --git a/src/client/app/reducers/currentUser.ts b/src/client/app/reducers/currentUser.ts index e12a12b7c..e43357177 100644 --- a/src/client/app/reducers/currentUser.ts +++ b/src/client/app/reducers/currentUser.ts @@ -3,10 +3,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; +import { createSelector, createSlice } from '@reduxjs/toolkit'; import { authApi } from '../redux/api/authApi'; import { userApi } from '../redux/api/userApi'; -import { User } from '../types/items'; +import { User, UserRole } from '../types/items'; import { CurrentUserState } from '../types/redux/currentUser'; import { setToken } from '../utils/token'; @@ -58,4 +58,14 @@ export const currentUserSlice = createSlice({ } }) -export const { selectCurrentUser } = currentUserSlice.selectors \ No newline at end of file +export const { selectCurrentUser } = currentUserSlice.selectors + +// Memoized Selectors for stable obj reference from derived Values +export const selectIsAdmin = createSelector( + selectCurrentUser, + currentUser => { + // True of token in state, and has Admin Role. + // Token If token is in state, it has been validated upon app initialization, or by login verification + return (currentUser.token && currentUser.profile?.role === UserRole.ADMIN) as boolean + } +) diff --git a/src/client/app/reducers/graph.ts b/src/client/app/reducers/graph.ts index b79711ec0..a4169c072 100644 --- a/src/client/app/reducers/graph.ts +++ b/src/client/app/reducers/graph.ts @@ -236,21 +236,20 @@ export const graphSlice = createSlice({ selectGraphState: state => state, selectThreeDState: state => state.threeD, selectBarWidthDays: state => state.barDuration, + selectSelectedUnit: state => state.selectedUnit, + selectAreaUnit: state => state.selectedAreaUnit, + selectChartToRender: state => state.chartToRender, + selectLineGraphRate: state => state.lineGraphRate, + selectComparePeriod: state => state.comparePeriod, selectSelectedMeters: state => state.selectedMeters, selectSelectedGroups: state => state.selectedGroups, + selectSortingOrder: state => state.compareSortingOrder, selectQueryTimeInterval: state => state.queryTimeInterval, - selectGraphUnitID: state => state.selectedUnit, - selectGraphAreaNormalization: state => state.areaNormalization, - selectChartToRender: state => state.chartToRender, selectThreeDMeterOrGroup: state => state.threeD.meterOrGroup, + selectCompareTimeInterval: state => state.compareTimeInterval, + selectGraphAreaNormalization: state => state.areaNormalization, selectThreeDMeterOrGroupID: state => state.threeD.meterOrGroupID, - selectThreeDReadingInterval: state => state.threeD.readingInterval, - selectLineGraphRate: state => state.lineGraphRate, - selectAreaUnit: state => state.selectedAreaUnit, - selectSortingOrder: state => state.compareSortingOrder, - selectSelectedUnit: state => state.selectedUnit, - selectComparePeriod: state => state.comparePeriod, - selectCompareTimeInterval: state => state.compareTimeInterval + selectThreeDReadingInterval: state => state.threeD.readingInterval } }) @@ -262,7 +261,6 @@ export const { selectSelectedMeters, selectSelectedGroups, selectQueryTimeInterval, - selectGraphUnitID, selectGraphAreaNormalization, selectChartToRender, selectThreeDMeterOrGroup, diff --git a/src/client/app/redux/api/groupsApi.ts b/src/client/app/redux/api/groupsApi.ts index 92bc49137..fd2250528 100644 --- a/src/client/app/redux/api/groupsApi.ts +++ b/src/client/app/redux/api/groupsApi.ts @@ -1,7 +1,7 @@ import * as _ from 'lodash'; import { GroupChildren, GroupData, GroupDataByID } from '../../types/redux/groups'; import { baseApi } from './baseApi'; -import { selectIsLoggedInAsAdmin } from '../selectors/authSelectors'; +import { selectIsAdmin } from '../../reducers/currentUser'; import { RootState } from '../../store'; import { CompareReadings } from 'types/readings'; import { TimeInterval } from '../../../../common/TimeInterval'; @@ -24,7 +24,7 @@ export const groupsApi = baseApi.injectEndpoints({ try { await api.queryFulfilled const state = api.getState() as RootState - const isAdmin = selectIsLoggedInAsAdmin(state) + const isAdmin = selectIsAdmin(state) // if user is an admin, automatically fetch allGroupChildren and update the if (isAdmin) { const { data = [] } = await api.dispatch(groupsApi.endpoints.getAllGroupsChildren.initiate(undefined)) diff --git a/src/client/app/redux/api/readingsApi.ts b/src/client/app/redux/api/readingsApi.ts index ad938e28e..bd6e8b04d 100644 --- a/src/client/app/redux/api/readingsApi.ts +++ b/src/client/app/redux/api/readingsApi.ts @@ -1,5 +1,5 @@ import * as _ from 'lodash'; -import { BarReadingApiArgs, CompareReadingApiArgs, LineReadingApiArgs, ThreeDReadingApiArgs } from '../../redux/selectors/dataSelectors'; +import { BarReadingApiArgs, CompareReadingApiArgs, LineReadingApiArgs, ThreeDReadingApiArgs } from '../selectors/chartQuerySelectors'; import { RootState } from '../../store'; import { BarReadings, CompareReadings, LineReadings, ThreeDReading } from '../../types/readings'; import { baseApi } from './baseApi'; diff --git a/src/client/app/redux/componentHooks.ts b/src/client/app/redux/componentHooks.ts index 4d0d956eb..29102b184 100644 --- a/src/client/app/redux/componentHooks.ts +++ b/src/client/app/redux/componentHooks.ts @@ -3,12 +3,12 @@ import { groupsApi } from './api/groupsApi'; import { metersApi } from './api/metersApi'; import { readingsApi } from './api/readingsApi'; import { useAppSelector } from './hooks'; -import { selectChartQueryArgs } from './selectors/dataSelectors'; +import { selectAllChartQueryArgs } from './selectors/chartQuerySelectors'; import { unitsApi } from './api/unitsApi'; // General purpose custom hook mostly useful for Select component loadingIndicators, and current graph loading state(s) export const useFetchingStates = () => { - const queryArgs = useAppSelector(state => selectChartQueryArgs(state)); + const queryArgs = useAppSelector(state => selectAllChartQueryArgs(state)); const { isFetching: meterLineIsFetching, isLoading: meterLineIsLoading } = readingsApi.endpoints.line.useQueryState(queryArgs.line.meterArgs); const { isFetching: groupLineIsFetching, isLoading: groupLineIsLoading } = readingsApi.endpoints.line.useQueryState(queryArgs.line.groupArgs); const { isFetching: meterBarIsFetching, isLoading: meterBarIsLoading } = readingsApi.endpoints.bar.useQueryState(queryArgs.bar.meterArgs); @@ -38,35 +38,5 @@ export const useFetchingStates = () => { metersFetching || groupsFetching || unitsIsFetching - } - // Since we're deriving data, we can useMemo() for stable references. - // const fetchInfo = React.useMemo(() => ({ - // endpointsFetchingData: { - // meterLineIsLoading, - // groupLineIsLoading, - // meterBarIsLoading, - // groupBarIsLoading, - // threeDIsLoading, - // metersLoading, - // groupsLoading, - // unitsIsLoading - // }, - // somethingIsFetching: meterLineIsLoading || - // groupLineIsLoading || - // meterBarIsLoading || - // groupBarIsLoading || - // threeDIsLoading || - // metersLoading || - // groupsLoading || - // unitsIsLoading - - // } - // ), [ - // meterLineIsLoading, groupLineIsLoading, - // meterBarIsLoading, groupBarIsLoading, - // threeDIsLoading, metersLoading, - // groupsLoading, unitsIsLoading - // ]) - } diff --git a/src/client/app/redux/selectors/adminSelectors.ts b/src/client/app/redux/selectors/adminSelectors.ts index e206fb6e4..a560bb737 100644 --- a/src/client/app/redux/selectors/adminSelectors.ts +++ b/src/client/app/redux/selectors/adminSelectors.ts @@ -2,16 +2,19 @@ import { createSelector } from '@reduxjs/toolkit' import * as _ from 'lodash' import { selectAdminState } from '../../reducers/admin' import { selectConversionsDetails } from '../../redux/api/conversionsApi' -import { selectMeterDataWithID } from '../../redux/api/metersApi' +import { selectGroupDataById } from '../../redux/api/groupsApi' +import { selectMeterDataById, selectMeterDataWithID } from '../../redux/api/metersApi' import { RootState } from '../../store' import { PreferenceRequestItem } from '../../types/items' +import { ConversionData } from '../../types/redux/conversions' +import { MeterData, MeterTimeSortType } from '../../types/redux/meters' import { UnitData, UnitType } from '../../types/redux/units' import { unitsCompatibleWithUnit } from '../../utils/determineCompatibleUnits' +import { AreaUnitType } from '../../utils/getAreaUnitConversion' import { noUnitTranslated, potentialGraphicUnits } from '../../utils/input' import translate from '../../utils/translate' import { selectUnitDataById } from '../api/unitsApi' -import { MeterData } from 'types/redux/meters' -import { ConversionData } from 'types/redux/conversions' +import { selectVisibleMetersAndGroups } from './authVisibilitySelectors' export const selectAdminPreferences = createSelector( selectAdminState, @@ -122,13 +125,12 @@ export const selectGraphicName = createSelector( * useAppSelector(state => selectGraphicUnitCompatibility(state, localMeterEdits.unitId, localMeterEdits.defaultGraphicUnit)) */ export const makeSelectGraphicUnitCompatibility = () => { - // 3rd/4th callback used to pass in non-state value in this case the local edits. - // two separate call backs so their return values will pass a === equality check for memoized behavior const selectGraphicUnitCompatibilityInstance = createSelector( selectPossibleGraphicUnits, selectPossibleMeterUnits, - (_state: RootState, meterDetails: MeterData) => meterDetails, - (possibleGraphicUnits, possibleMeterUnits, { unitId, defaultGraphicUnit }) => { + (_state: RootState, meterDetails: MeterData) => meterDetails.unitId, + (_state: RootState, meterDetails: MeterData) => meterDetails.defaultGraphicUnit, + (possibleGraphicUnits, possibleMeterUnits, unitId, defaultGraphicUnit) => { // Graphic units compatible with currently selected unit const compatibleGraphicUnits = new Set(); // Graphic units incompatible with currently selected unit @@ -201,8 +203,11 @@ export const makeSelectGraphicUnitCompatibility = () => { export const selectIsValidConversion = createSelector( selectUnitDataById, selectConversionsDetails, - (_state: RootState, conversionData: ConversionData) => conversionData, - (unitDataById, conversionData, { sourceId, destinationId, bidirectional }): [boolean, string] => { + (_state: RootState, conversionDetails: ConversionData) => conversionDetails.sourceId, + (_state: RootState, conversionDetails: ConversionData) => conversionDetails.destinationId, + (_state: RootState, conversionDetails: ConversionData) => conversionDetails.bidirectional, + (unitDataById, conversions, sourceId, destinationId, bidirectional): [boolean, string] => { + console.log('Validating Conversion Details!') /* Create Conversion Validation: Source equals destination: invalid conversion Conversion exists: invalid conversion @@ -213,6 +218,8 @@ export const selectIsValidConversion = createSelector( Cannot mix unit represent TODO Some of these can go away when we make the menus dynamic. */ + // console.log(sourceId, destinationId, bidirectional) + // The destination cannot be a meter unit. if (destinationId !== -999 && unitDataById[destinationId].typeOfUnit === UnitType.meter) { // notifyUser(translate('conversion.create.destination.meter')); @@ -220,15 +227,15 @@ export const selectIsValidConversion = createSelector( } // Source or destination not set - if (sourceId === -999 || destinationId === -999) { + if (sourceId == -999 || destinationId == -999) { // TODO Translate Me! return [false, 'Source or destination not set'] } // Conversion already exists - if ((conversionData.findIndex(conversionData => (( - conversionData.sourceId === sourceId) && - conversionData.destinationId === destinationId))) !== -1) { + if ((conversions.findIndex(conversion => (( + conversion.sourceId === sourceId) && + conversion.destinationId === destinationId))) !== -1) { // notifyUser(translate('conversion.create.exists')); return [false, translate('conversion.create.exists')]; } @@ -241,28 +248,81 @@ export const selectIsValidConversion = createSelector( } + console.log('Seems to Break about here!') let isValid = true; // Loop over conversions and check for existence of inverse of conversion passed in // If there exists an inverse that is bidirectional, then there is no point in making a conversion since it is essentially a duplicate. // If there is a non bidirectional inverse, then it is a valid conversion - Object.values(conversionData).forEach(conversion => { + + Object.values(conversions).forEach(conversion => { + // Do not allow for a bidirectional conversion with an inverse that is not bidirectional // Inverse exists - if ((conversion.sourceId === destinationId) && (conversion.destinationId === sourceId)) { - // Inverse is bidirectional - if (conversion.bidirectional) { - isValid = false; - } - // Inverse is not bidirectional - else { - // Do not allow for a bidirectional conversion with an inverse that is not bidirectional - if (bidirectional) { - // The new conversion is bidirectional - isValid = false; - } - } + if ((conversion.sourceId === destinationId) && (conversion.destinationId === sourceId) + && + // Inverse is bidirectional or new conversion is bidirectional + (conversion.bidirectional || bidirectional)) { + isValid = false; } }); - return !isValid ? [false, translate('conversion.create.exists.inverse')] : [isValid, 'Conversion is Valid'] + console.log('Conversion never seems to get here? ') + return isValid ? [isValid, 'Conversion is Valid'] : [false, translate('conversion.create.exists.inverse')] + } +) + +export const selectVisibleMeterAndGroupDataByID = createSelector( + selectVisibleMetersAndGroups, + selectMeterDataById, + selectGroupDataById, + (visible, meterDataById, groupDataById) => { + const visibleMeters = Object.values(meterDataById).filter(meterData => visible.meters.has(meterData.id)) + const visibleGroups = Object.values(groupDataById).filter(groupData => visible.groups.has(groupData.id)) + return { visibleMeters, visibleGroups } } -) \ No newline at end of file +) + +export const selectDefaultCreateMeterValues = createSelector( + selectAdminPreferences, + adminPreferences => { + const defaultValues = { + id: -99, + identifier: '', + name: '', + area: 0, + enabled: false, + displayable: false, + meterType: '', + url: '', + timeZone: '', + gps: '', + // Defaults of -999 (not to be confused with -99 which is no unit) + // Purely for allowing the default select to be "select a ..." + unitId: -99, + defaultGraphicUnit: -99, + note: '', + cumulative: false, + cumulativeReset: false, + cumulativeResetStart: '', + cumulativeResetEnd: '', + endOnlyTime: false, + readingGap: adminPreferences.defaultMeterReadingGap, + readingVariation: 0, + readingDuplication: 1, + timeSort: MeterTimeSortType.increasing, + reading: 0.0, + startTimestamp: '', + endTimestamp: '', + previousEnd: '', + areaUnit: AreaUnitType.none, + readingFrequency: adminPreferences.defaultMeterReadingFrequency, + minVal: adminPreferences.defaultMeterMinimumValue, + maxVal: adminPreferences.defaultMeterMaximumValue, + minDate: adminPreferences.defaultMeterMinimumDate, + maxDate: adminPreferences.defaultMeterMaximumDate, + maxError: adminPreferences.defaultMeterMaximumErrors, + disableChecks: adminPreferences.defaultMeterDisableChecks + } + + return defaultValues + } +) diff --git a/src/client/app/redux/selectors/authSelectors.ts b/src/client/app/redux/selectors/authSelectors.ts deleted file mode 100644 index 92a34e4e0..000000000 --- a/src/client/app/redux/selectors/authSelectors.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { UserRole } from '../../types/items'; -import { selectCurrentUser } from '../../reducers/currentUser' - -// Memoized Selectors for stable obj reference from derived Values -export const selectIsLoggedInAsAdmin = createSelector( - selectCurrentUser, - currentUser => { - // True of token in state, and has Admin Role. - // Token If token is in state, it has been validated upon app initialization, or by login verification - // Type looked weird without boolean - return (currentUser.token && currentUser.profile?.role === UserRole.ADMIN) as boolean - } -) diff --git a/src/client/app/redux/selectors/authVisibilitySelectors.ts b/src/client/app/redux/selectors/authVisibilitySelectors.ts new file mode 100644 index 000000000..88e8b7370 --- /dev/null +++ b/src/client/app/redux/selectors/authVisibilitySelectors.ts @@ -0,0 +1,52 @@ +import { createSelector } from '@reduxjs/toolkit'; +import * as _ from 'lodash'; +import { selectGroupDataById } from '../api/groupsApi'; +import { selectMeterDataById } from '../api/metersApi'; +import { selectUnitDataById } from '../api/unitsApi'; +import { DisplayableType, UnitType } from '../../types/redux/units'; +import { selectIsAdmin } from '../../reducers/currentUser'; + + +export const selectVisibleMetersAndGroups = createSelector( + selectMeterDataById, + selectGroupDataById, + selectIsAdmin, + (meterDataByID, groupDataById, isAdmin) => { + // Holds all meters visible to the user + const meters = new Set(); + const groups = new Set(); + Object.values(meterDataByID) + .forEach(meter => { + if (isAdmin || meter.displayable) { + meters.add(meter.id); + } + }); + Object.values(groupDataById) + .forEach(group => { + if (isAdmin || group.displayable) { + groups.add(group.id); + } + }); + return { meters, groups } + } +); + +/** + * Filters all units that are of type meter or displayable type none from the redux state, as well as admin only units if the user is not an admin. + * @param state - current redux state + * @returns an array of UnitData + */ +export const selectVisibleUnitOrSuffixState = createSelector( + selectUnitDataById, + selectIsAdmin, + (unitDataById, isAdmin) => { + const visibleUnitsOrSuffixes = _.filter(unitDataById, data => + (data.typeOfUnit == UnitType.unit || data.typeOfUnit == UnitType.suffix) + && + (isAdmin + ? data.displayable != DisplayableType.none + : data.displayable == DisplayableType.all) + ); + return visibleUnitsOrSuffixes; + } +) \ No newline at end of file diff --git a/src/client/app/redux/selectors/chartQuerySelectors.ts b/src/client/app/redux/selectors/chartQuerySelectors.ts new file mode 100644 index 000000000..61145ddea --- /dev/null +++ b/src/client/app/redux/selectors/chartQuerySelectors.ts @@ -0,0 +1,186 @@ +import { createSelector } from '@reduxjs/toolkit'; +import * as moment from 'moment'; +import { RootState } from 'store'; +import { TimeInterval } from '../../../../common/TimeInterval'; +import { + selectBarWidthDays, selectComparePeriod, + selectCompareTimeInterval, selectQueryTimeInterval, + selectSelectedGroups, selectSelectedMeters, + selectSelectedUnit, selectThreeDState +} from '../../reducers/graph'; +import { MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; +import { calculateCompareShift } from '../../utils/calculateCompare'; +import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; + +// query args that 'most' graphs share +export interface commonQueryArgs { + ids: number[]; + timeInterval: string; + unitID: number; + meterOrGroup: MeterOrGroup; +} + +// endpoint specific args +export interface LineReadingApiArgs extends commonQueryArgs { } +export interface BarReadingApiArgs extends commonQueryArgs { barWidthDays: number } + +export interface ThreeDReadingApiArgs extends Omit { id: number, readingInterval: ReadingInterval } +export interface CompareReadingApiArgs extends Omit { + // compare breaks the timeInterval pattern query pattern therefore omit and add required for api. + shift: string, + curr_start: string, + curr_end: string +} +// Maps uses the Bar Endpoint so just use its args for simplicity, however barWidthDays should be durationDays +export interface MapReadingApiArgs extends BarReadingApiArgs { } + + +export const selectCommonQueryArgs = createSelector( + selectSelectedMeters, + selectSelectedGroups, + selectQueryTimeInterval, + selectSelectedUnit, + (selectedMeters, selectedGroups, queryTimeInterval, selectedUnit) => { + // args that 'most' meters queries share + const meterArgs = { + ids: selectedMeters, + timeInterval: queryTimeInterval.toString(), + unitID: selectedUnit, + meterOrGroup: MeterOrGroup.meters + } + + // args that 'most' groups queries share + const groupArgs = { + ids: selectedGroups, + timeInterval: queryTimeInterval.toString(), + unitID: selectedUnit, + meterOrGroup: MeterOrGroup.groups + } + const meterSkip = !meterArgs.ids.length; + const groupSkip = !groupArgs.ids.length; + + return { meterArgs, groupArgs, meterSkip, groupSkip } + } +) + +export const selectLineChartQueryArgs = createSelector( + selectCommonQueryArgs, + common => { + // Args to pass into the line chart component + const meterArgs: LineReadingApiArgs = common.meterArgs; + const groupArgs: LineReadingApiArgs = common.groupArgs; + const meterShouldSkip = common.meterSkip; + const groupShouldSkip = common.groupSkip; + return { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } + } +) + +export const selectBarChartQueryArgs = createSelector( + selectCommonQueryArgs, + selectBarWidthDays, + (common, barWidthDays) => { + // QueryArguments to pass into the bar chart component + const barWidthAsDays = Math.round(barWidthDays.asDays()) + const meterArgs: BarReadingApiArgs = { + ...common.meterArgs, + barWidthDays: barWidthAsDays + }; + const groupArgs: BarReadingApiArgs = { + ...common.groupArgs, + barWidthDays: barWidthAsDays + }; + const meterShouldSkip = common.meterSkip; + const groupShouldSkip = common.groupSkip; + return { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } + } +) + +export const selectCompareChartQueryArgs = createSelector( + selectCommonQueryArgs, + selectComparePeriod, + selectCompareTimeInterval, + (common, comparePeriod, compareTimeInterval) => { + const compareArgs = { + shift: calculateCompareShift(comparePeriod).toISOString(), + curr_start: compareTimeInterval.getStartTimestamp()?.toISOString(), + curr_end: compareTimeInterval.getEndTimestamp()?.toISOString() + } + const meterArgs: CompareReadingApiArgs = { + ...common.meterArgs, + ...compareArgs + + } + const groupArgs: CompareReadingApiArgs = { + ...common.groupArgs, + ...compareArgs + } + const meterShouldSkip = common.meterSkip; + const groupShouldSkip = common.groupSkip; + return { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } + } +) + +export const selectMapChartQueryArgs = createSelector( + selectBarChartQueryArgs, + selectQueryTimeInterval, + (state: RootState) => state.maps, + (barChartArgs, queryTimeInterval, maps) => { + const durationDays = Math.round(( + queryTimeInterval.equals(TimeInterval.unbounded()) + ? moment.duration(4, 'weeks') + : moment.duration(queryTimeInterval.duration('days'), 'days') + ).asDays()) + + const meterArgs: MapReadingApiArgs = { + ...barChartArgs.meterArgs, + // Maps uses the Bar Endpoint so just use its args for simplicity, however barWidthDays should be durationDays + barWidthDays: durationDays + } + const groupArgs: MapReadingApiArgs = { + ...barChartArgs.groupArgs, + // Maps uses the Bar Endpoint so just use its args for simplicity, however barWidthDays should be durationDays + barWidthDays: durationDays + + } + const meterShouldSkip = barChartArgs.meterShouldSkip || maps.selectedMap === 0 + const groupShouldSkip = barChartArgs.groupShouldSkip || maps.selectedMap === 0 + return { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } + } + +) + + +// Selector prepares the query args for ALL graph endpoints based on the current graph slice state +// TODO Break down into individual selectors? +// Verify if prop drilling is a better pattern vs useSelector in same sameComponent +export const selectThreeDQueryArgs = createSelector( + selectQueryTimeInterval, + selectSelectedUnit, + selectThreeDState, + (queryTimeInterval, selectedUnit, threeD) => { + const args: ThreeDReadingApiArgs = { + id: threeD.meterOrGroupID!, + timeInterval: roundTimeIntervalForFetch(queryTimeInterval).toString(), + unitID: selectedUnit, + readingInterval: threeD.readingInterval, + meterOrGroup: threeD.meterOrGroup! + } + const shouldSkipQuery = !threeD.meterOrGroupID || !queryTimeInterval.getIsBounded() + return { args, shouldSkipQuery } + } +) + +export const selectAllChartQueryArgs = createSelector( + selectLineChartQueryArgs, + selectBarChartQueryArgs, + selectCompareChartQueryArgs, + selectMapChartQueryArgs, + selectThreeDQueryArgs, + (line, bar, compare, map, threeD) => ({ + line, + bar, + compare, + map, + threeD + }) +) \ No newline at end of file diff --git a/src/client/app/redux/selectors/dataSelectors.ts b/src/client/app/redux/selectors/dataSelectors.ts deleted file mode 100644 index 2eb3ee6eb..000000000 --- a/src/client/app/redux/selectors/dataSelectors.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import * as _ from 'lodash'; -import * as moment from 'moment'; -import { TimeInterval } from '../../../../common/TimeInterval'; -import { selectBarWidthDays, selectComparePeriod, selectCompareTimeInterval, selectGraphState, selectQueryTimeInterval } from '../../reducers/graph'; -import { MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; -import { calculateCompareShift } from '../../utils/calculateCompare'; -import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; -import * as moment from 'moment'; -import { TimeInterval } from '../../../../common/TimeInterval'; -import { selectBarWidthDays, selectComparePeriod, selectCompareTimeInterval, selectGraphState, selectQueryTimeInterval } from '../../reducers/graph'; -import { MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; -import { calculateCompareShift } from '../../utils/calculateCompare'; -import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; -import { selectGroupDataById } from '../api/groupsApi'; -import { selectMeterDataById } from '../api/metersApi'; -import { selectIsLoggedInAsAdmin } from './authSelectors'; -import { RootState } from 'store'; - -// TODO DUPLICATE SELECTOR? UI SELECTOR MAY CONTAIN SAME LOGIC, CONSOLIDATE IF POSSIBLE? -export const selectVisibleMetersGroupsDataByID = createSelector( - selectMeterDataById, - selectGroupDataById, - selectIsLoggedInAsAdmin, - (meterDataById, groupDataById, isAdmin) => { - let visibleMeters; - let visibleGroups; - if (isAdmin) { - visibleMeters = meterDataById - visibleGroups = groupDataById; - } else { - visibleMeters = _.filter(meterDataById, meter => meter.displayable); - visibleGroups = _.filter(groupDataById, group => group.displayable); - } - - return { visibleMeters, visibleGroups } - } -) - -// query args that 'most' graphs share -export interface commonArgsMultiID { - ids: number[]; - timeInterval: string; - unitID: number; - meterOrGroup: MeterOrGroup; -} -export interface commonArgsSingleID extends Omit { id: number } - -// endpoint specific args -export interface LineReadingApiArgs extends commonArgsMultiID { } -export interface BarReadingApiArgs extends commonArgsMultiID { barWidthDays: number } - -export interface ThreeDReadingApiArgs extends commonArgsSingleID { readingInterval: ReadingInterval } -export interface CompareReadingApiArgs extends Omit { - // compare breaks the timeInterval pattern query pattern therefore omit and add required for api. - shift: string, - curr_start: string, - curr_end: string -} -// Maps uses the Bar Endpoint so just use its args for simplicity, however barWidthDays should be durationDays -export interface MapReadingApiArgs extends BarReadingApiArgs { } - - -export const selectCommonQueryArgs = createSelector( - selectGraphState, - graphState => { - const queryTimeInterval = graphState.queryTimeInterval - // args that 'most' meters queries share - const commonMeterArgs: commonArgsMultiID = { - ids: graphState.selectedMeters, - timeInterval: queryTimeInterval.toString(), - unitID: graphState.selectedUnit, - meterOrGroup: MeterOrGroup.meters - } - - // args that 'most' groups queries share - const commonGroupArgs: commonArgsMultiID = { - ids: graphState.selectedGroups, - timeInterval: queryTimeInterval.toString(), - unitID: graphState.selectedUnit, - meterOrGroup: MeterOrGroup.groups - } - - return { commonMeterArgs, commonGroupArgs } - } -) - -export const selectLineChartQueryArgs = createSelector( - selectCommonQueryArgs, - ({ commonMeterArgs, commonGroupArgs }) => { - // props to pass into the line chart component - - const meterArgs: LineReadingApiArgs = commonMeterArgs; - const groupArgs: LineReadingApiArgs = commonGroupArgs; - const meterShouldSkip = !commonMeterArgs.ids.length; - const groupShouldSkip = !commonGroupArgs.ids.length; - return { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } - } -) - -export const selectBarChartQueryArgs = createSelector( - selectCommonQueryArgs, - selectBarWidthDays, - ({ commonMeterArgs, commonGroupArgs }, barWidthDays) => { - // props to pass into the line chart component - - const meterArgs: BarReadingApiArgs = { - ...commonMeterArgs, - barWidthDays: Math.round(barWidthDays.asDays()) - - }; - const groupArgs: BarReadingApiArgs = { - ...commonGroupArgs, - barWidthDays: Math.round(barWidthDays.asDays()) - }; - const meterShouldSkip = !commonMeterArgs.ids.length; - const groupShouldSkip = !commonGroupArgs.ids.length; - return { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } - } -) - -export const selectCompareChartQueryArgs = createSelector( - selectCommonQueryArgs, - selectComparePeriod, - selectCompareTimeInterval, - ({ commonMeterArgs, commonGroupArgs }, comparePeriod, compareTimeInterval) => { - const meterArgs: CompareReadingApiArgs = { - ...commonMeterArgs, - shift: calculateCompareShift(comparePeriod).toISOString(), - curr_start: compareTimeInterval.getStartTimestamp()?.toISOString(), - curr_end: compareTimeInterval.getEndTimestamp()?.toISOString() - } - const groupArgs: CompareReadingApiArgs = { - ...commonGroupArgs, - shift: calculateCompareShift(comparePeriod).toISOString(), - curr_start: compareTimeInterval.getStartTimestamp()?.toISOString(), - curr_end: compareTimeInterval.getEndTimestamp()?.toISOString() - } - const meterShouldSkip = !commonMeterArgs.ids.length; - const groupShouldSkip = !commonGroupArgs.ids.length; - return { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } - } -) - -export const selectMapChartQueryArgs = createSelector( - selectBarChartQueryArgs, - selectQueryTimeInterval, - (state: RootState) => state.maps, - (barChartArgs, queryTimeInterval, maps) => { - - const meterArgs: MapReadingApiArgs = { - ...barChartArgs.meterArgs, - // Maps uses the Bar Endpoint so just use its args for simplicity, however barWidthDays should be durationDays - barWidthDays: Math.round(((queryTimeInterval.equals(TimeInterval.unbounded())) - ? moment.duration(4, 'weeks') - : moment.duration(queryTimeInterval.duration('days'), 'days')).asDays()) - } - const groupArgs: MapReadingApiArgs = { - ...barChartArgs.groupArgs, - // Maps uses the Bar Endpoint so just use its args for simplicity, however barWidthDays should be durationDays - barWidthDays: Math.round(((queryTimeInterval.equals(TimeInterval.unbounded())) - ? moment.duration(4, 'weeks') - : moment.duration(queryTimeInterval.duration('days'), 'days')).asDays() - ) - } - const meterShouldSkip = barChartArgs.meterShouldSkip || maps.selectedMap === 0 - const groupShouldSkip = barChartArgs.groupShouldSkip || maps.selectedMap === 0 - return { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } - } - -) - - -// Selector prepares the query args for ALL graph endpoints based on the current graph slice state -// TODO Break down into individual selectors? -// Verify if prop drilling is a better pattern vs useSelector in same sameComponent -export const selectThreeDQueryArgs = createSelector( - selectGraphState, - graphState => { - const queryTimeInterval = graphState.queryTimeInterval - const args: ThreeDReadingApiArgs = { - id: graphState.threeD.meterOrGroupID!, - timeInterval: roundTimeIntervalForFetch(queryTimeInterval).toString(), - unitID: graphState.selectedUnit, - readingInterval: graphState.threeD.readingInterval, - meterOrGroup: graphState.threeD.meterOrGroup! - } - const shouldSkipQuery = !graphState.threeD.meterOrGroupID || !queryTimeInterval.getIsBounded() - return { args, shouldSkipQuery } - } -) - -export const selectChartQueryArgs = createSelector( - selectLineChartQueryArgs, - selectBarChartQueryArgs, - selectCompareChartQueryArgs, - selectMapChartQueryArgs, - selectThreeDQueryArgs, - (line, bar, compare, map, threeD) => ({ - line, - bar, - compare, - map, - threeD - }) -) \ No newline at end of file diff --git a/src/client/app/redux/selectors/selectors.ts b/src/client/app/redux/selectors/selectors.ts new file mode 100644 index 000000000..cd92bcfe9 --- /dev/null +++ b/src/client/app/redux/selectors/selectors.ts @@ -0,0 +1,8 @@ +import { + createSelectorCreator, + weakMapMemoize, + unstable_autotrackMemoize as autoTrackMemoize +} from 'reselect' + +export const createAutoTrackSelector = createSelectorCreator(autoTrackMemoize); +export const createWeakmapSelector = createSelectorCreator(weakMapMemoize); \ 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 130c437f3..89f5e4791 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -8,7 +8,7 @@ import { selectMapState } from '../../reducers/maps'; import { DataType } from '../../types/Datasources'; import { GroupedOption, SelectOption } from '../../types/items'; import { ChartTypes, MeterOrGroup } from '../../types/redux/graph'; -import { DisplayableType, UnitRepresentType, UnitType } from '../../types/redux/units'; +import { UnitDataById, UnitRepresentType } from '../../types/redux/units'; import { CartesianPoint, Dimensions, calculateScaleFromEndpoints, gpsToUserGrid, itemDisplayableOnMap, itemMapInfoOk, normalizeImageDimensions @@ -16,59 +16,27 @@ import { import { metersInGroup, unitsCompatibleWithMeters } from '../../utils/determineCompatibleUnits'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; -import { selectCurrentUser } from '../../reducers/currentUser'; import { - selectChartToRender, selectGraphAreaNormalization, selectGraphUnitID, - selectQueryTimeInterval, selectSelectedGroups, selectSelectedMeters + selectChartToRender, selectGraphAreaNormalization, + selectSelectedGroups, selectSelectedMeters, + selectSelectedUnit } from '../../reducers/graph'; import { selectGroupDataById } from '../../redux/api/groupsApi'; -import { selectMeterDataById } from '../api/metersApi'; import { selectUnitDataById } from '../../redux/api/unitsApi'; +import { selectMeterDataById } from '../api/metersApi'; +import { selectVisibleMetersAndGroups, selectVisibleUnitOrSuffixState } from './authVisibilitySelectors'; +import { MeterDataByID } from 'types/redux/meters'; +import { GroupDataByID } from 'types/redux/groups'; -export const selectVisibleMetersAndGroups = createSelector( - selectMeterDataById, - selectGroupDataById, - selectCurrentUser, - (meterDataByID, groupDataById, currentUser) => { - // Holds all meters visible to the user - const visibleMeters = new Set(); - const visibleGroups = new Set(); - - // Get all the meters that this user can see. - if (currentUser.profile?.role === 'admin') { - // Can see all meters - Object.values(meterDataByID).forEach(meter => { - visibleMeters.add(meter.id); - }); - Object.values(groupDataById).forEach(group => { - visibleGroups.add(group.id); - }); - } - else { - // Regular user or not logged in so only add displayable meters - Object.values(meterDataByID).forEach(meter => { - if (meter.displayable) { - visibleMeters.add(meter.id); - } - }); - Object.values(groupDataById).forEach(group => { - if (group.displayable) { - visibleGroups.add(group.id); - } - }); - } - return { meters: visibleMeters, groups: visibleGroups } - } -); export const selectCurrentUnitCompatibility = createSelector( selectVisibleMetersAndGroups, selectMeterDataById, selectGroupDataById, - selectGraphUnitID, - (visible, meterDataById, groupDataById, graphUnitID) => { + selectSelectedUnit, + (visible, meterDataById, groupDataById, selectedUnitId) => { // meters and groups that can graph const compatibleMeters = new Set(); const compatibleGroups = new Set(); @@ -77,62 +45,45 @@ export const selectCurrentUnitCompatibility = createSelector( const incompatibleMeters = new Set(); const incompatibleGroups = new Set(); - if (graphUnitID === -99) { - // No unit is selected then no meter/group should be selected. - // In this case, every meter is valid (provided it has a default graphic unit) + visible.meters.forEach(meterId => { + const meterGraphingUnit = meterDataById[meterId].defaultGraphicUnit; + // when no unit is currently selected, every meter/group is valid provided it has a default graphic unit // If the meter/group has a default graphic unit set then it can graph, otherwise it cannot. - visible.meters.forEach(meterId => { - const meterGraphingUnit = meterDataById[meterId].defaultGraphicUnit; - if (meterGraphingUnit === -99) { - //Default graphic unit is not set - incompatibleMeters.add(meterId); - } - else { - //Default graphic unit is set - compatibleMeters.add(meterId); - } - }); - visible.groups.forEach(groupId => { + // selectedUnitId -99 indicates no unit selected + if (selectedUnitId === -99 && meterGraphingUnit === -99) { + // No Unit Selected and Default graphic unit is not set + incompatibleMeters.add(meterId); + } + else if (selectedUnitId === -99) { + // no unitSelected, but has a default unit + compatibleMeters.add(meterId) + } + else { + // A unit is selected + // Get all of compatible units for this meter + const compatibleUnits = unitsCompatibleWithMeters(new Set([meterId])); + // Then, check if the selected unit exists in that set of compatible units + compatibleUnits.has(selectedUnitId) ? compatibleMeters.add(meterId) : incompatibleMeters.add(meterId); + } + }); + + // Same As Meters + visible.groups + .forEach(groupId => { const groupGraphingUnit = groupDataById[groupId].defaultGraphicUnit; - if (groupGraphingUnit === -99) { - //Default graphic unit is not set + if (selectedUnitId === -99 && groupGraphingUnit === -99) { incompatibleGroups.add(groupId); } - else { - //Default graphic unit is set - compatibleGroups.add(groupId); - } - }); - } else { - // A unit is selected - // For each meter get all of its compatible units - // Then, check if the selected unit exists in that set of compatible units - visible.meters.forEach(meterId => { - // Get the set of units compatible with the current meter - const compatibleUnits = unitsCompatibleWithMeters(new Set([meterId])); - if (compatibleUnits.has(graphUnitID)) { - // The selected unit is part of the set of compatible units with this meter - compatibleMeters.add(meterId); + else if (selectedUnitId === -99) { + compatibleGroups.add(groupId) } else { - // The selected unit is not part of the compatible units set for this meter - incompatibleMeters.add(meterId); + // Get the set of units compatible with the current group (through its deepMeters attribute) + // TODO If a meter in a group is not visible to this user then it is not in Redux state and this fails. + const compatibleUnits = unitsCompatibleWithMeters(metersInGroup(groupId)); + compatibleUnits.has(selectedUnitId) ? compatibleGroups.add(groupId) : incompatibleGroups.add(groupId); } - }); - visible.groups.forEach(groupId => { - // Get the set of units compatible with the current group (through its deepMeters attribute) - // TODO If a meter in a group is not visible to this user then it is not in Redux state and this fails. - const compatibleUnits = unitsCompatibleWithMeters(metersInGroup(groupId)); - if (compatibleUnits.has(graphUnitID)) { - // The selected unit is part of the set of compatible units with this group - compatibleGroups.add(groupId); - } - else { - // The selected unit is not part of the compatible units set for this group - incompatibleGroups.add(groupId); - } - }); - } + }) return { compatibleMeters, incompatibleMeters, compatibleGroups, incompatibleGroups } } @@ -141,11 +92,11 @@ export const selectCurrentUnitCompatibility = createSelector( export const selectCurrentAreaCompatibility = createSelector( selectCurrentUnitCompatibility, selectGraphAreaNormalization, - selectGraphUnitID, + selectSelectedUnit, selectMeterDataById, selectGroupDataById, selectUnitDataById, - (currentUnitCompatibility, areaNormalization, unitID, meterDataById, groupDataById, unitDataById) => { + (currentUnitCompatibility, areaNormalization, selectedUnit, meterDataById, groupDataById, unitDataById) => { // Deep Copy previous selector's values, and update as needed based on current Area Normalization setting const compatibleMeters = new Set(currentUnitCompatibility.compatibleMeters); const compatibleGroups = new Set(currentUnitCompatibility.compatibleGroups); @@ -157,31 +108,20 @@ export const selectCurrentAreaCompatibility = createSelector( // only run this check if area normalization is on if (areaNormalization) { compatibleMeters.forEach(meterID => { - const meterGraphingUnit = meterDataById[meterID].defaultGraphicUnit; // No unit is selected then no meter/group should be selected if area normalization is enabled and meter type is raw - if ((unitID === -99 && unitDataById[meterGraphingUnit] && unitDataById[meterGraphingUnit].unitRepresent === UnitRepresentType.raw) || - // do not allow meter to be selected if it has zero area or no area unit - meterDataById[meterID].area === 0 || meterDataById[meterID].areaUnit === AreaUnitType.none - ) { + if (!isAreaNormCompatible(meterID, selectedUnit, meterDataById, unitDataById)) { incompatibleMeters.add(meterID); } }); compatibleGroups.forEach(groupID => { - const groupGraphingUnit = groupDataById[groupID].defaultGraphicUnit; - // No unit is selected then no meter/group should be selected if area normalization is enabled and meter type is raw - - if ((unitID === -99 && unitDataById[groupGraphingUnit] && unitDataById[groupGraphingUnit].unitRepresent === UnitRepresentType.raw) || - // do not allow group to be selected if it has zero area or no area unit - groupDataById[groupID].area === 0 || groupDataById[groupID].areaUnit === AreaUnitType.none) { + if (!isAreaNormCompatible(groupID, selectedUnit, groupDataById, unitDataById)) { incompatibleGroups.add(groupID); } }); - // Filter out any new incompatible meters/groups from the compatibility list. - incompatibleMeters.forEach(meterID => compatibleMeters.delete(meterID)) - incompatibleGroups.forEach(groupID => compatibleGroups.delete(groupID)) } - - + // Filter out any new incompatible meters/groups from the compatibility list. + incompatibleMeters.forEach(meterID => compatibleMeters.delete(meterID)) + incompatibleGroups.forEach(groupID => compatibleGroups.delete(groupID)) return { compatibleMeters, incompatibleMeters, compatibleGroups, incompatibleGroups } } ) @@ -308,12 +248,13 @@ export const selectMeterGroupSelectData = createSelector( const meterSelectOptions = getSelectOptionsByItem(compatibleMeters, incompatibleMeters, meterDataById, 'meter') const groupSelectOptions = getSelectOptionsByItem(compatibleGroups, incompatibleGroups, groupDataById, 'group') - // currently when selected values are found to be incompatible (by area for example) get removed from selected options. - // in the near future they should instead remain selected but visually appear disabled. - // TODO WRITE CUSTOM SELECT VALUE TO BE ABLE TO UTILIZE THESE Values + // currently when selected values are found to be incompatible get removed from selected options (by area for example) . + // in the near future they should instead remain 'selected' but visually appear disabled. + // TODO write custom React-Select component to be able to utilize these values + // The select options have an attached 'disabled' boolean which can be used when writing custom component for react-select // These value(s) is not currently utilized - const selectedMeterValues = selectedMeterOptions.compatible.concat(selectedMeterOptions.incompatible) - const selectedGroupValues = selectedGroupOptions.compatible.concat(selectedGroupOptions.incompatible) + const allSelectedMeterValues = selectedMeterOptions.compatible.concat(selectedMeterOptions.incompatible) + const allSelectedGroupValues = selectedGroupOptions.compatible.concat(selectedGroupOptions.incompatible) // Format The generated selectOptions into grouped options for the React-Select component const meterGroupedOptions: GroupedOption[] = [ @@ -325,35 +266,11 @@ export const selectMeterGroupSelectData = createSelector( { label: 'Incompatible Options', options: groupSelectOptions.incompatible } ] - return { meterGroupedOptions, groupsGroupedOptions, selectedMeterValues, selectedGroupValues } + return { meterGroupedOptions, groupsGroupedOptions, selectedMeterOptions, selectedGroupOptions, allSelectedMeterValues, allSelectedGroupValues } } ) -/** - * Filters all units that are of type meter or displayable type none from the redux state, as well as admin only units if the user is not an admin. - * @param state - current redux state - * @returns an array of UnitData - */ -export const selectVisibleUnitOrSuffixState = createSelector( - selectUnitDataById, - selectCurrentUser, - (unitDataById, currentUser) => { - let visibleUnitsOrSuffixes; - if (currentUser.profile?.role === 'admin') { - // User is an admin, allow all units to be seen - visibleUnitsOrSuffixes = _.filter(unitDataById, o => { - return (o.typeOfUnit == UnitType.unit || o.typeOfUnit == UnitType.suffix) && o.displayable != DisplayableType.none; - }); - } - else { - // User is not an admin, do not allow for admin units to be seen - visibleUnitsOrSuffixes = _.filter(unitDataById, o => { - return (o.typeOfUnit == UnitType.unit || o.typeOfUnit == UnitType.suffix) && o.displayable == DisplayableType.all; - }); - } - return visibleUnitsOrSuffixes; - } -) + export const selectUnitSelectData = createSelector( selectUnitDataById, @@ -440,6 +357,7 @@ export function getSelectOptionsByItem( incompatibleItems: Set, // using any due to ts errs dataById: any, + // After moving to RTK the older instanceOfx no longer works due to lack of meter,group,unit state reducers. type: 'meter' | 'group' | 'unit') { // TODO Refactor original // redefined here for testing. @@ -476,7 +394,7 @@ export function getSelectOptionsByItem( } // TODO This is a bit of a hack. When an admin logs in they may not have the new state so label is null. // This should clear once the state is loaded. - label = label === null ? '' : label; + // label = label === null ? '' : label; compatibleItemOptions.push({ value: itemId, label: label, @@ -505,9 +423,6 @@ export function getSelectOptionsByItem( meterOrGroup = MeterOrGroup.groups defaultGraphicUnit = dataById[itemId]?.defaultGraphicUnit; } - // TODO This is a bit of a hack. When an admin logs in they may not have the new state so label is null. - // This should clear once the state is loaded. - label = label === null ? '' : label; incompatibleItemOptions.push({ value: itemId, label: label, @@ -518,16 +433,25 @@ export function getSelectOptionsByItem( } as SelectOption ); }); - const sortedCompatibleOptions = _.sortBy(compatibleItemOptions, item => item.label?.toLowerCase(), 'asc') - const sortedIncompatibleOptions = _.sortBy(incompatibleItemOptions, item => item.label?.toLowerCase(), 'asc') + const compatible = _.sortBy(compatibleItemOptions, item => item.label?.toLowerCase(), 'asc') + const incompatible = _.sortBy(incompatibleItemOptions, item => item.label?.toLowerCase(), 'asc') - return { compatible: sortedCompatibleOptions, incompatible: sortedIncompatibleOptions } + return { compatible, incompatible } } -export const selectDateRangeInterval = createSelector( - selectQueryTimeInterval, - timeInterval => { - return timeInterval - } -) + + +// Helper function for area compatibility +// areaNorm should be active when called +const isAreaNormCompatible = (id: number, selectedUnit: number, meterOrGroupData: MeterDataByID | GroupDataByID, unitDataById: UnitDataById) => { + const meterGraphingUnit = meterOrGroupData[id].defaultGraphicUnit; + + // If no unit is selected then no meter/group should be selected if meter type is raw + const noUnitAndRaw = selectedUnit === -99 && unitDataById[meterGraphingUnit]?.unitRepresent === UnitRepresentType.raw + + // do not allow meter to be selected if it has zero area or no area unit + const noAreaOrUnitType = meterOrGroupData[id].area === 0 || meterOrGroupData[id].areaUnit === AreaUnitType.none + const isAreaNormCompatible = !noUnitAndRaw && !noAreaOrUnitType + return isAreaNormCompatible +} \ No newline at end of file diff --git a/src/client/app/store.ts b/src/client/app/store.ts index 0439fb869..b02f9e10f 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -7,7 +7,7 @@ import { rootReducer } from './reducers'; import { baseApi } from './redux/api/baseApi'; import { Dispatch } from './types/redux/actions'; import { listenerMiddleware } from './redux/middleware/middleware'; - +import { setInputStabilityCheckEnabled } from 'reselect' export const store = configureStore({ reducer: rootReducer, @@ -18,11 +18,13 @@ export const store = configureStore({ .prepend(listenerMiddleware.middleware) .concat(baseApi.middleware) }); +// TODO determine where to place this, and only set for dev env +setInputStabilityCheckEnabled('always') // Infer the `RootState` and `AppDispatch` types from the store itself // https://react-redux.js.org/using-react-redux/usage-with-typescript#define-root-state-and-dispatch-types export type RootState = ReturnType 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 + // 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 From 3fa18cf68d879770330fb4f67f0228e054d370db Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Tue, 14 Nov 2023 03:33:31 +0000 Subject: [PATCH 043/131] Entity Adapter Reafactor --- src/client/app/actions/meters.ts | 4 +- .../app/components/ThreeDPillComponent.tsx | 4 +- .../ConversionsDetailComponentWIP.tsx | 9 +- .../CreateConversionModalComponentWIP.tsx | 18 +--- .../groups/CreateGroupModalComponentWIP.tsx | 4 +- .../groups/EditGroupModalComponentWIP.tsx | 2 +- .../groups/GroupsDetailComponentWIP.tsx | 4 - .../meters/CreateMeterModalComponentWIP.tsx | 50 +++++------ .../meters/EditMeterModalComponent.tsx | 6 +- .../meters/EditMeterModalComponentWIP.tsx | 4 +- .../meters/MetersDetailComponent.tsx | 15 ++-- .../meters/MetersDetailComponentWIP.tsx | 6 -- .../components/unit/UnitsDetailComponent.tsx | 21 ++--- .../app/containers/BarChartContainer.ts | 3 + .../app/containers/CompareChartContainer.ts | 2 +- .../app/containers/LineChartContainer.ts | 2 + .../app/containers/MapChartContainer.ts | 3 + .../app/containers/MeterDropdownContainer.ts | 9 +- src/client/app/reducers/groups.ts | 3 + src/client/app/reducers/meters.ts | 2 + src/client/app/reducers/units.ts | 2 + src/client/app/redux/api/baseApi.ts | 8 +- src/client/app/redux/api/conversionsApi.ts | 9 +- src/client/app/redux/api/groupsApi.ts | 83 +++++++++--------- src/client/app/redux/api/metersApi.ts | 86 +++++++++---------- src/client/app/redux/api/readingsApi.ts | 68 ++++++--------- src/client/app/redux/api/unitsApi.ts | 54 ++++-------- src/client/app/redux/api/userApi.ts | 4 +- .../app/redux/selectors/adminSelectors.ts | 69 +++++++++------ .../selectors/authVisibilitySelectors.ts | 34 ++++---- .../redux/selectors/chartQuerySelectors.ts | 13 +-- .../app/redux/selectors/threeDSelectors.ts | 2 +- src/client/app/redux/selectors/uiSelectors.ts | 4 +- .../app/utils/determineCompatibleUnits.ts | 12 +-- 34 files changed, 296 insertions(+), 323 deletions(-) diff --git a/src/client/app/actions/meters.ts b/src/client/app/actions/meters.ts index b9934fb1b..7497997fb 100644 --- a/src/client/app/actions/meters.ts +++ b/src/client/app/actions/meters.ts @@ -1,3 +1,6 @@ +/* eslint-disable */ +//@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 translate from '../utils/translate'; import * as t from '../types/redux/meters'; import { metersApi } from '../utils/api'; import { updateCikAndDBViewsIfNeeded } from './admin'; -import { metersSlice } from '../reducers/meters'; export function fetchMetersDetails(): Thunk { diff --git a/src/client/app/components/ThreeDPillComponent.tsx b/src/client/app/components/ThreeDPillComponent.tsx index 598347e05..84bc08243 100644 --- a/src/client/app/components/ThreeDPillComponent.tsx +++ b/src/client/app/components/ThreeDPillComponent.tsx @@ -6,10 +6,10 @@ import * as React from 'react'; import { Badge } from 'reactstrap'; import { updateThreeDMeterOrGroupInfo } from '../reducers/graph'; import { selectGroupDataById } from '../redux/api/groupsApi'; -import { selectMeterDataById } from '../redux/api/metersApi'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { MeterOrGroup, MeterOrGroupPill } from '../types/redux/graph'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; +import { selectMeterDataById } from '../redux/api/metersApi'; /** * A component used in the threeD graphics to select a single meter from the currently selected meters and groups. @@ -17,7 +17,7 @@ import { AreaUnitType } from '../utils/getAreaUnitConversion'; */ export default function ThreeDPillComponent() { const dispatch = useAppDispatch(); - const meterDataById = useAppSelector(selectMeterDataById); + const meterDataById = useAppSelector(selectMeterDataById); const groupDataById = useAppSelector(selectGroupDataById); const threeDState = useAppSelector(state => state.graph.threeD); const graphState = useAppSelector(state => state.graph); diff --git a/src/client/app/components/conversion/ConversionsDetailComponentWIP.tsx b/src/client/app/components/conversion/ConversionsDetailComponentWIP.tsx index 3b06f615b..2929ad31c 100644 --- a/src/client/app/components/conversion/ConversionsDetailComponentWIP.tsx +++ b/src/client/app/components/conversion/ConversionsDetailComponentWIP.tsx @@ -7,7 +7,7 @@ import { FormattedMessage } from 'react-intl'; import SpinnerComponent from '../../components/SpinnerComponent'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { conversionsApi } from '../../redux/api/conversionsApi'; -import { unitsApi } from '../../redux/api/unitsApi'; +import { unitsAdapter, unitsApi } from '../../redux/api/unitsApi'; import { ConversionData } from '../../types/redux/conversions'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import ConversionViewComponentWIP from './ConversionViewComponentWIP'; @@ -23,7 +23,12 @@ export default function ConversionsDetailComponent() { // Conversions state const { data: conversionsState = [], isFetching: conversionsFetching } = conversionsApi.useGetConversionsDetailsQuery(); // Units DataById - const { data: unitDataById = {}, isFetching: unitsFetching } = unitsApi.useGetUnitsDetailsQuery() + const { unitDataById = {}, isFetching: unitsFetching } = unitsApi.useGetUnitsDetailsQuery(undefined, { + selectFromResult: ({ data, ...result }) => ({ + ...result, + unitDataById: data && unitsAdapter.getSelectors().selectEntities(data) + }) + }) // const x = useAppSelector(state => conversionsApi.endpoints.refresh.select()(state)) // unnecessary? Currently this occurs as a side effect of the mutation which will invalidate meters/group diff --git a/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx b/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx index e6376e5ec..7abd1d71a 100644 --- a/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx +++ b/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx @@ -9,9 +9,8 @@ import { FormattedMessage } from 'react-intl'; import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { conversionsApi } from '../../redux/api/conversionsApi'; -import { selectUnitDataById } from '../../redux/api/unitsApi'; import { useAppSelector } from '../../redux/hooks'; -import { selectIsValidConversion } from '../../redux/selectors/adminSelectors'; +import { selectDefaultCreateConversionValues, selectIsValidConversion } from '../../redux/selectors/adminSelectors'; import '../../styles/modal.css'; import { tooltipBaseStyle } from '../../styles/modalStyle'; import { TrueFalseType } from '../../types/items'; @@ -25,22 +24,9 @@ import TooltipMarkerComponent from '../TooltipMarkerComponent'; */ export default function CreateConversionModalComponent() { const [addConversionMutation] = conversionsApi.useAddConversionMutation() - const unitDataById = useAppSelector(selectUnitDataById) // Want units in sorted order by identifier regardless of case. - const sortedUnitData = _.sortBy(Object.values(unitDataById), unit => unit.identifier.toLowerCase(), 'asc'); - const defaultValues = { - // Invalid source/destination ids arbitrarily set to -999. - // Meter Units are not allowed to be a destination. - sourceId: -999, - sourceOptions: sortedUnitData, - destinationId: -999, - destinationOptions: sortedUnitData.filter(unit => unit.typeOfUnit !== 'meter'), - bidirectional: true, - slope: 0, - intercept: 0, - note: '' - } + const defaultValues = useAppSelector(selectDefaultCreateConversionValues) /* State */ // Modal show diff --git a/src/client/app/components/groups/CreateGroupModalComponentWIP.tsx b/src/client/app/components/groups/CreateGroupModalComponentWIP.tsx index 5b0e4ce4f..dafc16010 100644 --- a/src/client/app/components/groups/CreateGroupModalComponentWIP.tsx +++ b/src/client/app/components/groups/CreateGroupModalComponentWIP.tsx @@ -12,7 +12,6 @@ import { import { GroupData } from 'types/redux/groups'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { groupsApi, selectGroupDataById } from '../../redux/api/groupsApi'; -import { selectMeterDataById } from '../../redux/api/metersApi'; import { selectUnitDataById } from '../../redux/api/unitsApi'; import { useAppSelector } from '../../redux/hooks'; import { selectPossibleGraphicUnits } from '../../redux/selectors/adminSelectors'; @@ -33,6 +32,7 @@ import translate from '../../utils/translate'; import ListDisplayComponent from '../ListDisplayComponent'; import MultiSelectComponent from '../MultiSelectComponent'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; +import { selectMeterDataById } from '../../redux/api/metersApi'; /** * Defines the create group modal form @@ -46,7 +46,7 @@ export default function CreateGroupModalComponentWIP() { // Groups state const groupDataById = useAppSelector(selectGroupDataById); // Units state - const unitsDataById = useAppSelector(selectUnitDataById); + const unitsDataById = useAppSelector(selectUnitDataById); // Check for admin status const possibleGraphicUnits = useAppSelector(selectPossibleGraphicUnits) diff --git a/src/client/app/components/groups/EditGroupModalComponentWIP.tsx b/src/client/app/components/groups/EditGroupModalComponentWIP.tsx index 52eb07c45..c28ae4bc1 100644 --- a/src/client/app/components/groups/EditGroupModalComponentWIP.tsx +++ b/src/client/app/components/groups/EditGroupModalComponentWIP.tsx @@ -13,7 +13,6 @@ import { } from 'reactstrap'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { groupsApi, selectGroupDataById } from '../../redux/api/groupsApi'; -import { selectMeterDataById } from '../../redux/api/metersApi'; import { useAppSelector } from '../../redux/hooks'; import { selectPossibleGraphicUnits } from '../../redux/selectors/adminSelectors'; import { selectIsAdmin } from '../../reducers/currentUser'; @@ -40,6 +39,7 @@ import ConfirmActionModalComponent from '../ConfirmActionModalComponent'; import ListDisplayComponent from '../ListDisplayComponent'; import MultiSelectComponent from '../MultiSelectComponent'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; +import { selectMeterDataById } from '../../redux/api/metersApi'; interface EditGroupModalComponentProps { show: boolean; diff --git a/src/client/app/components/groups/GroupsDetailComponentWIP.tsx b/src/client/app/components/groups/GroupsDetailComponentWIP.tsx index 3bbbb0969..3361a3579 100644 --- a/src/client/app/components/groups/GroupsDetailComponentWIP.tsx +++ b/src/client/app/components/groups/GroupsDetailComponentWIP.tsx @@ -58,14 +58,10 @@ export default function GroupsDetailComponentWIP() { } {
- {/* Create a GroupViewComponent for each groupData in Groups State after sorting by name */} {Object.values(visibleGroups) - .sort((groupA, groupB) => (groupA.name.toLowerCase() > groupB.name.toLowerCase()) ? 1 : - ((groupB.name.toLowerCase() > groupA.name.toLowerCase()) ? -1 : 0)) .map(groupData => ())}
} diff --git a/src/client/app/components/meters/CreateMeterModalComponentWIP.tsx b/src/client/app/components/meters/CreateMeterModalComponentWIP.tsx index 2d3707de6..2a1544c93 100644 --- a/src/client/app/components/meters/CreateMeterModalComponentWIP.tsx +++ b/src/client/app/components/meters/CreateMeterModalComponentWIP.tsx @@ -58,7 +58,7 @@ export default function CreateMeterModalComponent() { compatibleGraphicUnits, compatibleUnits, incompatibleUnits - // Weird Type assertion due to conflicting GPS Property + // Type assertion due to conflicting GPS Property } = useAppSelector(state => selectGraphicUnitCompatibility(state, meterDetails as unknown as MeterData)) const handleShow = () => setShowModal(true); @@ -79,26 +79,9 @@ export default function CreateMeterModalComponent() { } // Dropdowns - const [selectedUnitId, setSelectedUnitId] = useState(false) - const [selectedGraphicId, setSelectedGraphicId] = useState(false) - /* Create Meter Validation: - Name cannot be blank - Area must be positive or zero - If area is nonzero, area unit must be set - Reading Gap must be greater than zero - Reading Variation must be greater than zero - Reading Duplication must be between 1 and 9 - Reading frequency cannot be blank - Unit and Default Graphic Unit must be set (can be to no unit) - Meter type must be set - If displayable is true and unitId is set to -99, warn admin - Mininum Value cannot bigger than Maximum Value - Minimum Value and Maximum Value must be between valid input - Minimum Date and Maximum cannot be blank - Minimum Date cannot be after Maximum Date - Minimum Date and Maximum Value must be between valid input - Maximum No of Error must be between 0 and valid input - */ + const [selectedUnitId, setSelectedUnitId] = useState(false); + const [selectedGraphicId, setSelectedGraphicId] = useState(false); + const [validMeter, setValidMeter] = useState(false); useEffect(() => { @@ -133,7 +116,6 @@ export default function CreateMeterModalComponent() { // TODO Maybe should do as a single popup? - // Check GPS entered. // Validate GPS is okay and take from string to GPSPoint to submit. const gpsInput = meterDetails.gps; @@ -183,8 +165,8 @@ export default function CreateMeterModalComponent() { }) .catch(err => { // TODO Better way than popup with React but want to stay so user can read/copy. - - window.alert(translate('meter.failed.to.create.meter') + '"' + err.response.data + '"'); + console.log(err) + window.alert(translate('meter.failed.to.create.meter') + '"' + err.data + '"'); }) } else { // Tell user that not going to update due to input issues. @@ -769,6 +751,26 @@ export default function CreateMeterModalComponent() { ); } + + +/* Create Meter Validation: + Name cannot be blank + Area must be positive or zero + If area is nonzero, area unit must be set + Reading Gap must be greater than zero + Reading Variation must be greater than zero + Reading Duplication must be between 1 and 9 + Reading frequency cannot be blank + Unit and Default Graphic Unit must be set (can be to no unit) + Meter type must be set + If displayable is true and unitId is set to -99, warn admin + Minimum Value cannot bigger than Maximum Value + Minimum Value and Maximum Value must be between valid input + Minimum Date and Maximum cannot be blank + Minimum Date cannot be after Maximum Date + Minimum Date and Maximum Value must be between valid input + Maximum No of Error must be between 0 and valid input +*/ const isValidCreateMeter = (meterDetails: MeterData) => { return meterDetails.name !== '' && (meterDetails.area === 0 || (meterDetails.area > 0 && meterDetails.areaUnit !== AreaUnitType.none)) && diff --git a/src/client/app/components/meters/EditMeterModalComponent.tsx b/src/client/app/components/meters/EditMeterModalComponent.tsx index ae102e264..41baa1e86 100644 --- a/src/client/app/components/meters/EditMeterModalComponent.tsx +++ b/src/client/app/components/meters/EditMeterModalComponent.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; import { FormattedMessage } from 'react-intl'; import translate from '../../utils/translate'; -import { useDispatch, useSelector } from 'react-redux'; +import { useDispatch, useSelector as useAppSelector } from 'react-redux'; import { useState, useEffect } from 'react'; import { State } from 'types/redux/state'; import '../../styles/modal.css'; @@ -45,7 +45,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr const dispatch: Dispatch = useDispatch(); // The current meter's state of meter being edited. It should always be valid. - const meterState = useSelector((state: State) => state.meters.byMeterID[props.meter.id]); + const meterState = useAppSelector((state: State) => state.meters.byMeterID[props.meter.id]); // Set existing meter values const values = { @@ -117,7 +117,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr const [dropdownsState, setDropdownsState] = useState(dropdownsStateDefaults); // unit state - const unitState = useSelector((state: State) => state.units.units); + const unitState = useAppSelector((state: State) => state.units.units); /* Edit Meter Validation: diff --git a/src/client/app/components/meters/EditMeterModalComponentWIP.tsx b/src/client/app/components/meters/EditMeterModalComponentWIP.tsx index 52f2b667d..2280d1d0a 100644 --- a/src/client/app/components/meters/EditMeterModalComponentWIP.tsx +++ b/src/client/app/components/meters/EditMeterModalComponentWIP.tsx @@ -10,7 +10,7 @@ import { FormattedMessage } from 'react-intl'; import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; -import { metersApi, selectMeterDataWithID } from '../../redux/api/metersApi'; +import { metersApi, selectMeterById } from '../../redux/api/metersApi'; import { selectUnitDataById } from '../../redux/api/unitsApi'; import { useAppDispatch, useAppSelector } from '../../redux/hooks'; import { makeSelectGraphicUnitCompatibility } from '../../redux/selectors/adminSelectors'; @@ -44,7 +44,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr // to have a single selector per modal instance. Memo ensures that this is a stable reference const selectGraphicUnitCompatibility = React.useMemo(makeSelectGraphicUnitCompatibility, []) // The current meter's state of meter being edited. It should always be valid. - const meterState = useAppSelector(state => selectMeterDataWithID(state, props.meter.id)) as MeterData; + const meterState = useAppSelector(state => selectMeterById(state, props.meter.id)); const [localMeterEdits, setLocalMeterEdits] = useState(_.cloneDeep(meterState)); const { compatibleGraphicUnits, diff --git a/src/client/app/components/meters/MetersDetailComponent.tsx b/src/client/app/components/meters/MetersDetailComponent.tsx index 5863a0c36..afa5f36db 100644 --- a/src/client/app/components/meters/MetersDetailComponent.tsx +++ b/src/client/app/components/meters/MetersDetailComponent.tsx @@ -25,14 +25,14 @@ import { selectUnitDataById } from '../../redux/api/unitsApi'; */ export default function MetersDetailComponent() { // current user state - const currentUserState = useAppSelector(state => selectCurrentUser(state)); + const currentUserState = useAppSelector(selectCurrentUser); // Check for admin status const isAdmin = useAppSelector(selectIsAdmin); // We only want displayable meters if non-admins because they still have // non-displayable in state. - const { visibleMeters } = useAppSelector(state => selectVisibleMeterAndGroupDataByID(state)); + const { visibleMeters } = useAppSelector(selectVisibleMeterAndGroupDataByID); // Units state const unitDataById = useAppSelector(selectUnitDataById); @@ -87,17 +87,16 @@ export default function MetersDetailComponent() { } {
- {/* Create a MeterViewComponent for each MeterData in Meters State after sorting by identifier */} - {Object.values(visibleMeters) - .sort((MeterA: MeterData, MeterB: MeterData) => (MeterA.identifier.toLowerCase() > MeterB.identifier.toLowerCase()) ? 1 : - ((MeterB.identifier.toLowerCase() > MeterA.identifier.toLowerCase()) ? -1 : 0)) - .map(MeterData => ( ( + ))} + possibleGraphicUnits={possibleGraphicUnits} + /> + ))}
}
diff --git a/src/client/app/components/meters/MetersDetailComponentWIP.tsx b/src/client/app/components/meters/MetersDetailComponentWIP.tsx index aeac7d081..0e2ce0028 100644 --- a/src/client/app/components/meters/MetersDetailComponentWIP.tsx +++ b/src/client/app/components/meters/MetersDetailComponentWIP.tsx @@ -46,12 +46,6 @@ export default function MetersDetailComponent() { {/* Create a MeterViewComponent for each MeterData in Meters State after sorting by identifier */} {/* Optional Chaining to prevent from crashing upon startup race conditions*/} {Object.values(visibleMeters) - .sort((MeterA, MeterB) => - (MeterA.identifier?.toLowerCase() > MeterB.identifier?.toLowerCase()) - ? 1 - : ((MeterB.identifier?.toLowerCase() > MeterA.identifier?.toLowerCase()) - ? -1 - : 0)) .map(MeterData => ( @@ -48,15 +48,12 @@ export default function UnitsDetailComponent() {
{/* Create a UnitViewComponent for each UnitData in Units State after sorting by identifier */} { - Object.values(unitDataById) - .sort((unitA, unitB) => (unitA.identifier.toLowerCase() > unitB.identifier.toLowerCase()) ? 1 : - ((unitB.identifier.toLowerCase() > unitA.identifier.toLowerCase()) ? -1 : 0)) - .map(unitData => ( - - ))} + unitData.map(unitData => ( + + ))}
diff --git a/src/client/app/containers/BarChartContainer.ts b/src/client/app/containers/BarChartContainer.ts index 794bb3ba2..a6521c209 100644 --- a/src/client/app/containers/BarChartContainer.ts +++ b/src/client/app/containers/BarChartContainer.ts @@ -1,3 +1,6 @@ +/* eslint-disable */ +//@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/. */ diff --git a/src/client/app/containers/CompareChartContainer.ts b/src/client/app/containers/CompareChartContainer.ts index 6f118fe71..dd643e4be 100644 --- a/src/client/app/containers/CompareChartContainer.ts +++ b/src/client/app/containers/CompareChartContainer.ts @@ -14,8 +14,8 @@ import { UnitRepresentType } from '../types/redux/units'; import { getAreaUnitConversion } from '../utils/getAreaUnitConversion'; import { selectUnitDataById } from '../redux/api/unitsApi'; import { RootState } from '../store'; -import { selectMeterDataById } from '../redux/api/metersApi'; import { selectGroupDataById } from '../redux/api/groupsApi'; +import { selectMeterDataById } from '../redux/api/metersApi'; export interface CompareEntity { id: number; isGroup: boolean; diff --git a/src/client/app/containers/LineChartContainer.ts b/src/client/app/containers/LineChartContainer.ts index 7183fe911..f69fa1fb5 100644 --- a/src/client/app/containers/LineChartContainer.ts +++ b/src/client/app/containers/LineChartContainer.ts @@ -1,3 +1,5 @@ +/* eslint-disable */ +//@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/. */ diff --git a/src/client/app/containers/MapChartContainer.ts b/src/client/app/containers/MapChartContainer.ts index 849e6c17c..130e65b07 100644 --- a/src/client/app/containers/MapChartContainer.ts +++ b/src/client/app/containers/MapChartContainer.ts @@ -1,3 +1,6 @@ +/* eslint-disable */ +//@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/. */ diff --git a/src/client/app/containers/MeterDropdownContainer.ts b/src/client/app/containers/MeterDropdownContainer.ts index 728a040c3..33d409f70 100644 --- a/src/client/app/containers/MeterDropdownContainer.ts +++ b/src/client/app/containers/MeterDropdownContainer.ts @@ -4,14 +4,15 @@ import * as _ from 'lodash'; import { connect } from 'react-redux'; +import { selectMeterDataById } from 'redux/api/metersApi'; import MeterDropdownComponent from '../components/MeterDropDownComponent'; -import { State } from '../types/redux/state'; -import { Dispatch } from '../types/redux/actions'; import { adminSlice } from '../reducers/admin'; +import { RootState } from '../store'; +import { Dispatch } from '../types/redux/actions'; -function mapStateToProps(state: State) { +function mapStateToProps(state: RootState) { return { - meters: _.sortBy(_.values(state.meters.byMeterID).map(meter => ({ id: meter.id, name: meter.name })), 'name') + meters: _.sortBy(_.values(selectMeterDataById(state)).map(meter => ({ id: meter.id, name: meter.name })), 'name') }; } function mapDispatchToProps(dispatch: Dispatch) { diff --git a/src/client/app/reducers/groups.ts b/src/client/app/reducers/groups.ts index d894d11fd..3f77c219a 100644 --- a/src/client/app/reducers/groups.ts +++ b/src/client/app/reducers/groups.ts @@ -1,3 +1,6 @@ +/* eslint-disable */ +//@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/. */ diff --git a/src/client/app/reducers/meters.ts b/src/client/app/reducers/meters.ts index d5c44ad8a..c5ebf4184 100644 --- a/src/client/app/reducers/meters.ts +++ b/src/client/app/reducers/meters.ts @@ -1,3 +1,5 @@ +/* eslint-disable */ +//@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/. */ diff --git a/src/client/app/reducers/units.ts b/src/client/app/reducers/units.ts index 4b90e4c25..0869b4e90 100644 --- a/src/client/app/reducers/units.ts +++ b/src/client/app/reducers/units.ts @@ -1,3 +1,5 @@ +/* eslint-disable */ +//@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/. */ diff --git a/src/client/app/redux/api/baseApi.ts b/src/client/app/redux/api/baseApi.ts index 4e6834ab1..8f7cb63a4 100644 --- a/src/client/app/redux/api/baseApi.ts +++ b/src/client/app/redux/api/baseApi.ts @@ -1,4 +1,4 @@ -import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' +import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; import { RootState } from '../../store'; // TODO Should be env variable? const baseHref = (document.getElementsByTagName('base')[0] || {}).href; @@ -14,7 +14,11 @@ export const baseApi = createApi({ if (state.currentUser.token) { headers.set('token', state.currentUser.token) } - } + }, + // Default Behavior assumes all response are json + // use content type cause 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. // Must be defined here, for use in injected endpoints diff --git a/src/client/app/redux/api/conversionsApi.ts b/src/client/app/redux/api/conversionsApi.ts index 2b5ecc6e1..1a8793af2 100644 --- a/src/client/app/redux/api/conversionsApi.ts +++ b/src/client/app/redux/api/conversionsApi.ts @@ -15,8 +15,7 @@ export const conversionsApi = baseApi.injectEndpoints({ query: conversion => ({ url: 'api/conversions/addConversion', method: 'POST', - body: conversion, - responseHandler: 'text' + body: conversion }), onQueryStarted: async (_arg, api) => { // TODO write more robust logic for error handling, and manually invalidate tags instead? @@ -36,8 +35,7 @@ export const conversionsApi = baseApi.injectEndpoints({ query: conversion => ({ url: 'api/conversions/delete', method: 'POST', - body: conversion, - responseHandler: 'text' + body: conversion }), onQueryStarted: async (_, { queryFulfilled, dispatch }) => { // TODO write more robust logic for error handling, and manually invalidate tags instead? @@ -80,8 +78,7 @@ export const conversionsApi = baseApi.injectEndpoints({ body: { redoCik: args.redoCik, refreshReadingViews: args.refreshReadingViews - }, - responseHandler: 'text' + } }), // TODO check behavior with maintainers, always invalidates, should be conditional? invalidatesTags: ['ConversionDetails'] diff --git a/src/client/app/redux/api/groupsApi.ts b/src/client/app/redux/api/groupsApi.ts index fd2250528..47bf6dc09 100644 --- a/src/client/app/redux/api/groupsApi.ts +++ b/src/client/app/redux/api/groupsApi.ts @@ -1,42 +1,47 @@ +import { EntityState, createEntityAdapter } from '@reduxjs/toolkit'; import * as _ from 'lodash'; -import { GroupChildren, GroupData, GroupDataByID } from '../../types/redux/groups'; -import { baseApi } from './baseApi'; -import { selectIsAdmin } from '../../reducers/currentUser'; -import { RootState } from '../../store'; import { CompareReadings } from 'types/readings'; import { TimeInterval } from '../../../../common/TimeInterval'; -import { createSelector } from '@reduxjs/toolkit'; +import { selectIsAdmin } from '../../reducers/currentUser'; +import { RootState } from '../../store'; +import { GroupChildren, GroupData } from '../../types/redux/groups'; +import { baseApi } from './baseApi'; +export const groupsAdapter = createEntityAdapter({ + sortComparer: (groupA, groupB) => groupA.name.localeCompare(groupB.name) +}) +export const groupsInitialState = groupsAdapter.getInitialState() +export type GroupDataState = EntityState export const groupsApi = baseApi.injectEndpoints({ endpoints: builder => ({ - getGroups: builder.query({ + getGroups: builder.query({ query: () => 'api/groups', transformResponse: (response: GroupData[]) => { - const groupsData = response.map(groupData => ({ - ...groupData, - // endpoint doesn't return these so define them here or else undefined may cause issues on admin pages - childMeters: [], - childGroups: [] - })) - return _.keyBy(groupsData, 'id') + return groupsAdapter.setAll( + groupsInitialState, + response.map(groupData => ({ + ...groupData, + // endpoint doesn't return these so define them here or else undefined may cause issues on admin pages + childMeters: [], + childGroups: [] + }))) }, - onQueryStarted: async (_, api) => { + onQueryStarted: async (_, { dispatch, queryFulfilled, getState }) => { try { - await api.queryFulfilled - const state = api.getState() as RootState - const isAdmin = selectIsAdmin(state) + await queryFulfilled + const state = getState() as RootState // if user is an admin, automatically fetch allGroupChildren and update the - if (isAdmin) { - const { data = [] } = await api.dispatch(groupsApi.endpoints.getAllGroupsChildren.initiate(undefined)) - api.dispatch(groupsApi.util.updateQueryData('getGroups', undefined, groupDataById => { - data.forEach(groupInfo => { - const groupId = groupInfo.groupId; - // Group id of the current item - // Reset the newState for this group to have child meters/groups. - groupDataById[groupId].childMeters = groupInfo.childMeters; - groupDataById[groupId].childGroups = groupInfo.childGroups; - }) - })) + if (selectIsAdmin(state)) { + const { data = [] } = await dispatch(groupsApi.endpoints.getAllGroupsChildren.initiate()) + // Map the data to the format needed for updateMany + const updates = data.map(childrenInfo => ({ + id: childrenInfo.groupId, + changes: { + childMeters: childrenInfo.childMeters, + childGroups: childrenInfo.childGroups + } + })); + dispatch(groupsApi.util.updateQueryData('getGroups', undefined, groupDataById => { groupsAdapter.updateMany(groupDataById, updates) })) } } catch (e) { console.log(e) @@ -98,20 +103,18 @@ export const groupsApi = baseApi.injectEndpoints({ }) }) -export const selectGroupDataByIdQueryState = groupsApi.endpoints.getGroups.select(); -export const selectGroupDataById = createSelector( - selectGroupDataByIdQueryState, - ({ data: groupDataById = {} }) => { - return groupDataById - } -) +export const selectGroupDataResult = groupsApi.endpoints.getGroups.select(); + +export const { + selectAll: selectAllGroups, + selectById: selectGroupById, + selectTotal: selectGroupTotal, + selectIds: selectGroupIds, + selectEntities: selectGroupDataById +} = groupsAdapter.getSelectors((state: RootState) => selectGroupDataResult(state).data ?? groupsInitialState) -export const selectGroupDataWithID = (state: RootState, groupId: number): GroupData | undefined => { - const groupDataById = selectGroupDataById(state) - return groupDataById[groupId] -} export const selectGroupNameWithID = (state: RootState, groupId: number) => { - const groupInfo = selectGroupDataWithID(state, groupId) + const groupInfo = selectGroupById(state, groupId) return groupInfo ? groupInfo.name : ''; } \ No newline at end of file diff --git a/src/client/app/redux/api/metersApi.ts b/src/client/app/redux/api/metersApi.ts index 51b407968..e4d493ff8 100644 --- a/src/client/app/redux/api/metersApi.ts +++ b/src/client/app/redux/api/metersApi.ts @@ -1,23 +1,31 @@ -import * as _ from 'lodash'; +import { EntityState, createEntityAdapter } from '@reduxjs/toolkit'; import { NamedIDItem } from 'types/items'; import { RawReadings } from 'types/readings'; import { TimeInterval } from '../../../../common/TimeInterval'; import { RootState } from '../../store'; -import { MeterData, MeterDataByID } from '../../types/redux/meters'; +import { MeterData } from '../../types/redux/meters'; import { durationFormat } from '../../utils/durationFormat'; import { baseApi } from './baseApi'; import { conversionsApi } from './conversionsApi'; -import { createSelector } from '@reduxjs/toolkit'; +export const meterAdapter = createEntityAdapter({ + sortComparer: (MeterA, MeterB) => MeterA.identifier.localeCompare(MeterB.identifier) + +}) +export const metersInitialState = meterAdapter.getInitialState() +export type MeterDataState = EntityState export const metersApi = baseApi.injectEndpoints({ endpoints: builder => ({ - getMeters: builder.query({ + getMeters: builder.query({ query: () => 'api/meters', // Optional endpoint property that can transform incoming api responses if needed + // Use EntityAdapters from RTK to normalizeData, and generate commonSelectors transformResponse: (response: MeterData[]) => { - response.forEach(meter => { meter.readingFrequency = durationFormat(meter.readingFrequency) }); - return _.keyBy(response, meter => meter.id) + return meterAdapter.setAll(metersInitialState, response.map(meter => ({ + ...meter, + readingFrequency: durationFormat(meter.readingFrequency) + }))) }, // Tags used for invalidation by mutation requests. providesTags: ['MeterData'] @@ -28,10 +36,12 @@ export const metersApi = baseApi.injectEndpoints({ method: 'POST', body: { ...meterData } }), - onQueryStarted: async ({ shouldRefreshViews }, api) => { - await api.queryFulfilled.then(() => { + onQueryStarted: async ({ meterData, shouldRefreshViews }, { dispatch, queryFulfilled }) => { + queryFulfilled.then(() => { // Update reading views if needed. Never redoCik so false. - api.dispatch(conversionsApi.endpoints.refresh.initiate({ redoCik: false, refreshReadingViews: shouldRefreshViews })) + dispatch(conversionsApi.endpoints.refresh.initiate({ redoCik: false, refreshReadingViews: shouldRefreshViews })) + dispatch(metersApi.util.updateQueryData('getMeters', undefined, cacheDraft => { meterAdapter.addOne(cacheDraft, meterData) })) + }) }, invalidatesTags: ['MeterData'] @@ -42,10 +52,13 @@ export const metersApi = baseApi.injectEndpoints({ method: 'POST', body: { ...meter } }), - - invalidatesTags: ['MeterData'] + transformResponse: (data: MeterData) => ({ ...data, readingFrequency: durationFormat(data.readingFrequency) }), + onQueryStarted: (_arg, { dispatch, queryFulfilled }) => { + queryFulfilled.then(({ data }) => { + dispatch(metersApi.util.updateQueryData('getMeters', undefined, cacheDraft => { meterAdapter.addOne(cacheDraft, data) })) + }) + } }), - lineReadingsCount: builder.query({ query: ({ meterIDs, timeInterval }) => `api/readings/line/count/meters/${meterIDs.join(',')}?timeInterval=${timeInterval.toString()}` }), @@ -58,37 +71,16 @@ export const metersApi = baseApi.injectEndpoints({ }) }) -export const selectMeterDataByIdQueryState = metersApi.endpoints.getMeters.select() -/** - * Selects the meter data associated with a given meter ID from the Redux state. - * @param {RootState} state - The current state of the Redux store. - * @returns The latest query state for the given which can be destructured for the dataById - * @example - * const endpointState = useAppSelector(state => selectMeterDataById(state)) - * const meterDataByID = endpointState.data - * or - * const { data: meterDataByID } = useAppSelector(state => selectMeterDataById(state)) - */ -export const selectMeterDataById = createSelector( - selectMeterDataByIdQueryState, - ({ data: meterDataById = {} }) => { - return meterDataById - } -) +export const selectMeterDataResult = metersApi.endpoints.getMeters.select() -/** - * Selects the meter data associated with a given meter ID from the Redux state. - * @param state - The current state of the Redux store. - * @param meterID - The unique identifier for the meter. - * @returns The data for the specified meter or undefined if not found. - * @example - * const meterData = useAppSelector(state => selectMeterDataWithID(state, 42)) - */ -export const selectMeterDataWithID = (state: RootState, meterID: number): MeterData | undefined => { - const meterDataByID = selectMeterDataById(state); - return meterDataByID[meterID]; -} +export const { + selectAll: selectAllMeters, + selectById: selectMeterById, + selectTotal: selectMeterTotal, + selectIds: selectMeterIds, + selectEntities: selectMeterDataById +} = meterAdapter.getSelectors((state: RootState) => selectMeterDataResult(state).data ?? metersInitialState) /** @@ -97,10 +89,10 @@ export const selectMeterDataWithID = (state: RootState, meterID: number): MeterD * @param meterID - The unique identifier for the meter. * @returns The name of the specified meter or an empty string if not found. * @example - * const meterName = useAppSelector(state => selectMeterNameWithID(state, 42)) + * const meterName = useAppSelector(state => selectMeterNameById(state, 42)) */ -export const selectMeterNameWithID = (state: RootState, meterID: number) => { - const meterInfo = selectMeterDataWithID(state, meterID); +export const selectMeterNameById = (state: RootState, meterID: number) => { + const meterInfo = selectMeterById(state, meterID); return meterInfo ? meterInfo.name : ''; } @@ -110,9 +102,9 @@ export const selectMeterNameWithID = (state: RootState, meterID: number) => { * @param meterID - The unique identifier for the meter. * @returns The identifier for the specified meter or an empty string if not found. * @example - * const meterIdentifier = useAppSelector(state => selectMeterIdentifier(state, 42)) + * const meterIdentifier = useAppSelector(state => selectMeterIdentifierById(state, 42)) */ -export const selectMeterIdentifierWithID = (state: RootState, meterID: number) => { - const meterInfo = selectMeterDataWithID(state, meterID); +export const selectMeterIdentifierById = (state: RootState, meterID: number) => { + const meterInfo = selectMeterById(state, meterID); return meterInfo ? meterInfo.identifier : ''; } \ No newline at end of file diff --git a/src/client/app/redux/api/readingsApi.ts b/src/client/app/redux/api/readingsApi.ts index bd6e8b04d..0f0b1eb6d 100644 --- a/src/client/app/redux/api/readingsApi.ts +++ b/src/client/app/redux/api/readingsApi.ts @@ -6,23 +6,17 @@ import { baseApi } from './baseApi'; export const readingsApi = baseApi.injectEndpoints({ endpoints: builder => ({ - // threeD: the queryEndpoint name // builder.query threeD: builder.query({ // ThreeD request only single meters at a time which plays well with default cache behavior // No other properties are necessary for this endpoint // Refer to the line endpoint for an example of an endpoint with custom cache behavior - query: ({ id, timeInterval, unitID, readingInterval, meterOrGroup }) => { + query: ({ id, timeInterval, graphicUnitId, readingInterval, meterOrGroup }) => ({ // destructure args that are passed into the callback, and generate the API url for the request. - const endpoint = `api/unitReadings/threeD/${meterOrGroup}/` - const args = `${id}?timeInterval=${timeInterval}&graphicUnitId=${unitID}&readingInterval=${readingInterval}` - return `${endpoint}${args}` - } + url: `api/unitReadings/threeD/${meterOrGroup}/${id}?`, + params: { timeInterval, graphicUnitId, readingInterval } + }) }), - // line: the queryEndpoint name // builder.query line: builder.query({ - // To see another example of (serializeQueryArgs, merge, forceRefetch) being used in tandem to customize cache behavior refer to: - // Example for merge https://redux-toolkit.js.org/rtk-query/api/createApi#merge - // Customize Cache Behavior by utilizing (serializeQueryArgs, merge, forceRefetch) serializeQueryArgs: ({ queryArgs }) => { // Modify the default serialization behavior to better suit our use case, to avoid querying already cached data. @@ -45,17 +39,14 @@ export const readingsApi = baseApi.injectEndpoints({ // Since we modified the way the we serialize the args any subsequent query would return the cache data, even if new meters were requested // To resolve this we provide a forceRefetch where we decide if data needs to be fetched, or retrieved from the cache. - // check if there is data in the endpointState, - const currentData = endpointState?.data ? Object.keys(endpointState.data).map(Number) : undefined - if (!currentData) { - // No data, so force fetch - return true - } - // check if the requested id's already exist in cache + // get existing cached Keys if any + const currentData = endpointState?.data ? Object.keys(endpointState.data).map(Number) : [] + + // check if the ALL requested id's already exist in cache const dataInCache = currentArg?.ids.every(id => currentData.includes(id)) // if data requested already lives in the cache, no fetch necessary, else fetch for data - return dataInCache ? false : true + return !dataInCache }, queryFn: async (args, queryApi, _extra, baseQuery) => { // We opt for a query function here instead of the normal query: args => {....} @@ -70,15 +61,17 @@ export const readingsApi = baseApi.injectEndpoints({ // map cache keys to a number array, if any const cachedIDs = cachedData ? Object.keys(cachedData).map(Number) : [] // get the args provided in the original request - const { ids, timeInterval, unitID, meterOrGroup } = args + const { ids, timeInterval, graphicUnitId, meterOrGroup } = args // subtract any already cached keys from the requested ids, and stringify the array for the url endpoint const idsToFetch = _.difference(ids, cachedIDs).join(',') - // api url from derived request arguments - const endpointURL = `api/unitReadings/line/${meterOrGroup}/${idsToFetch}?timeInterval=${timeInterval}&graphicUnitId=${unitID}` // use the baseQuery from the queryFn with our url endpoint - const { data, error } = await baseQuery(endpointURL) + const { data, error } = await baseQuery({ + // api url from derived request arguments + url: `api/unitReadings/line/${meterOrGroup}/${idsToFetch}`, + params: { timeInterval, graphicUnitId } + }) // https://redux-toolkit.js.org/rtk-query/usage/customizing-queries#implementing-a-queryfn // queryFn requires either a data, or error object to be returned @@ -95,21 +88,20 @@ export const readingsApi = baseApi.injectEndpoints({ serializeQueryArgs: ({ queryArgs }) => _.omit(queryArgs, 'ids'), merge: (currentCacheData, responseData) => { Object.assign(currentCacheData, responseData) }, forceRefetch: ({ currentArg, endpointState }) => { - const currentData = endpointState?.data ? Object.keys(endpointState.data).map(Number) : undefined - if (!currentData) { return true } - const dataInCache = currentArg?.ids.every(id => currentData.includes(id)) - return !dataInCache ? true : false + // const currentData = endpointState?.data ? Object.keys(endpointState.data).map(Number) : undefined + // if (!currentData) { return true } + // const dataInCache = currentArg?.ids.every(id => currentData.includes(id)) + // return !dataInCache ? true : false + const currentData = endpointState?.data ? Object.keys(endpointState.data).map(Number) : []; + return !currentArg?.ids.every(id => currentData.includes(id)); }, queryFn: async (args, queryApi, _extra, baseQuery) => { - const { ids, timeInterval, unitID, meterOrGroup, barWidthDays } = args + const { ids, meterOrGroup, ...params } = args const state = queryApi.getState() as RootState const cachedData = readingsApi.endpoints.bar.select(args)(state).data const cachedIDs = cachedData ? Object.keys(cachedData).map(Number) : [] const idsToFetch = _.difference(ids, cachedIDs).join(',') - const endpoint = `api/unitReadings/bar/${meterOrGroup}/${idsToFetch}?` - const queryArgs = `timeInterval=${timeInterval}&barWidthDays=${barWidthDays}&graphicUnitId=${unitID}` - const endpointURL = `${endpoint}${queryArgs}` - const { data, error } = await baseQuery(endpointURL) + const { data, error } = await baseQuery({ url: `api/unitReadings/bar/${meterOrGroup}/${idsToFetch}`, params }) return error ? { error } : { data: data as BarReadings } } }), @@ -125,21 +117,17 @@ export const readingsApi = baseApi.injectEndpoints({ serializeQueryArgs: ({ queryArgs }) => _.omit(queryArgs, 'ids'), merge: (currentCacheData, responseData) => { Object.assign(currentCacheData, responseData) }, forceRefetch: ({ currentArg, endpointState }) => { - const currentData = endpointState?.data ? Object.keys(endpointState.data).map(Number) : undefined - if (!currentData) { return true } - const dataInCache = currentArg?.ids.every(id => currentData.includes(id)) - return !dataInCache ? true : false + const currentData = endpointState?.data ? Object.keys(endpointState.data).map(Number) : [] + const requestedAlreadyCached = currentArg?.ids.every(id => currentData.includes(id)) + return !requestedAlreadyCached }, queryFn: async (args, queryApi, _extra, baseQuery) => { - const { ids, curr_start, curr_end, shift, unitID, meterOrGroup } = args + const { ids, meterOrGroup, ...params } = args const state = queryApi.getState() as RootState const cachedData = readingsApi.endpoints.compare.select(args)(state).data const cachedIDs = cachedData ? Object.keys(cachedData).map(Number) : [] const idsToFetch = _.difference(ids, cachedIDs).join(',') - const apiURL = `/api/compareReadings/${meterOrGroup}/${idsToFetch}?` - const params = `curr_start=${curr_start}&curr_end=${curr_end}&shift=${shift}&graphicUnitId=${unitID}` - const URL = `${apiURL}${params}` - const { data, error } = await baseQuery(URL) + const { data, error } = await baseQuery({ url: `/api/compareReadings/${meterOrGroup}/${idsToFetch}`, params }) return error ? { error } : { data: data as CompareReadings } } }) diff --git a/src/client/app/redux/api/unitsApi.ts b/src/client/app/redux/api/unitsApi.ts index 348d35b5a..50ce54d3c 100644 --- a/src/client/app/redux/api/unitsApi.ts +++ b/src/client/app/redux/api/unitsApi.ts @@ -1,15 +1,19 @@ -import * as _ from 'lodash'; +import { EntityState, createEntityAdapter } from '@reduxjs/toolkit'; import { RootState } from 'store'; -import { UnitData, UnitDataById } from '../../types/redux/units'; +import { UnitData } from '../../types/redux/units'; import { baseApi } from './baseApi'; -import { createSelector } from '@reduxjs/toolkit'; +export const unitsAdapter = createEntityAdapter({ + sortComparer: (a, b) => a.identifier.localeCompare(b.identifier, undefined, { sensitivity:'base' }) +}); +export const unitsInitialState = unitsAdapter.getInitialState(); +export type UnitDataState = EntityState; export const unitsApi = baseApi.injectEndpoints({ endpoints: builder => ({ - getUnitsDetails: builder.query({ + getUnitsDetails: builder.query({ query: () => 'api/units', transformResponse: (response: UnitData[]) => { - return _.keyBy(response, unit => unit.id) + return unitsAdapter.setAll(unitsInitialState, response) } }), addUnit: builder.mutation({ @@ -40,36 +44,12 @@ export const unitsApi = baseApi.injectEndpoints({ * const queryState = useAppSelector(state => selectUnitDataByIdQueryState(state)) * const {data: unitDataById = {}} = useAppSelector(state => selectUnitDataById(state)) */ -export const selectUnitDataByIdQueryState = unitsApi.endpoints.getUnitsDetails.select() +export const selectUnitDataResult = unitsApi.endpoints.getUnitsDetails.select() +export const { + selectAll: selectAllUnits, + selectById: selectUnitById, + selectTotal: selectUnitTotal, + selectIds: selectUnitIds, + selectEntities: selectUnitDataById +} = unitsAdapter.getSelectors((state: RootState) => selectUnitDataResult(state).data ?? unitsInitialState) -/** - * Selects the most recent query status - * @param state - The complete state of the redux store. - * @returns The unit data corresponding to the `unitID` if found, or undefined if not. - * @example - * - * const unitDataById = useAppSelector(state =>selectUnitDataById(state)) - * const unitDataById = useAppSelector(selectUnitDataById) - */ -export const selectUnitDataById = createSelector( - selectUnitDataByIdQueryState, - ({ data: unitDataById = {} }) => { - return unitDataById - } -) - -/** - * Selects a unit from the state by its unique identifier. - * @param state - The complete state of the redux store. - * @param unitID - The unique identifier for the unit to be retrieved. - * @returns The unit data corresponding to the `unitID` if found, or undefined if not. - * @example - * - * // Get Unit Data for unit with ID of '1' - * const unit = useAppSelector(state => selectUnitWithID(state, 1)) - */ -export const selectUnitWithID = (state: RootState, unitID: number) => { - const unitDataById = selectUnitDataById(state) - return unitDataById[unitID] - -} diff --git a/src/client/app/redux/api/userApi.ts b/src/client/app/redux/api/userApi.ts index b5ab97b16..4a6dab595 100644 --- a/src/client/app/redux/api/userApi.ts +++ b/src/client/app/redux/api/userApi.ts @@ -25,9 +25,7 @@ export const userApi = baseApi.injectEndpoints({ query: users => ({ url: 'api/users/edit', method: 'POST', - body: { users }, - // Response not json. Use 'text' responseHandler to parsing errors avoid - responseHandler: 'text' + body: { users } }), invalidatesTags: ['Users'] }), diff --git a/src/client/app/redux/selectors/adminSelectors.ts b/src/client/app/redux/selectors/adminSelectors.ts index a560bb737..7263b4410 100644 --- a/src/client/app/redux/selectors/adminSelectors.ts +++ b/src/client/app/redux/selectors/adminSelectors.ts @@ -2,8 +2,8 @@ import { createSelector } from '@reduxjs/toolkit' import * as _ from 'lodash' import { selectAdminState } from '../../reducers/admin' import { selectConversionsDetails } from '../../redux/api/conversionsApi' -import { selectGroupDataById } from '../../redux/api/groupsApi' -import { selectMeterDataById, selectMeterDataWithID } from '../../redux/api/metersApi' +import { selectAllGroups } from '../../redux/api/groupsApi' +import { selectAllMeters, selectMeterById } from '../../redux/api/metersApi' import { RootState } from '../../store' import { PreferenceRequestItem } from '../../types/items' import { ConversionData } from '../../types/redux/conversions' @@ -13,7 +13,7 @@ import { unitsCompatibleWithUnit } from '../../utils/determineCompatibleUnits' import { AreaUnitType } from '../../utils/getAreaUnitConversion' import { noUnitTranslated, potentialGraphicUnits } from '../../utils/input' import translate from '../../utils/translate' -import { selectUnitDataById } from '../api/unitsApi' +import { selectAllUnits, selectUnitDataById } from '../api/unitsApi' import { selectVisibleMetersAndGroups } from './authVisibilitySelectors' export const selectAdminPreferences = createSelector( @@ -56,11 +56,11 @@ export const selectPossibleGraphicUnits = createSelector( * @returns The set of all possible graphic units for a meter */ export const selectPossibleMeterUnits = createSelector( - selectUnitDataById, - unitDataById => { + selectAllUnits, + unitData => { let possibleMeterUnits = new Set(); // The meter unit can be any unit of type meter. - Object.values(unitDataById).forEach(unit => { + unitData.forEach(unit => { if (unit.typeOfUnit == UnitType.meter) { possibleMeterUnits.add(unit); } @@ -87,7 +87,7 @@ export const selectUnitName = createSelector( // The second test of -99 is for meters without units. // ThisSelector takes an argument, due to one or more of the selectors accepts an argument (selectUnitWithID selectMeterDataWithID) selectUnitDataById, - selectMeterDataWithID, + selectMeterById, (unitDataById, meterData) => { const unitName = (Object.keys(unitDataById).length === 0 || !meterData || meterData.unitId === -99) ? noUnitTranslated().identifier : unitDataById[meterData.defaultGraphicUnit].identifier @@ -108,7 +108,7 @@ export const selectGraphicName = createSelector( // This is the default graphic unit associated with the meter. See above for how code works. // notice that this selector is written with inline selectors for demonstration purposes selectUnitDataById, - selectMeterDataWithID, + selectMeterById, (unitDataById, meterData) => { const graphicName = (Object.keys(unitDataById).length === 0 || !meterData || meterData.defaultGraphicUnit === -99) ? noUnitTranslated().identifier : unitDataById[meterData.defaultGraphicUnit].identifier @@ -249,34 +249,31 @@ export const selectIsValidConversion = createSelector( console.log('Seems to Break about here!') - let isValid = true; - // Loop over conversions and check for existence of inverse of conversion passed in - // If there exists an inverse that is bidirectional, then there is no point in making a conversion since it is essentially a duplicate. // If there is a non bidirectional inverse, then it is a valid conversion - Object.values(conversions).forEach(conversion => { - // Do not allow for a bidirectional conversion with an inverse that is not bidirectional - // Inverse exists - if ((conversion.sourceId === destinationId) && (conversion.destinationId === sourceId) - && - // Inverse is bidirectional or new conversion is bidirectional - (conversion.bidirectional || bidirectional)) { - isValid = false; + for (const conversion of Object.values(conversions)) { + // Loop over conversions and check for existence of inverse of conversion passed in + const inverseExists = (conversion.sourceId === destinationId) && (conversion.destinationId === sourceId) + const isBidirectional = conversion.bidirectional || bidirectional + + // If there exists an inverse that is bidirectional, then there is no point in making a conversion since it is essentially a duplicate. + if (inverseExists && isBidirectional) { + return [false, translate('conversion.create.exists.inverse')] } - }); + } console.log('Conversion never seems to get here? ') - return isValid ? [isValid, 'Conversion is Valid'] : [false, translate('conversion.create.exists.inverse')] + return [true, 'Conversion is Valid'] } ) export const selectVisibleMeterAndGroupDataByID = createSelector( selectVisibleMetersAndGroups, - selectMeterDataById, - selectGroupDataById, - (visible, meterDataById, groupDataById) => { - const visibleMeters = Object.values(meterDataById).filter(meterData => visible.meters.has(meterData.id)) - const visibleGroups = Object.values(groupDataById).filter(groupData => visible.groups.has(groupData.id)) + selectAllMeters, + selectAllGroups, + (visible, meterData, groupData) => { + const visibleMeters = meterData.filter(meterData => visible.meters.has(meterData.id)) + const visibleGroups = groupData.filter(groupData => visible.groups.has(groupData.id)) return { visibleMeters, visibleGroups } } ) @@ -294,6 +291,7 @@ export const selectDefaultCreateMeterValues = createSelector( meterType: '', url: '', timeZone: '', + // String type conflicts with MeterDataType GPSPoint gps: '', // Defaults of -999 (not to be confused with -99 which is no unit) // Purely for allowing the default select to be "select a ..." @@ -326,3 +324,22 @@ export const selectDefaultCreateMeterValues = createSelector( return defaultValues } ) + +export const selectDefaultCreateConversionValues = createSelector( + selectAllUnits, + sortedUnitData => { + const defaultValues = { + // Invalid source/destination ids arbitrarily set to -999. + // Meter Units are not allowed to be a destination. + sourceId: -999, + sourceOptions: sortedUnitData, + destinationId: -999, + destinationOptions: sortedUnitData.filter(unit => unit.typeOfUnit !== 'meter'), + bidirectional: true, + slope: 0, + intercept: 0, + note: '' + } + return defaultValues + } +) \ No newline at end of file diff --git a/src/client/app/redux/selectors/authVisibilitySelectors.ts b/src/client/app/redux/selectors/authVisibilitySelectors.ts index 88e8b7370..1cb07ae38 100644 --- a/src/client/app/redux/selectors/authVisibilitySelectors.ts +++ b/src/client/app/redux/selectors/authVisibilitySelectors.ts @@ -1,32 +1,30 @@ import { createSelector } from '@reduxjs/toolkit'; import * as _ from 'lodash'; -import { selectGroupDataById } from '../api/groupsApi'; -import { selectMeterDataById } from '../api/metersApi'; -import { selectUnitDataById } from '../api/unitsApi'; -import { DisplayableType, UnitType } from '../../types/redux/units'; import { selectIsAdmin } from '../../reducers/currentUser'; +import { selectAllMeters } from '../../redux/api/metersApi'; +import { DisplayableType, UnitType } from '../../types/redux/units'; +import { selectAllGroups } from '../api/groupsApi'; +import { selectUnitDataById } from '../api/unitsApi'; export const selectVisibleMetersAndGroups = createSelector( - selectMeterDataById, - selectGroupDataById, + selectAllMeters, + selectAllGroups, selectIsAdmin, (meterDataByID, groupDataById, isAdmin) => { // Holds all meters visible to the user const meters = new Set(); const groups = new Set(); - Object.values(meterDataByID) - .forEach(meter => { - if (isAdmin || meter.displayable) { - meters.add(meter.id); - } - }); - Object.values(groupDataById) - .forEach(group => { - if (isAdmin || group.displayable) { - groups.add(group.id); - } - }); + meterDataByID.forEach(meter => { + if (isAdmin || meter.displayable) { + meters.add(meter.id); + } + }); + groupDataById.forEach(group => { + if (isAdmin || group.displayable) { + groups.add(group.id); + } + }); return { meters, groups } } ); diff --git a/src/client/app/redux/selectors/chartQuerySelectors.ts b/src/client/app/redux/selectors/chartQuerySelectors.ts index 61145ddea..da115d5ad 100644 --- a/src/client/app/redux/selectors/chartQuerySelectors.ts +++ b/src/client/app/redux/selectors/chartQuerySelectors.ts @@ -1,4 +1,5 @@ import { createSelector } from '@reduxjs/toolkit'; +import * as _ from 'lodash'; import * as moment from 'moment'; import { RootState } from 'store'; import { TimeInterval } from '../../../../common/TimeInterval'; @@ -16,7 +17,7 @@ import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; export interface commonQueryArgs { ids: number[]; timeInterval: string; - unitID: number; + graphicUnitId: number; meterOrGroup: MeterOrGroup; } @@ -45,7 +46,7 @@ export const selectCommonQueryArgs = createSelector( const meterArgs = { ids: selectedMeters, timeInterval: queryTimeInterval.toString(), - unitID: selectedUnit, + graphicUnitId: selectedUnit, meterOrGroup: MeterOrGroup.meters } @@ -53,7 +54,7 @@ export const selectCommonQueryArgs = createSelector( const groupArgs = { ids: selectedGroups, timeInterval: queryTimeInterval.toString(), - unitID: selectedUnit, + graphicUnitId: selectedUnit, meterOrGroup: MeterOrGroup.groups } const meterSkip = !meterArgs.ids.length; @@ -106,12 +107,12 @@ export const selectCompareChartQueryArgs = createSelector( curr_end: compareTimeInterval.getEndTimestamp()?.toISOString() } const meterArgs: CompareReadingApiArgs = { - ...common.meterArgs, + ..._.omit(common.meterArgs, 'timeInterval'), ...compareArgs } const groupArgs: CompareReadingApiArgs = { - ...common.groupArgs, + ..._.omit(common.groupArgs, 'timeInterval'), ...compareArgs } const meterShouldSkip = common.meterSkip; @@ -161,7 +162,7 @@ export const selectThreeDQueryArgs = createSelector( const args: ThreeDReadingApiArgs = { id: threeD.meterOrGroupID!, timeInterval: roundTimeIntervalForFetch(queryTimeInterval).toString(), - unitID: selectedUnit, + graphicUnitId: selectedUnit, readingInterval: threeD.readingInterval, meterOrGroup: threeD.meterOrGroup! } diff --git a/src/client/app/redux/selectors/threeDSelectors.ts b/src/client/app/redux/selectors/threeDSelectors.ts index 7b0e4b6d8..773d34001 100644 --- a/src/client/app/redux/selectors/threeDSelectors.ts +++ b/src/client/app/redux/selectors/threeDSelectors.ts @@ -3,9 +3,9 @@ import { selectThreeDMeterOrGroup, selectThreeDMeterOrGroupID } from '../../reducers/graph'; import { selectGroupDataById } from '../../redux/api/groupsApi'; -import { selectMeterDataById } from '../../redux/api/metersApi'; import { MeterOrGroup } from '../../types/redux/graph'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; +import { selectMeterDataById } from '../../redux/api/metersApi'; // Memoized Selectors diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts index 89f5e4791..79161e1f1 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -15,19 +15,17 @@ import { } from '../../utils/calibration'; import { metersInGroup, unitsCompatibleWithMeters } from '../../utils/determineCompatibleUnits'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; - import { selectChartToRender, selectGraphAreaNormalization, selectSelectedGroups, selectSelectedMeters, selectSelectedUnit } from '../../reducers/graph'; - import { selectGroupDataById } from '../../redux/api/groupsApi'; import { selectUnitDataById } from '../../redux/api/unitsApi'; -import { selectMeterDataById } from '../api/metersApi'; import { selectVisibleMetersAndGroups, selectVisibleUnitOrSuffixState } from './authVisibilitySelectors'; import { MeterDataByID } from 'types/redux/meters'; import { GroupDataByID } from 'types/redux/groups'; +import { selectMeterDataById } from '../../redux/api/metersApi'; diff --git a/src/client/app/utils/determineCompatibleUnits.ts b/src/client/app/utils/determineCompatibleUnits.ts index 32f8d0eb2..1cc94a9f9 100644 --- a/src/client/app/utils/determineCompatibleUnits.ts +++ b/src/client/app/utils/determineCompatibleUnits.ts @@ -5,14 +5,14 @@ import * as _ from 'lodash'; import React from 'react'; import { selectPik } from '../redux/api/conversionsApi'; -import { selectGroupDataById } from '../redux/api/groupsApi'; -import { selectMeterDataById } from '../redux/api/metersApi'; +import { selectAllGroups, selectGroupDataById } from '../redux/api/groupsApi'; import { selectUnitDataById } from '../redux/api/unitsApi'; import { store } from '../store'; import { DataType } from '../types/Datasources'; import { SelectOption } from '../types/items'; import { GroupData } from '../types/redux/groups'; import { UnitData, UnitType } from '../types/redux/units'; +import { selectAllMeters, selectMeterDataById } from '../redux/api/metersApi'; /** @@ -197,12 +197,12 @@ export function getMeterMenuOptionsForGroup(defaultGraphicUnit: number, deepMete // Get the units that are compatible with this set of meters. const currentUnits = unitsCompatibleWithMeters(deepMetersSet); // Get all meters' state. - const meterDataById = selectMeterDataById(state) + const meterData = selectAllMeters(state) // Options for the meter menu. const options: SelectOption[] = []; // For each meter, decide its compatibility for the menu - Object.values(meterDataById).forEach(meter => { + meterData.forEach(meter => { const option = { label: meter.identifier, value: meter.id, @@ -240,12 +240,12 @@ export function getGroupMenuOptionsForGroup(groupId: number, defaultGraphicUnit: // Get the currentGroup's compatible units. const currentUnits = unitsCompatibleWithMeters(deepMetersSet); // Get all groups' state. - const groupDataById = selectGroupDataById(store.getState()); + const groupData = selectAllGroups(store.getState()); // Options for the group menu. const options: SelectOption[] = []; - Object.values(groupDataById).forEach(group => { + groupData.forEach(group => { // You cannot have yourself in the group so not an option. if (group.id !== groupId) { const option = { From 2bdf17d70637a302e3b7a0fb2768d6b772d1f49e Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Tue, 14 Nov 2023 03:33:31 +0000 Subject: [PATCH 044/131] PRSTASH --- package-lock.json | 38 ++++----- package.json | 2 +- .../app/components/DashboardComponent.tsx | 1 - .../app/components/RouteComponentWIP.tsx | 84 ++++++------------- .../csv/MetersCSVUploadComponent.tsx | 3 +- .../groups/GroupsDetailComponent.tsx | 4 +- .../groups/GroupsDetailComponentWIP.tsx | 4 +- .../meters/MetersDetailComponent.tsx | 4 +- .../meters/MetersDetailComponentWIP.tsx | 4 +- src/client/app/initScript.ts | 2 +- src/client/app/reducers/appStateSlice.ts | 4 +- src/client/app/reducers/currentUser.ts | 20 ++--- src/client/app/reducers/graph.ts | 2 +- src/client/app/redux/api/groupsApi.ts | 2 +- src/client/app/redux/api/metersApi.ts | 2 +- src/client/app/redux/api/unitsApi.ts | 3 +- src/client/app/redux/componentHooks.ts | 10 +++ .../app/redux/selectors/adminSelectors.ts | 2 +- src/client/app/redux/selectors/uiSelectors.ts | 2 +- .../app/utils/determineCompatibleUnits.ts | 6 +- 20 files changed, 88 insertions(+), 111 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5c8a00cc0..2cf98968b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MPL-2.0", "dependencies": { - "@reduxjs/toolkit": "~2.0.0-beta.4", + "@reduxjs/toolkit": "~2.0.0-rc.0", "@wojtekmaj/react-daterange-picker": "~5.2.0", "axios": "~0.24.0", "bcryptjs": "~2.4.3", @@ -2542,18 +2542,18 @@ } }, "node_modules/@reduxjs/toolkit": { - "version": "2.0.0-beta.4", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.0.0-beta.4.tgz", - "integrity": "sha512-Bh53FuSfh0eeXIBzoWkB6XdBt61Pxk3xHsUmzcz4aMr5l3Tu1YcPx0KxRJqKJUnTU/I9k92536cv7d4CGU/oOg==", + "version": "2.0.0-rc.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.0.0-rc.0.tgz", + "integrity": "sha512-+C1pCdur4w1EMRjbISjbS4cuuhbm0ZQ8f+6G4Q2QUAynie9H0Q36JVCVnPYvTO320FBunSurbwKC7DAsuiwWKQ==", "dependencies": { - "immer": "^10.0.2", - "redux": "^5.0.0-beta.0", - "redux-thunk": "^3.0.0-beta.0", - "reselect": "^5.0.0-beta.0" + "immer": "^10.0.3", + "redux": "^5.0.0-rc.0", + "redux-thunk": "^3.0.0-rc.0", + "reselect": "^5.0.0-beta.1" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18", - "react-redux": "^7.2.1 || ^8.0.2 || ^9.0.0-beta.0" + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0-rc.0" }, "peerDependenciesMeta": { "react": { @@ -2565,16 +2565,16 @@ } }, "node_modules/@reduxjs/toolkit/node_modules/redux": { - "version": "5.0.0-beta.0", - "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.0-beta.0.tgz", - "integrity": "sha512-RHSGHIiJ+1nkuve0daeveubiEdloy+DkYkP63uHk2FHpP18kb5umytsPU8TY8Lw8sLjL1eFg0DD5yf99ry/JhA==" + "version": "5.0.0-rc.0", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.0-rc.0.tgz", + "integrity": "sha512-QBfAlLf1FPQLbsrkTcfWdCijzN284/fQEiAkjS/FTlq244fSBkrh0mDsMQrCIfxvRr2tOC3NQvNUJjYV+izUcA==" }, "node_modules/@reduxjs/toolkit/node_modules/redux-thunk": { - "version": "3.0.0-beta.0", - "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.0.0-beta.0.tgz", - "integrity": "sha512-BLed4FtBhPv52AgqeR7DiOhrDA8z6owqXOkObOqgl1kwq4QQ1T74dy32qxyWsdyAlvq9wAHHW6t4tlxz8XnFhA==", + "version": "3.0.0-rc.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.0.0-rc.0.tgz", + "integrity": "sha512-2NcCM9cZZT+mrAISy0LWYKX4HjeMg7o8vcs/JgZCx8o+3/sXmzW7tk64Xvc6wTQwIWinpQCTX6eHNGCF4en/nA==", "peerDependencies": { - "redux": "^4 || ^5.0.0-beta.0" + "redux": "^5.0.0-rc.0" } }, "node_modules/@remix-run/router": { @@ -10874,9 +10874,9 @@ "peer": true }, "node_modules/reselect": { - "version": "5.0.0-beta.0", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.0.0-beta.0.tgz", - "integrity": "sha512-q9cwinGYNn3xNtyknjmNZQvH6FYdxS0tqUin1MIrtMLt1QB17FFz/C2WhlPgYBYuKJe9K/+bTKALuYXn+Elp1g==" + "version": "5.0.0-beta.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.0.0-beta.1.tgz", + "integrity": "sha512-DxH9DLLbaiuLI4gCM8kSfonXtqd9Lh1BsNQhZByr3pHb3c75eFfgxb4vNJxeTgHFn27aLlt3tX0Xh+IqvJsyuw==" }, "node_modules/resolve": { "version": "1.22.6", diff --git a/package.json b/package.json index 4c4ba55ef..e7c868942 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "babel-plugin-lodash": "~3.3.4" }, "dependencies": { - "@reduxjs/toolkit": "~2.0.0-beta.4", + "@reduxjs/toolkit": "~2.0.0-rc.0", "@wojtekmaj/react-daterange-picker": "~5.2.0", "axios": "~0.24.0", "bcryptjs": "~2.4.3", diff --git a/src/client/app/components/DashboardComponent.tsx b/src/client/app/components/DashboardComponent.tsx index 92dc2d7bf..2bf3493e0 100644 --- a/src/client/app/components/DashboardComponent.tsx +++ b/src/client/app/components/DashboardComponent.tsx @@ -20,7 +20,6 @@ import UIOptionsComponent from './UIOptionsComponent'; export default function DashboardComponent() { const chartToRender = useAppSelector(state => state.graph.chartToRender); const optionsVisibility = useAppSelector(state => state.graph.optionsVisibility); - const optionsClassName = optionsVisibility ? 'col-2 d-none d-lg-block' : 'd-none'; const chartClassName = optionsVisibility ? 'col-12 col-lg-10' : 'col-12'; // const optionsClassName = optionsVisibility ? 'col-3 d-none d-lg-block' : 'd-none'; diff --git a/src/client/app/components/RouteComponentWIP.tsx b/src/client/app/components/RouteComponentWIP.tsx index ed40ee1ff..36ee89e7c 100644 --- a/src/client/app/components/RouteComponentWIP.tsx +++ b/src/client/app/components/RouteComponentWIP.tsx @@ -12,9 +12,8 @@ import CreateUserContainer from '../containers/admin/CreateUserContainer'; import UploadCSVContainer from '../containers/csv/UploadCSVContainer'; import MapCalibrationContainer from '../containers/maps/MapCalibrationContainer'; import MapsDetailContainer from '../containers/maps/MapsDetailContainer'; -import { selectCurrentUser, selectIsAdmin } from '../reducers/currentUser'; import { graphSlice } from '../reducers/graph'; -import { baseApi } from '../redux/api/baseApi'; +import { useWaitForInit } from '../redux/componentHooks'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; import LocaleTranslationData from '../translations/data'; import { UserRole } from '../types/items'; @@ -23,8 +22,8 @@ import { validateComparePeriod, validateSortingOrder } from '../utils/calculateC import { AreaUnitType } from '../utils/getAreaUnitConversion'; import { showErrorNotification } from '../utils/notifications'; import translate from '../utils/translate'; -import HomeComponent from './HomeComponent'; import AppLayout from './AppLayout'; +import HomeComponent from './HomeComponent'; import LoginComponent from './LoginComponent'; import SpinnerComponent from './SpinnerComponent'; import AdminComponent from './admin/AdminComponent'; @@ -37,73 +36,41 @@ import UnitsDetailComponent from './unit/UnitsDetailComponent'; -const useWaitForInit = () => { - const dispatch = useAppDispatch(); - const isAdmin = useAppSelector(selectIsAdmin); - const currentUser = useAppSelector(state => selectCurrentUser(state)); - const [initComplete, setInitComplete] = React.useState(false); - - React.useEffect(() => { - // Initialization sequence if not navigating here from the ui. - // E.g entering 'localhost:3000/groups' into the browser nav bar etc.. - const waitForInit = async () => { - await Promise.all(dispatch(baseApi.util.getRunningQueriesThunk())) - setInitComplete(true) - // TODO Startup Crashing fixed, authen - } - - waitForInit(); - }, [dispatch]); - return { isAdmin, currentUser, initComplete } -} - export const AdminOutlet = () => { - const { isAdmin - // , initComplete - } = useWaitForInit(); + const { isAdmin, initComplete } = useWaitForInit(); - // if (!initComplete) { - // // Return a spinner until all init queries return and populate cache with data - // return - // } + if (!initComplete) { + // Return a spinner until all init queries return and populate cache with data + return + } // Keeping for now in case changes are desired if (isAdmin) { return } - return - // For now this functionality is disabled. - // If no longer desired can remove this and close Issue #817 - // No other cases means user doesn't have the permissions. - // return + return } // Function that returns a JSX element. Either the requested route's Component, as outlet or back to root export const RoleOutlet = ({ UserRole }: { UserRole: UserRole }) => { - const { currentUser - // , initComplete - } = useWaitForInit(); + const { userRole, initComplete } = useWaitForInit(); // // If state contains token it has been validated on startup or login. - // if (!initComplete) { - // return - // } - - + if (!initComplete) { + return + } // Keeping for now in case changes are desired - if (currentUser.profile?.role === UserRole) { + if (userRole === UserRole) { return } - // If no longer desired can remove this and close Issue #817 - // For now this functionality is disabled. - // return - return + + return } export const NotFound = () => { // redirect to home page if non-existent route is requested. - return + return } @@ -120,12 +87,6 @@ export const GraphLink = () => { URLSearchParams.forEach((value, key) => { //TODO validation could be implemented across all cases similar to compare period and sorting order switch (key) { - case 'meterIDs': - dispatchQueue.push(graphSlice.actions.updateSelectedMeters(value.split(',').map(s => parseInt(s)))) - break; - case 'groupIDs': - dispatchQueue.push(graphSlice.actions.updateSelectedGroups(value.split(',').map(s => parseInt(s)))) - break; case 'chartType': dispatchQueue.push(graphSlice.actions.changeChartToRender(value as ChartTypes)) break; @@ -193,6 +154,12 @@ export const GraphLink = () => { case 'readingInterval': dispatchQueue.push(graphSlice.actions.updateThreeDReadingInterval(parseInt(value))); break; + case 'meterIDs': + dispatchQueue.push(graphSlice.actions.updateSelectedMeters(value.split(',').map(s => parseInt(s)))) + break; + case 'groupIDs': + dispatchQueue.push(graphSlice.actions.updateSelectedGroups(value.split(',').map(s => parseInt(s)))) + break; default: throw new Error('Unknown query parameter'); } @@ -204,7 +171,7 @@ export const GraphLink = () => { // All appropriate state updates should've been executed // redirect to clear the link - return + return } @@ -216,6 +183,9 @@ const router = createBrowserRouter([ children: [ { index: true, element: }, { path: '/login', element: }, + { path: 'groups', element: }, + { path: 'meters', element: }, + { path: 'graph', element: }, { path: '/', element: , @@ -226,8 +196,6 @@ const router = createBrowserRouter([ { path: 'users/new', element: }, { path: 'units', element: }, { path: 'conversions', element: }, - { path: 'groups', element: }, - { path: 'meters', element: }, { path: 'users', element: }, { path: '/', diff --git a/src/client/app/components/csv/MetersCSVUploadComponent.tsx b/src/client/app/components/csv/MetersCSVUploadComponent.tsx index 851ff7937..e4889dbf8 100644 --- a/src/client/app/components/csv/MetersCSVUploadComponent.tsx +++ b/src/client/app/components/csv/MetersCSVUploadComponent.tsx @@ -5,7 +5,6 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { Button, Form, FormGroup, Input, Label } from 'reactstrap'; -import { fetchMetersDetails } from '../../actions/meters'; import { MODE } from '../../containers/csv/UploadCSVContainer'; import { MetersCSVUploadProps } from '../../types/csvUploadForm'; import FormFileUploaderComponent from '../FormFileUploaderComponent'; @@ -38,7 +37,7 @@ export default class MetersCSVUploadComponent extends React.Component) { diff --git a/src/client/app/components/groups/GroupsDetailComponent.tsx b/src/client/app/components/groups/GroupsDetailComponent.tsx index b6c31c797..dcec4050d 100644 --- a/src/client/app/components/groups/GroupsDetailComponent.tsx +++ b/src/client/app/components/groups/GroupsDetailComponent.tsx @@ -7,7 +7,7 @@ import { FormattedMessage } from 'react-intl'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { useAppSelector } from '../../redux/hooks'; -import { selectVisibleMeterAndGroupDataByID } from '../../redux/selectors/adminSelectors'; +import { selectVisibleMeterAndGroupData } from '../../redux/selectors/adminSelectors'; import { potentialGraphicUnits } from '../../utils/input'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateGroupModalComponent from './CreateGroupModalComponent'; @@ -26,7 +26,7 @@ export default function GroupsDetailComponent() { const isAdmin = useAppSelector(state => selectIsAdmin(state)); // We only want displayable groups if non-admins because they still have non-displayable in state. - const { visibleGroups } = useAppSelector(state => selectVisibleMeterAndGroupDataByID(state)); + const { visibleGroups } = useAppSelector(state => selectVisibleMeterAndGroupData(state)); // Units state const unitDataById = useAppSelector(selectUnitDataById); diff --git a/src/client/app/components/groups/GroupsDetailComponentWIP.tsx b/src/client/app/components/groups/GroupsDetailComponentWIP.tsx index 3361a3579..ef5c9e546 100644 --- a/src/client/app/components/groups/GroupsDetailComponentWIP.tsx +++ b/src/client/app/components/groups/GroupsDetailComponentWIP.tsx @@ -7,7 +7,7 @@ import { FormattedMessage } from 'react-intl'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { useAppSelector } from '../../redux/hooks'; import { selectIsAdmin } from '../../reducers/currentUser'; -import { selectVisibleMeterAndGroupDataByID } from '../../redux/selectors/adminSelectors'; +import { selectVisibleMeterAndGroupData } from '../../redux/selectors/adminSelectors'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateGroupModalComponentWIP from './CreateGroupModalComponentWIP'; import GroupViewComponentWIP from './GroupViewComponentWIP'; @@ -22,7 +22,7 @@ export default function GroupsDetailComponentWIP() { const isAdmin = useAppSelector(state => selectIsAdmin(state)); // We only want displayable groups if non-admins because they still have non-displayable in state. - const { visibleGroups } = useAppSelector(state => selectVisibleMeterAndGroupDataByID(state)); + const { visibleGroups } = useAppSelector(state => selectVisibleMeterAndGroupData(state)); diff --git a/src/client/app/components/meters/MetersDetailComponent.tsx b/src/client/app/components/meters/MetersDetailComponent.tsx index afa5f36db..a6d635f02 100644 --- a/src/client/app/components/meters/MetersDetailComponent.tsx +++ b/src/client/app/components/meters/MetersDetailComponent.tsx @@ -7,7 +7,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { useAppSelector } from '../../redux/hooks'; -import { selectVisibleMeterAndGroupDataByID } from '../../redux/selectors/adminSelectors'; +import { selectVisibleMeterAndGroupData } from '../../redux/selectors/adminSelectors'; import '../../styles/card-page.css'; import { MeterData } from '../../types/redux/meters'; import { UnitData, UnitType } from '../../types/redux/units'; @@ -32,7 +32,7 @@ export default function MetersDetailComponent() { // We only want displayable meters if non-admins because they still have // non-displayable in state. - const { visibleMeters } = useAppSelector(selectVisibleMeterAndGroupDataByID); + const { visibleMeters } = useAppSelector(selectVisibleMeterAndGroupData); // Units state const unitDataById = useAppSelector(selectUnitDataById); diff --git a/src/client/app/components/meters/MetersDetailComponentWIP.tsx b/src/client/app/components/meters/MetersDetailComponentWIP.tsx index 0e2ce0028..96db39216 100644 --- a/src/client/app/components/meters/MetersDetailComponentWIP.tsx +++ b/src/client/app/components/meters/MetersDetailComponentWIP.tsx @@ -7,7 +7,7 @@ import { FormattedMessage } from 'react-intl'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { useAppSelector } from '../../redux/hooks'; import { selectIsAdmin } from '../../reducers/currentUser'; -import { selectVisibleMeterAndGroupDataByID } from '../../redux/selectors/adminSelectors'; +import { selectVisibleMeterAndGroupData } from '../../redux/selectors/adminSelectors'; import '../../styles/card-page.css'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateMeterModalComponentWIP from './CreateMeterModalComponentWIP'; @@ -23,7 +23,7 @@ export default function MetersDetailComponent() { const isAdmin = useAppSelector(state => selectIsAdmin(state)); // We only want displayable meters if non-admins because they still have // non-displayable in state. - const { visibleMeters } = useAppSelector(selectVisibleMeterAndGroupDataByID); + const { visibleMeters } = useAppSelector(selectVisibleMeterAndGroupData); return (
diff --git a/src/client/app/initScript.ts b/src/client/app/initScript.ts index 18b28c181..044353a61 100644 --- a/src/client/app/initScript.ts +++ b/src/client/app/initScript.ts @@ -47,7 +47,7 @@ export const initializeApp = async () => { } } - // Request meter/group/details + // Request meter/group/details post-auth store.dispatch(metersApi.endpoints.getMeters.initiate()) store.dispatch(groupsApi.endpoints.getGroups.initiate()) store.dispatch(appStateSlice.actions.setInitComplete(true)) diff --git a/src/client/app/reducers/appStateSlice.ts b/src/client/app/reducers/appStateSlice.ts index 9b86a5c21..ea4e0b4a0 100644 --- a/src/client/app/reducers/appStateSlice.ts +++ b/src/client/app/reducers/appStateSlice.ts @@ -45,6 +45,7 @@ export const appStateSlice = createSlice({ }) }), selectors: { + selectInitComplete: state => state.initComplete, selectBackHistoryStack: state => state.backHistoryStack, selectForwardHistoryStack: state => state.forwardHistoryStack, // Explicit return value required when calling sameSlice's getSelectors, otherwise circular type inference breaks TS. @@ -68,5 +69,6 @@ export const { export const { selectBackHistoryStack, selectForwardHistoryStack, - selectBackHistoryTop + selectBackHistoryTop, + selectInitComplete } = appStateSlice.selectors diff --git a/src/client/app/reducers/currentUser.ts b/src/client/app/reducers/currentUser.ts index e43357177..19546ccdb 100644 --- a/src/client/app/reducers/currentUser.ts +++ b/src/client/app/reducers/currentUser.ts @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSelector, createSlice } from '@reduxjs/toolkit'; +import { createSlice } from '@reduxjs/toolkit'; import { authApi } from '../redux/api/authApi'; import { userApi } from '../redux/api/userApi'; import { User, UserRole } from '../types/items'; @@ -53,19 +53,15 @@ export const currentUserSlice = createSlice({ }) }, selectors: { - selectCurrentUser: state => state + selectCurrentUser: state => state, + selectCurrentUserRole: state => state.profile?.role, + selectIsAdmin: state => Boolean(state.token && state.profile?.role === UserRole.ADMIN) // Should resolve to a boolean, Typescript doesn't agree so type assertion 'as boolean' } }) -export const { selectCurrentUser } = currentUserSlice.selectors - -// Memoized Selectors for stable obj reference from derived Values -export const selectIsAdmin = createSelector( +export const { selectCurrentUser, - currentUser => { - // True of token in state, and has Admin Role. - // Token If token is in state, it has been validated upon app initialization, or by login verification - return (currentUser.token && currentUser.profile?.role === UserRole.ADMIN) as boolean - } -) + selectCurrentUserRole, + selectIsAdmin +} = currentUserSlice.selectors diff --git a/src/client/app/reducers/graph.ts b/src/client/app/reducers/graph.ts index a4169c072..32f1d0353 100644 --- a/src/client/app/reducers/graph.ts +++ b/src/client/app/reducers/graph.ts @@ -305,4 +305,4 @@ export const { updateSelectedMetersOrGroups, resetTimeInterval, setGraphState -} = graphSlice.actions \ No newline at end of file +} = graphSlice.actions diff --git a/src/client/app/redux/api/groupsApi.ts b/src/client/app/redux/api/groupsApi.ts index 47bf6dc09..12179fed9 100644 --- a/src/client/app/redux/api/groupsApi.ts +++ b/src/client/app/redux/api/groupsApi.ts @@ -7,7 +7,7 @@ import { RootState } from '../../store'; import { GroupChildren, GroupData } from '../../types/redux/groups'; import { baseApi } from './baseApi'; export const groupsAdapter = createEntityAdapter({ - sortComparer: (groupA, groupB) => groupA.name.localeCompare(groupB.name) + sortComparer: (groupA, groupB) => groupA.name?.localeCompare(groupB.name, undefined, { sensitivity: 'accent' }) }) export const groupsInitialState = groupsAdapter.getInitialState() export type GroupDataState = EntityState diff --git a/src/client/app/redux/api/metersApi.ts b/src/client/app/redux/api/metersApi.ts index e4d493ff8..b856cb73d 100644 --- a/src/client/app/redux/api/metersApi.ts +++ b/src/client/app/redux/api/metersApi.ts @@ -9,7 +9,7 @@ import { baseApi } from './baseApi'; import { conversionsApi } from './conversionsApi'; export const meterAdapter = createEntityAdapter({ - sortComparer: (MeterA, MeterB) => MeterA.identifier.localeCompare(MeterB.identifier) + sortComparer: (meterA, meterB) => meterA.identifier?.localeCompare(meterB.identifier, undefined, { sensitivity: 'accent' }) }) export const metersInitialState = meterAdapter.getInitialState() diff --git a/src/client/app/redux/api/unitsApi.ts b/src/client/app/redux/api/unitsApi.ts index 50ce54d3c..533434890 100644 --- a/src/client/app/redux/api/unitsApi.ts +++ b/src/client/app/redux/api/unitsApi.ts @@ -3,7 +3,7 @@ import { RootState } from 'store'; import { UnitData } from '../../types/redux/units'; import { baseApi } from './baseApi'; export const unitsAdapter = createEntityAdapter({ - sortComparer: (a, b) => a.identifier.localeCompare(b.identifier, undefined, { sensitivity:'base' }) + sortComparer: (unitA, unitB) => unitA.identifier?.localeCompare(unitB.identifier, undefined, { sensitivity: 'accent' }) }); export const unitsInitialState = unitsAdapter.getInitialState(); export type UnitDataState = EntityState; @@ -14,6 +14,7 @@ export const unitsApi = baseApi.injectEndpoints({ query: () => 'api/units', transformResponse: (response: UnitData[]) => { return unitsAdapter.setAll(unitsInitialState, response) + return unitsAdapter.setAll(unitsInitialState, response) } }), addUnit: builder.mutation({ diff --git a/src/client/app/redux/componentHooks.ts b/src/client/app/redux/componentHooks.ts index 29102b184..31a9d1fc9 100644 --- a/src/client/app/redux/componentHooks.ts +++ b/src/client/app/redux/componentHooks.ts @@ -5,6 +5,16 @@ import { readingsApi } from './api/readingsApi'; import { useAppSelector } from './hooks'; import { selectAllChartQueryArgs } from './selectors/chartQuerySelectors'; import { unitsApi } from './api/unitsApi'; +import { selectInitComplete } from '../reducers/appStateSlice'; +import { selectIsAdmin, selectCurrentUserRole } from '../reducers/currentUser'; + + +export const useWaitForInit = () => { + const isAdmin = useAppSelector(selectIsAdmin); + const userRole = useAppSelector(selectCurrentUserRole); + const initComplete = useAppSelector(selectInitComplete); + return { isAdmin, userRole, initComplete } +} // General purpose custom hook mostly useful for Select component loadingIndicators, and current graph loading state(s) export const useFetchingStates = () => { diff --git a/src/client/app/redux/selectors/adminSelectors.ts b/src/client/app/redux/selectors/adminSelectors.ts index 7263b4410..cdb152e39 100644 --- a/src/client/app/redux/selectors/adminSelectors.ts +++ b/src/client/app/redux/selectors/adminSelectors.ts @@ -267,7 +267,7 @@ export const selectIsValidConversion = createSelector( } ) -export const selectVisibleMeterAndGroupDataByID = createSelector( +export const selectVisibleMeterAndGroupData = createSelector( selectVisibleMetersAndGroups, selectAllMeters, selectAllGroups, diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts index 79161e1f1..34f735bad 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -414,7 +414,7 @@ export function getSelectOptionsByItem( else if (type === 'meter') { label = dataById[itemId]?.identifier; meterOrGroup = MeterOrGroup.meters - defaultGraphicUnit = dataById[itemId].defaultGraphicUnit; + defaultGraphicUnit = dataById[itemId]?.defaultGraphicUnit; } else if (type === 'group') { label = dataById[itemId]?.name; diff --git a/src/client/app/utils/determineCompatibleUnits.ts b/src/client/app/utils/determineCompatibleUnits.ts index 1cc94a9f9..8c162bced 100644 --- a/src/client/app/utils/determineCompatibleUnits.ts +++ b/src/client/app/utils/determineCompatibleUnits.ts @@ -45,7 +45,8 @@ export function unitsCompatibleWithMeters(meters: Set): Set { // If meter had no unit then nothing compatible with it. // This probably won't happen but be safe. Note once you have one of these then // the final result must be empty set but don't check specially since don't expect. - if (meter.unitId != -99) { + // null meter can crash on startup without undef check here + if (meter && meter.unitId != -99) { // Set of compatible units with this meter. meterUnits = unitsCompatibleWithUnit(meter.unitId); } @@ -149,7 +150,8 @@ export function metersInGroup(groupId: number): Set { const groupDataById = selectGroupDataById(state) const group = _.get(groupDataById, groupId); // Create a set of the deep meters of this group and return it. - return new Set(group.deepMeters); + // null group can break on startup without optional chain + return new Set(group?.deepMeters); } /** From 6bc3d04fe8da406e4a172cef5612bf2f4aad0bc5 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Mon, 20 Nov 2023 05:10:59 +0000 Subject: [PATCH 045/131] Hist --- .../components/AreaUnitSelectComponent.tsx | 4 +- .../app/components/BarChartComponent.tsx | 17 +- .../app/components/BarControlsComponent.tsx | 6 +- .../app/components/ChartSelectComponent.tsx | 10 +- .../components/CompareControlsComponent.tsx | 6 +- .../app/components/DashboardComponent.tsx | 5 +- src/client/app/components/ExportComponent.tsx | 5 +- .../components/GraphicRateMenuComponent.tsx | 4 +- .../app/components/HeaderButtonsComponent.tsx | 7 +- .../app/components/HistoryComponent.tsx | 23 +- .../app/components/LineChartComponent.tsx | 6 +- .../app/components/MapControlsComponent.tsx | 4 +- .../ReadingsPerDaySelectComponent.tsx | 8 +- .../app/components/ThreeDPillComponent.tsx | 6 +- .../app/components/UnitSelectComponent.tsx | 6 +- .../app/containers/ChartLinkContainer.ts | 44 +- .../app/containers/CompareChartContainer.ts | 16 +- src/client/app/reducers/appStateSlice.ts | 49 +-- src/client/app/reducers/graph.ts | 377 +++++++++++------- .../app/redux/middleware/graphHistory.ts | 32 +- src/client/index.html | 2 - 21 files changed, 344 insertions(+), 293 deletions(-) diff --git a/src/client/app/components/AreaUnitSelectComponent.tsx b/src/client/app/components/AreaUnitSelectComponent.tsx index 4ea374ca5..8d6cb4eb3 100644 --- a/src/client/app/components/AreaUnitSelectComponent.tsx +++ b/src/client/app/components/AreaUnitSelectComponent.tsx @@ -7,7 +7,7 @@ import { FormattedMessage } from 'react-intl'; import { useDispatch } from 'react-redux'; import Select from 'react-select'; import { useAppSelector } from '../redux/hooks'; -import { graphSlice } from '../reducers/graph'; +import { graphSlice, selectGraphState } from '../reducers/graph'; import { selectUnitDataById } from '../redux/api/unitsApi'; import { StringSelectOption } from '../types/items'; import { UnitRepresentType } from '../types/redux/units'; @@ -22,7 +22,7 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; export default function AreaUnitSelectComponent() { const dispatch = useDispatch(); - const graphState = useAppSelector(state => state.graph); + const graphState = useAppSelector(selectGraphState); const unitDataById = useAppSelector(selectUnitDataById); // Array of select options created from the area unit enum diff --git a/src/client/app/components/BarChartComponent.tsx b/src/client/app/components/BarChartComponent.tsx index e33da4452..cb5624a81 100644 --- a/src/client/app/components/BarChartComponent.tsx +++ b/src/client/app/components/BarChartComponent.tsx @@ -8,7 +8,10 @@ import { PlotRelayoutEvent } from 'plotly.js'; import * as React from 'react'; import Plot from 'react-plotly.js'; import { TimeInterval } from '../../../common/TimeInterval'; -import { graphSlice, selectSelectedGroups, selectSelectedMeters } from '../reducers/graph'; +import { + graphSlice, selectAreaUnit, selectBarStacking, selectBarWidthDays, + selectGraphAreaNormalization, selectSelectedGroups, selectSelectedMeters, selectSelectedUnit +} from '../reducers/graph'; import { selectGroupDataById } from '../redux/api/groupsApi'; import { selectMeterDataById } from '../redux/api/metersApi'; import { readingsApi } from '../redux/api/readingsApi'; @@ -35,15 +38,15 @@ export default function BarChartComponent() { const { data: meterReadings, isLoading: meterIsFetching } = readingsApi.useBarQuery(meterArgs, { skip: meterShouldSkip }); const { data: groupData, isLoading: groupIsFetching } = readingsApi.useBarQuery(groupArgs, { skip: groupShouldSkip }); - const barDuration = useAppSelector(state => state.graph.barDuration); - const barStacking = useAppSelector(state => state.graph.barStacking); - const unitID = useAppSelector(state => state.graph.selectedUnit); + const barDuration = useAppSelector(selectBarWidthDays); + const barStacking = useAppSelector(selectBarStacking); + const unitID = useAppSelector(selectSelectedUnit); // The unit label depends on the unit which is in selectUnit state. - const graphingUnit = useAppSelector(state => state.graph.selectedUnit); + const graphingUnit = useAppSelector(selectSelectedUnit); const unitDataById = useAppSelector(selectUnitDataById); - const selectedAreaNormalization = useAppSelector(state => state.graph.areaNormalization); - const selectedAreaUnit = useAppSelector(state => state.graph.selectedAreaUnit); + const selectedAreaNormalization = useAppSelector(selectGraphAreaNormalization); + const selectedAreaUnit = useAppSelector(selectAreaUnit); const selectedMeters = useAppSelector(selectSelectedMeters); const selectedGroups = useAppSelector(selectSelectedGroups); const meterDataByID = useAppSelector(selectMeterDataById); diff --git a/src/client/app/components/BarControlsComponent.tsx b/src/client/app/components/BarControlsComponent.tsx index 396bc8b18..a5ee87dd7 100644 --- a/src/client/app/components/BarControlsComponent.tsx +++ b/src/client/app/components/BarControlsComponent.tsx @@ -3,7 +3,7 @@ import sliderWithoutTooltips, { createSliderWithTooltip } from 'rc-slider'; import 'rc-slider/assets/index.css'; import * as React from 'react'; import { Button, ButtonGroup } from 'reactstrap'; -import { graphSlice } from '../reducers/graph'; +import { graphSlice, selectBarStacking, selectBarWidthDays } from '../reducers/graph'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; @@ -13,8 +13,8 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; */ export default function BarControlsComponent() { const dispatch = useAppDispatch(); - const barDuration = useAppSelector(state => state.graph.barDuration); - const barStacking = useAppSelector(state => state.graph.barStacking); + const barDuration = useAppSelector(selectBarWidthDays); + const barStacking = useAppSelector(selectBarStacking); const [showSlider, setShowSlider] = React.useState(false); const [sliderVal, setSliderVal] = React.useState(barDuration.asDays()); diff --git a/src/client/app/components/ChartSelectComponent.tsx b/src/client/app/components/ChartSelectComponent.tsx index b56e9b21c..f1c83a45c 100644 --- a/src/client/app/components/ChartSelectComponent.tsx +++ b/src/client/app/components/ChartSelectComponent.tsx @@ -5,12 +5,10 @@ import * as React from 'react'; import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useDispatch, useSelector } from 'react-redux'; import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; -import { graphSlice } from '../reducers/graph'; -import { Dispatch } from '../types/redux/actions'; +import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { graphSlice, selectChartToRender } from '../reducers/graph'; import { ChartTypes } from '../types/redux/graph'; -import { State } from '../types/redux/state'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; @@ -19,8 +17,8 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; * @returns Chart select element */ export default function ChartSelectComponent() { - const currentChartToRender = useSelector((state: State) => state.graph.chartToRender) - const dispatch: Dispatch = useDispatch(); + const currentChartToRender = useAppSelector(selectChartToRender) + const dispatch = useAppDispatch(); const [expand, setExpand] = useState(false); // TODO Re-write as selector to use elsewhere diff --git a/src/client/app/components/CompareControlsComponent.tsx b/src/client/app/components/CompareControlsComponent.tsx index ef18bfb8b..709747396 100644 --- a/src/client/app/components/CompareControlsComponent.tsx +++ b/src/client/app/components/CompareControlsComponent.tsx @@ -1,7 +1,7 @@ import * as moment from 'moment'; import * as React from 'react'; import { Button, ButtonGroup, Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; -import { graphSlice } from '../reducers/graph'; +import { graphSlice, selectComparePeriod, selectSortingOrder } from '../reducers/graph'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { ComparePeriod, SortingOrder } from '../utils/calculateCompare'; import translate from '../utils/translate'; @@ -12,8 +12,8 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; */ export default function CompareControlsComponent() { const dispatch = useAppDispatch(); - const comparePeriod = useAppSelector(state => state.graph.comparePeriod); - const compareSortingOrder = useAppSelector(state => state.graph.compareSortingOrder); + const comparePeriod = useAppSelector(selectComparePeriod); + const compareSortingOrder = useAppSelector(selectSortingOrder); const [compareSortingDropdownOpen, setCompareSortingDropdownOpen] = React.useState(false); const handleCompareButton = (comparePeriod: ComparePeriod) => { dispatch(graphSlice.actions.updateComparePeriod({ comparePeriod, currentTime: moment() })) diff --git a/src/client/app/components/DashboardComponent.tsx b/src/client/app/components/DashboardComponent.tsx index 2bf3493e0..f8b056232 100644 --- a/src/client/app/components/DashboardComponent.tsx +++ b/src/client/app/components/DashboardComponent.tsx @@ -12,14 +12,15 @@ import MapChartComponent from './MapChartComponent'; import MultiCompareChartComponentWIP from './MultiCompareChartComponentWIP'; import ThreeDComponent from './ThreeDComponent'; import UIOptionsComponent from './UIOptionsComponent'; +import { selectChartToRender, selectOptionsVisibility } from '../reducers/graph'; /** * React component that controls the dashboard * @returns the Primary Dashboard Component comprising of Ui Controls, and */ export default function DashboardComponent() { - const chartToRender = useAppSelector(state => state.graph.chartToRender); - const optionsVisibility = useAppSelector(state => state.graph.optionsVisibility); + const chartToRender = useAppSelector(selectChartToRender); + const optionsVisibility = useAppSelector(selectOptionsVisibility); const optionsClassName = optionsVisibility ? 'col-2 d-none d-lg-block' : 'd-none'; const chartClassName = optionsVisibility ? 'col-12 col-lg-10' : 'col-12'; // const optionsClassName = optionsVisibility ? 'col-3 d-none d-lg-block' : 'd-none'; diff --git a/src/client/app/components/ExportComponent.tsx b/src/client/app/components/ExportComponent.tsx index 40fb5c86b..138e180f9 100644 --- a/src/client/app/components/ExportComponent.tsx +++ b/src/client/app/components/ExportComponent.tsx @@ -23,6 +23,7 @@ import { barUnitLabel, lineUnitLabel } from '../utils/graphics'; import { hasToken } from '../utils/token'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; +import { selectGraphState, selectShowMinMax } from '../reducers/graph'; /** * Creates export buttons and does code for handling export to CSV files. @@ -38,11 +39,11 @@ export default function ExportComponent() { // Conversion state const conversionState = useAppSelector(selectConversionsDetails); // graph state - const graphState = useAppSelector(state => state.graph); + const graphState = useAppSelector(selectGraphState); // admin state const adminState = useAppSelector(state => state.admin); // error bar state - const errorBarState = useAppSelector(state => state.graph.showMinMax); + const errorBarState = useAppSelector(selectShowMinMax); // Time range of graphic const timeInterval = graphState.queryTimeInterval; diff --git a/src/client/app/components/GraphicRateMenuComponent.tsx b/src/client/app/components/GraphicRateMenuComponent.tsx index d46408271..3f4d58759 100644 --- a/src/client/app/components/GraphicRateMenuComponent.tsx +++ b/src/client/app/components/GraphicRateMenuComponent.tsx @@ -7,7 +7,7 @@ import { FormattedMessage } from 'react-intl'; import { useDispatch } from 'react-redux'; import Select from 'react-select'; import { selectUnitDataById } from '../redux/api/unitsApi'; -import { graphSlice } from '../reducers/graph'; +import { graphSlice, selectGraphState } from '../reducers/graph'; import { useAppSelector } from '../redux/hooks'; import { SelectOption } from '../types/items'; import { LineGraphRate, LineGraphRates } from '../types/redux/graph'; @@ -23,7 +23,7 @@ export default function GraphicRateMenuComponent() { const dispatch = useDispatch(); // Graph state - const graphState = useAppSelector(state => state.graph); + const graphState = useAppSelector(selectGraphState); // Unit state const unitDataById = useAppSelector(selectUnitDataById); diff --git a/src/client/app/components/HeaderButtonsComponent.tsx b/src/client/app/components/HeaderButtonsComponent.tsx index 6d7f9b180..12677e2cc 100644 --- a/src/client/app/components/HeaderButtonsComponent.tsx +++ b/src/client/app/components/HeaderButtonsComponent.tsx @@ -8,7 +8,7 @@ import { FormattedMessage } from 'react-intl'; import { Link, useLocation } from 'react-router-dom-v5-compat'; import { DropdownItem, DropdownMenu, DropdownToggle, Nav, NavLink, Navbar, UncontrolledDropdown } from 'reactstrap'; import TooltipHelpComponent from '../components/TooltipHelpComponent'; -import { toggleOptionsVisibility } from '../reducers/graph'; +import { selectOptionsVisibility, toggleOptionsVisibility } from '../reducers/graph'; import { unsavedWarningSlice } from '../reducers/unsavedWarning'; import { authApi } from '../redux/api/authApi'; import { selectOEDVersion } from '../redux/api/versionApi'; @@ -19,6 +19,7 @@ import translate from '../utils/translate'; import LanguageSelectorComponent from './LanguageSelectorComponent'; import { BASE_URL } from './TooltipHelpComponent'; import TooltipMarkerComponent from './TooltipMarkerComponent'; +import { selectCurrentUser } from '../reducers/currentUser'; /** * React Component that defines the header buttons at the top of a page @@ -75,11 +76,11 @@ export default function HeaderButtonsComponent() { // Local state for rendering. const [state, setState] = useState(defaultState); // Information on the current user. - const currentUser = useAppSelector(state => state.currentUser.profile); + const { profile: currentUser } = useAppSelector(selectCurrentUser); // Tracks unsaved changes. const unsavedChangesState = useAppSelector(state => state.unsavedWarning.hasUnsavedChanges); // whether to collapse options when on graphs page - const optionsVisibility = useAppSelector(state => state.graph.optionsVisibility); + const optionsVisibility = useAppSelector(selectOptionsVisibility); // Must update in case the version was not set when the page was loaded. useEffect(() => { diff --git a/src/client/app/components/HistoryComponent.tsx b/src/client/app/components/HistoryComponent.tsx index 81c937a1c..2ac60bdb6 100644 --- a/src/client/app/components/HistoryComponent.tsx +++ b/src/client/app/components/HistoryComponent.tsx @@ -1,31 +1,28 @@ import * as React from 'react'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; -import { prevHistory, forwardHistory, selectBackHistoryStack, selectForwardHistoryStack } from '../reducers/appStateSlice'; +import { + selectForwardHistory, selectPrevHistory, + traverseNextHistory, traversePrevHistory +} from '../reducers/graph'; /** * @returns Renders a history component with previous and next buttons. */ export default function HistoryComponent() { const dispatch = useAppDispatch(); - const backStack = useAppSelector(selectBackHistoryStack) - const forwardStack = useAppSelector(selectForwardHistoryStack) + const backStack = useAppSelector(selectPrevHistory) + const forwardStack = useAppSelector(selectForwardHistory) return (
dispatch(prevHistory())} + style={{ visibility: !backStack.length ? 'hidden' : 'visible', cursor: 'pointer' }} + onClick={() => dispatch(traversePrevHistory())} > dispatch(forwardHistory())} + style={{ visibility: !forwardStack.length ? 'hidden' : 'visible', cursor: 'pointer' }} + onClick={() => dispatch(traverseNextHistory())} > diff --git a/src/client/app/components/LineChartComponent.tsx b/src/client/app/components/LineChartComponent.tsx index 3355d2867..bc3a5bf99 100644 --- a/src/client/app/components/LineChartComponent.tsx +++ b/src/client/app/components/LineChartComponent.tsx @@ -10,7 +10,7 @@ import Plot from 'react-plotly.js'; import { TimeInterval } from '../../../common/TimeInterval'; import { graphSlice, selectAreaUnit, selectGraphAreaNormalization, - selectLineGraphRate, selectSelectedGroups, selectSelectedMeters + selectLineGraphRate, selectSelectedGroups, selectSelectedMeters, selectSelectedUnit } from '../reducers/graph'; import { selectGroupDataById } from '../redux/api/groupsApi'; import { selectMeterDataById } from '../redux/api/metersApi'; @@ -34,9 +34,9 @@ export default function LineChartComponent() { const { data: meterReadings, isLoading: meterIsLoading } = readingsApi.useLineQuery(meterArgs, { skip: meterShouldSkip }); const { data: groupData, isLoading: groupIsLoading } = readingsApi.useLineQuery(groupArgs, { skip: groupShouldSkip }); - const selectedUnit = useAppSelector(state => state.graph.selectedUnit); + const selectedUnit = useAppSelector(selectSelectedUnit); // The unit label depends on the unit which is in selectUnit state. - const graphingUnit = useAppSelector(state => state.graph.selectedUnit); + const graphingUnit = useAppSelector(selectSelectedUnit); // The current selected rate const currentSelectedRate = useAppSelector(selectLineGraphRate); const unitDataById = useAppSelector(selectUnitDataById); diff --git a/src/client/app/components/MapControlsComponent.tsx b/src/client/app/components/MapControlsComponent.tsx index 9de41f393..3acfd8be0 100644 --- a/src/client/app/components/MapControlsComponent.tsx +++ b/src/client/app/components/MapControlsComponent.tsx @@ -4,14 +4,14 @@ import { Button, ButtonGroup } from 'reactstrap'; import TooltipMarkerComponent from './TooltipMarkerComponent'; import MapChartSelectComponent from './MapChartSelectComponent'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; -import { graphSlice } from '../reducers/graph'; +import { graphSlice, selectBarWidthDays } from '../reducers/graph'; import * as moment from 'moment'; /** * @returns Map page controls */ export default function MapControlsComponent() { const dispatch = useAppDispatch(); - const barDuration = useAppSelector(state => state.graph.barDuration); + const barDuration = useAppSelector(selectBarWidthDays); const handleDurationChange = (value: number) => { dispatch(graphSlice.actions.updateBarDuration(moment.duration(value, 'days'))) diff --git a/src/client/app/components/ReadingsPerDaySelectComponent.tsx b/src/client/app/components/ReadingsPerDaySelectComponent.tsx index b651f82e2..0534fe4bf 100644 --- a/src/client/app/components/ReadingsPerDaySelectComponent.tsx +++ b/src/client/app/components/ReadingsPerDaySelectComponent.tsx @@ -5,13 +5,13 @@ import * as moment from 'moment'; import * as React from 'react'; import Select from 'react-select'; -import { updateThreeDReadingInterval } from '../reducers/graph'; +import { selectGraphState, selectThreeDReadingInterval, updateThreeDReadingInterval } from '../reducers/graph'; import { readingsApi } from '../redux/api/readingsApi'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { selectThreeDQueryArgs } from '../redux/selectors/chartQuerySelectors'; import { ChartTypes, ReadingInterval } from '../types/redux/graph'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; -import { selectThreeDQueryArgs } from '../redux/selectors/chartQuerySelectors'; /** * A component which allows users to select date ranges for the graphic @@ -19,8 +19,8 @@ import { selectThreeDQueryArgs } from '../redux/selectors/chartQuerySelectors'; */ export default function ReadingsPerDaySelect() { const dispatch = useAppDispatch(); - const graphState = useAppSelector(state => state.graph); - const readingInterval = useAppSelector(state => state.graph.threeD.readingInterval); + const graphState = useAppSelector(selectGraphState); + const readingInterval = useAppSelector(selectThreeDReadingInterval); const { args, shouldSkipQuery } = useAppSelector(selectThreeDQueryArgs); const { data, isFetching } = readingsApi.endpoints.threeD.useQuery(args, { skip: shouldSkipQuery }); diff --git a/src/client/app/components/ThreeDPillComponent.tsx b/src/client/app/components/ThreeDPillComponent.tsx index 84bc08243..3f7fde664 100644 --- a/src/client/app/components/ThreeDPillComponent.tsx +++ b/src/client/app/components/ThreeDPillComponent.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { Badge } from 'reactstrap'; -import { updateThreeDMeterOrGroupInfo } from '../reducers/graph'; +import { selectGraphState, selectThreeDState, updateThreeDMeterOrGroupInfo } from '../reducers/graph'; import { selectGroupDataById } from '../redux/api/groupsApi'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { MeterOrGroup, MeterOrGroupPill } from '../types/redux/graph'; @@ -19,8 +19,8 @@ export default function ThreeDPillComponent() { const dispatch = useAppDispatch(); const meterDataById = useAppSelector(selectMeterDataById); const groupDataById = useAppSelector(selectGroupDataById); - const threeDState = useAppSelector(state => state.graph.threeD); - const graphState = useAppSelector(state => state.graph); + const threeDState = useAppSelector(selectThreeDState); + const graphState = useAppSelector(selectGraphState); const meterPillData = graphState.selectedMeters.map(meterID => { const area = meterDataById[meterID].area; diff --git a/src/client/app/components/UnitSelectComponent.tsx b/src/client/app/components/UnitSelectComponent.tsx index 653056e44..014aef904 100644 --- a/src/client/app/components/UnitSelectComponent.tsx +++ b/src/client/app/components/UnitSelectComponent.tsx @@ -10,7 +10,7 @@ import { GroupedOption, SelectOption } from '../types/items'; // import TooltipMarkerComponent from './TooltipMarkerComponent'; // import { FormattedMessage } from 'react-intl'; import { Badge } from 'reactstrap'; -import { graphSlice } from '../reducers/graph'; +import { graphSlice, selectSelectedUnit } from '../reducers/graph'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; import { useFetchingStates } from '../redux/componentHooks'; @@ -22,8 +22,8 @@ import { selectUnitDataById } from '../redux/api/unitsApi'; */ export default function UnitSelectComponent() { const dispatch = useAppDispatch(); - const unitSelectOptions = useAppSelector(state => selectUnitSelectData(state)); - const selectedUnitID = useAppSelector(state => state.graph.selectedUnit); + const unitSelectOptions = useAppSelector(selectUnitSelectData); + const selectedUnitID = useAppSelector(selectSelectedUnit); const unitsByID = useAppSelector(selectUnitDataById); const { endpointsFetchingData } = useFetchingStates(); diff --git a/src/client/app/containers/ChartLinkContainer.ts b/src/client/app/containers/ChartLinkContainer.ts index 33a2f3124..ec219785f 100644 --- a/src/client/app/containers/ChartLinkContainer.ts +++ b/src/client/app/containers/ChartLinkContainer.ts @@ -4,8 +4,9 @@ import { connect } from 'react-redux'; import ChartLinkComponent from '../components/ChartLinkComponent'; -import { State } from '../types/redux/state'; import { ChartTypes } from '../types/redux/graph'; +import { RootState } from '../store'; +import { selectChartToRender, selectGraphState } from '../reducers/graph'; /** * Passes the current redux state of the chart link text, and turns it into props for the React @@ -14,8 +15,9 @@ import { ChartTypes } from '../types/redux/graph'; * * Returns the updated link text */ -function mapStateToProps(state: State) { - const chartType = state.graph.chartToRender; +function mapStateToProps(state: RootState) { + const current = selectGraphState(state) + const chartType = selectChartToRender(state); // Determine the beginning of the URL to add arguments to. // This is the current URL. const winLocHref = window.location.href; @@ -29,45 +31,45 @@ function mapStateToProps(state: State) { // Add graph? since we want to route to graph and have a ? before any arguments. let linkText = `${baseURL}graph?`; // let weeklyLink = ''; // reflects graph 7 days from present, with user selected meters and groups; - if (state.graph.selectedMeters.length > 0) { - linkText += `meterIDs=${state.graph.selectedMeters.toString()}&`; + if (current.selectedMeters.length > 0) { + linkText += `meterIDs=${current.selectedMeters.toString()}&`; } - if (state.graph.selectedGroups.length > 0) { - linkText += `groupIDs=${state.graph.selectedGroups.toString()}&`; + if (current.selectedGroups.length > 0) { + linkText += `groupIDs=${current.selectedGroups.toString()}&`; } - linkText += `chartType=${state.graph.chartToRender}`; + linkText += `chartType=${current.chartToRender}`; // weeklyLink = linkText + '&serverRange=7dfp'; // dfp: days from present; - linkText += `&serverRange=${state.graph.queryTimeInterval.toString()}`; + linkText += `&serverRange=${current.queryTimeInterval.toString()}`; switch (chartType) { case ChartTypes.bar: - linkText += `&barDuration=${state.graph.barDuration.asDays()}`; - linkText += `&barStacking=${state.graph.barStacking}`; + linkText += `&barDuration=${current.barDuration.asDays()}`; + linkText += `&barStacking=${current.barStacking}`; break; case ChartTypes.line: // no code for this case // under construction; - // linkText += `&displayRange=${state.graph.queryTimeInterval.toString().split('_')}`; + // linkText += `&displayRange=${current.queryTimeInterval.toString().split('_')}`; break; case ChartTypes.compare: - linkText += `&comparePeriod=${state.graph.comparePeriod}`; - linkText += `&compareSortingOrder=${state.graph.compareSortingOrder}`; + linkText += `&comparePeriod=${current.comparePeriod}`; + linkText += `&compareSortingOrder=${current.compareSortingOrder}`; break; case ChartTypes.map: linkText += `&mapID=${state.maps.selectedMap.toString()}`; break; case ChartTypes.threeD: - linkText += `&meterOrGroup=${state.graph.threeD.meterOrGroup}`; - linkText += `&meterOrGroupID=${state.graph.threeD.meterOrGroupID}`; - linkText += `&readingInterval=${state.graph.threeD.readingInterval}`; + linkText += `&meterOrGroup=${current.threeD.meterOrGroup}`; + linkText += `&meterOrGroupID=${current.threeD.meterOrGroupID}`; + linkText += `&readingInterval=${current.threeD.readingInterval}`; break; default: break; } - const unitID = state.graph.selectedUnit; + const unitID = current.selectedUnit; linkText += `&unitID=${unitID.toString()}`; - linkText += `&rate=${state.graph.lineGraphRate.label.toString()},${state.graph.lineGraphRate.rate.toString()}`; - linkText += `&areaUnit=${state.graph.selectedAreaUnit}&areaNormalization=${state.graph.areaNormalization}`; - linkText += `&minMax=${state.graph.showMinMax}`; + linkText += `&rate=${current.lineGraphRate.label.toString()},${current.lineGraphRate.rate.toString()}`; + linkText += `&areaUnit=${current.selectedAreaUnit}&areaNormalization=${current.areaNormalization}`; + linkText += `&minMax=${current.showMinMax}`; return { linkText, chartType diff --git a/src/client/app/containers/CompareChartContainer.ts b/src/client/app/containers/CompareChartContainer.ts index dd643e4be..d52cd4ba9 100644 --- a/src/client/app/containers/CompareChartContainer.ts +++ b/src/client/app/containers/CompareChartContainer.ts @@ -16,6 +16,7 @@ import { selectUnitDataById } from '../redux/api/unitsApi'; import { RootState } from '../store'; import { selectGroupDataById } from '../redux/api/groupsApi'; import { selectMeterDataById } from '../redux/api/metersApi'; +import { selectAreaUnit, selectComparePeriod, selectCompareTimeInterval, selectGraphAreaNormalization, selectSelectedUnit } from '../reducers/graph'; export interface CompareEntity { id: number; isGroup: boolean; @@ -40,12 +41,13 @@ interface CompareChartContainerProps { * @returns The props object */ function mapStateToProps(state: RootState, ownProps: CompareChartContainerProps): any { - const comparePeriod = state.graph.comparePeriod; + const comparePeriod = selectComparePeriod(state); + const compareTimeInterval = selectCompareTimeInterval(state); const datasets: any[] = []; const periodLabels = getComparePeriodLabels(comparePeriod); // The unit label depends on the unit which is in selectUnit state. // Also need to determine if raw. - const graphingUnit = state.graph.selectedUnit; + const graphingUnit = selectSelectedUnit(state); // This container is not called if there is no data of there are not units so this is safe. const unitDataById = selectUnitDataById(state) const meterDataById = selectMeterDataById(state) @@ -91,10 +93,10 @@ function mapStateToProps(state: RootState, ownProps: CompareChartContainerProps) // Thus, this may not be the reason but for now it is fixed as indicated. // getStartTimestamp() and getEndTimestamp() should return a moment object in UTC so it is fine to use. It could only be // null if it is unbounded but that should never happen with a compare interval. - const thisStartTime = moment.utc(state.graph.compareTimeInterval.getStartTimestamp().format('YYYY-MM-DD HH:mm:ss') + '+00:00'); + const thisStartTime = moment.utc(compareTimeInterval.getStartTimestamp().format('YYYY-MM-DD HH:mm:ss') + '+00:00'); // Only do to start of the hour since OED is using hourly data so fractions of an hour are not given. // The start time is always midnight so this is not needed. - const thisEndTime = moment.utc(state.graph.compareTimeInterval.getEndTimestamp().startOf('hour').format('YYYY-MM-DD HH:mm:ss') + '+00:00'); + const thisEndTime = moment.utc(compareTimeInterval.getEndTimestamp().startOf('hour').format('YYYY-MM-DD HH:mm:ss') + '+00:00'); // The desired label times for this interval that is internationalized and shows day of week, date and time with hours. const thisStartTimeLabel: string = thisStartTime.format('llll'); @@ -124,13 +126,13 @@ function mapStateToProps(state: RootState, ownProps: CompareChartContainerProps) let previousPeriod = entity.prevUsage; let currentPeriod = entity.currUsage; console.log(entity) - + const areaNormalization = selectGraphAreaNormalization(state) // Check if there is data to graph. if (previousPeriod !== null && currentPeriod !== null) { - if (state.graph.areaNormalization) { + if (areaNormalization) { const area = entity.isGroup ? groupDataById[entity.id].area : meterDataById[entity.id].area; const areaUnit = entity.isGroup ? groupDataById[entity.id].areaUnit : meterDataById[entity.id].areaUnit; - const normalization = area * getAreaUnitConversion(areaUnit, state.graph.selectedAreaUnit); + const normalization = area * getAreaUnitConversion(areaUnit, selectAreaUnit(state)); previousPeriod /= normalization; currentPeriod /= normalization; } diff --git a/src/client/app/reducers/appStateSlice.ts b/src/client/app/reducers/appStateSlice.ts index ea4e0b4a0..0dbabbe57 100644 --- a/src/client/app/reducers/appStateSlice.ts +++ b/src/client/app/reducers/appStateSlice.ts @@ -1,15 +1,10 @@ import { createSlice } from '@reduxjs/toolkit'; -import { GraphState } from '../types/redux/graph'; interface appStateSlice { initComplete: boolean; - backHistoryStack: GraphState[]; - forwardHistoryStack: GraphState[]; } const defaultState: appStateSlice = { - initComplete: false, - backHistoryStack: [], - forwardHistoryStack: [] + initComplete: false } export const appStateSlice = createSlice({ @@ -20,55 +15,17 @@ export const appStateSlice = createSlice({ // Allows thunks inside of reducers, and prepareReducers with 'create' builder notation setInitComplete: create.reducer((state, action) => { state.initComplete = action.payload - }), - updateHistory: create.reducer((state, action) => { - state.backHistoryStack.push(action.payload) - // reset forward history on new 'visit' - state.forwardHistoryStack.length = 0 - - }), - prevHistory: create.reducer(state => { - if (state.backHistoryStack.length > 1) { - // prev and forward can safely use type assertion due to length check. pop() Will never be undefined - state.forwardHistoryStack.push(state.backHistoryStack.pop() as GraphState) - } - }), - forwardHistory: create.reducer(state => { - if (state.forwardHistoryStack.length) { - state.backHistoryStack.push(state.forwardHistoryStack.pop() as GraphState) - } - }), - clearHistory: create.reducer(state => { - // TODO Verify the behavior of clear before adding an onClick - state.forwardHistoryStack.length = 0 - state.backHistoryStack.splice(0, state.backHistoryStack.length - 1) }) }), selectors: { - selectInitComplete: state => state.initComplete, - selectBackHistoryStack: state => state.backHistoryStack, - selectForwardHistoryStack: state => state.forwardHistoryStack, - // Explicit return value required when calling sameSlice's getSelectors, otherwise circular type inference breaks TS. - selectBackHistoryTop: (state): GraphState => { - const { selectBackHistoryStack } = appStateSlice.getSelectors() - const backHistory = selectBackHistoryStack(state) - const top = backHistory[backHistory.length - 1] - return top - } + selectInitComplete: state => state.initComplete } }) export const { - updateHistory, - prevHistory, - forwardHistory, - setInitComplete, - clearHistory + setInitComplete } = appStateSlice.actions export const { - selectBackHistoryStack, - selectForwardHistoryStack, - selectBackHistoryTop, selectInitComplete } = appStateSlice.selectors diff --git a/src/client/app/reducers/graph.ts b/src/client/app/reducers/graph.ts index 32f1d0353..ec814014f 100644 --- a/src/client/app/reducers/graph.ts +++ b/src/client/app/reducers/graph.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, createSlice } from '@reduxjs/toolkit'; +import { PayloadAction, SliceCaseReducers, ValidateSliceCaseReducers, createAction, createSlice } from '@reduxjs/toolkit'; import * as moment from 'moment'; import { ActionMeta } from 'react-select'; import { TimeInterval } from '../../../common/TimeInterval'; @@ -38,271 +38,364 @@ const defaultState: GraphState = { } }; +interface History { + prev: Array + current: T + next: Array +} +const initialState: History = { + prev: [], + current: defaultState, + next: [] +} + export const graphSlice = createSlice({ name: 'graph', - initialState: defaultState, + initialState: initialState, reducers: { confirmGraphRenderOnce: state => { - state.renderOnce = true + state.current.renderOnce = true }, updateSelectedMeters: (state, action: PayloadAction) => { - state.selectedMeters = action.payload + state.current.selectedMeters = action.payload }, updateSelectedGroups: (state, action: PayloadAction) => { - state.selectedGroups = action.payload + state.current.selectedGroups = action.payload }, updateSelectedUnit: (state, action: PayloadAction) => { // If Payload is defined, update selectedUnit if (action.payload) { - state.selectedUnit = action.payload + state.current.selectedUnit = action.payload } else { // If NewValue is undefined, the current Unit has been cleared // Reset groups and meters, and selected unit - state.selectedUnit = -99 - state.selectedMeters = [] - state.selectedGroups = [] + state.current.selectedUnit = -99 + state.current.selectedMeters = [] + state.current.selectedGroups = [] } }, updateSelectedAreaUnit: (state, action: PayloadAction) => { - state.selectedAreaUnit = action.payload + state.current.selectedAreaUnit = action.payload }, updateBarDuration: (state, action: PayloadAction) => { - state.barDuration = action.payload + state.current.barDuration = action.payload }, updateTimeInterval: (state, action: PayloadAction) => { - state.queryTimeInterval = action.payload + state.current.queryTimeInterval = action.payload }, changeSliderRange: (state, action: PayloadAction) => { - state.rangeSliderInterval = action.payload + state.current.rangeSliderInterval = action.payload }, resetRangeSliderStack: state => { - state.rangeSliderInterval = TimeInterval.unbounded() + state.current.rangeSliderInterval = TimeInterval.unbounded() }, updateComparePeriod: (state, action: PayloadAction<{ comparePeriod: ComparePeriod, currentTime: moment.Moment }>) => { - state.comparePeriod = action.payload.comparePeriod - state.compareTimeInterval = calculateCompareTimeInterval(action.payload.comparePeriod, action.payload.currentTime) + state.current.comparePeriod = action.payload.comparePeriod + state.current.compareTimeInterval = calculateCompareTimeInterval(action.payload.comparePeriod, action.payload.currentTime) }, changeChartToRender: (state, action: PayloadAction) => { - state.chartToRender = action.payload + state.current.chartToRender = action.payload }, toggleAreaNormalization: state => { - state.areaNormalization = !state.areaNormalization + state.current.areaNormalization = !state.current.areaNormalization }, setAreaNormalization: (state, action: PayloadAction) => { - state.areaNormalization = action.payload + state.current.areaNormalization = action.payload }, toggleShowMinMax: state => { - state.showMinMax = !state.showMinMax + state.current.showMinMax = !state.current.showMinMax }, setShowMinMax: (state, action: PayloadAction) => { - state.showMinMax = action.payload + state.current.showMinMax = action.payload }, changeBarStacking: state => { - state.barStacking = !state.barStacking + state.current.barStacking = !state.current.barStacking }, setBarStacking: (state, action: PayloadAction) => { - state.barStacking = action.payload + state.current.barStacking = action.payload }, setHotlinked: (state, action: PayloadAction) => { - state.hotlinked = action.payload + state.current.hotlinked = action.payload }, changeCompareSortingOrder: (state, action: PayloadAction) => { - state.compareSortingOrder = action.payload + state.current.compareSortingOrder = action.payload }, toggleOptionsVisibility: state => { - state.optionsVisibility = !state.optionsVisibility + state.current.optionsVisibility = !state.current.optionsVisibility }, setOptionsVisibility: (state, action: PayloadAction) => { - state.optionsVisibility = action.payload + state.current.optionsVisibility = action.payload }, updateLineGraphRate: (state, action: PayloadAction) => { - state.lineGraphRate = action.payload + state.current.lineGraphRate = action.payload }, updateThreeDReadingInterval: (state, action: PayloadAction) => { - state.threeD.readingInterval = action.payload + state.current.threeD.readingInterval = action.payload }, updateThreeDMeterOrGroupInfo: (state, action: PayloadAction<{ meterOrGroupID: number | undefined, meterOrGroup: MeterOrGroup }>) => { - state.threeD.meterOrGroupID = action.payload.meterOrGroupID - state.threeD.meterOrGroup = action.payload.meterOrGroup + state.current.threeD.meterOrGroupID = action.payload.meterOrGroupID + state.current.threeD.meterOrGroup = action.payload.meterOrGroup }, updateThreeDMeterOrGroupID: (state, action: PayloadAction) => { - state.threeD.meterOrGroupID = action.payload + state.current.threeD.meterOrGroupID = action.payload }, updateThreeDMeterOrGroup: (state, action: PayloadAction) => { - state.threeD.meterOrGroup = action.payload + state.current.threeD.meterOrGroup = action.payload }, - updateSelectedMetersOrGroups: (state, action: PayloadAction<{ newMetersOrGroups: number[], meta: ActionMeta }>) => { + updateSelectedMetersOrGroups: ({ current }, action: PayloadAction<{ newMetersOrGroups: number[], meta: ActionMeta }>) => { // This reducer handles the addition and subtraction values for both the meter and group select components. // The 'MeterOrGroup' type is heavily utilized in the reducer and other parts of the code. // Note that this option is binary, if it's not a meter, then it's a group. // Destructure payload const { newMetersOrGroups, meta } = action.payload; + const cleared = meta.action === 'clear' + const valueRemoved = meta.action === 'pop-value' || meta.action === 'remove-value' + const valueAdded = meta.action === 'select-option' + let isAMeter = true - // Used to check if value has been added or removed - // If 'meta.option' is defined, it indicates that a single value has been added or selected. - const addedMeterOrGroupID = meta.option?.value; - const addedMeterOrGroup = meta.option?.meterOrGroup; - const addedMeterOrGroupUnit = meta.option?.defaultGraphicUnit; - - // If 'meta.removedValue' is defined, it indicates that a single value has been removed or deselected. - const removedMeterOrGroupID = meta.removedValue?.value; - const removedMeterOrGroup = meta.removedValue?.meterOrGroup; - - // If meta.removedValues is defined, it indicates that all values have been cleared. - const clearedMeterOrGroups = meta.removedValues; - - // Generic if else block pertaining to all graph types - // Check for the three possible scenarios of a change in the meters - if (clearedMeterOrGroups) { + if (cleared) { + const clearedMeterOrGroups = meta.removedValues; // A Select has been cleared(all values removed with clear) // use the first index of cleared items to check for meter or group - const isAMeter = clearedMeterOrGroups[0].meterOrGroup === MeterOrGroup.meters + isAMeter = clearedMeterOrGroups[0].meterOrGroup === MeterOrGroup.meters // if a meter clear meters, else clear groups - isAMeter ? state.selectedMeters = [] : state.selectedGroups = [] + isAMeter ? current.selectedMeters = [] : current.selectedGroups = [] - } else if (removedMeterOrGroup) { + } + if (valueRemoved && meta.option) { + const isAMeter = meta.removedValue.meterOrGroup === MeterOrGroup.meters; // An entry was deleted. // Update either selected meters or groups - removedMeterOrGroup === MeterOrGroup.meters ? - state.selectedMeters = newMetersOrGroups - : - state.selectedGroups = newMetersOrGroups + isAMeter + ? current.selectedMeters = newMetersOrGroups + : current.selectedGroups = newMetersOrGroups + } - } else if (addedMeterOrGroup) { + if (valueAdded && meta.option) { + isAMeter = meta.option.meterOrGroup === MeterOrGroup.meters; + const addedMeterOrGroupUnit = meta.option.defaultGraphicUnit; // An entry was added, // Update either selected meters or groups - addedMeterOrGroup === MeterOrGroup.meters ? - state.selectedMeters = newMetersOrGroups + isAMeter ? + current.selectedMeters = newMetersOrGroups : - state.selectedGroups = newMetersOrGroups + current.selectedGroups = newMetersOrGroups // If the current unit is -99, there is not yet a graphic unit // Set the newly added meterOrGroup's default graphic unit as the current selected unit. - if (state.selectedUnit === -99 && addedMeterOrGroupUnit) { - state.selectedUnit = addedMeterOrGroupUnit; + if (current.selectedUnit === -99 && addedMeterOrGroupUnit) { + current.selectedUnit = addedMeterOrGroupUnit; } } + // Blocks Pertaining to behaviors of specific pages below - // Blocks Pertaining to behaviors of specific pages + // Additional 3d logic for each case. + // Reset Currently Selected 3D Meter Or Group if it has been removed from any page + if (cleared) { + const removedType = meta.removedValues[0].meterOrGroup + const threeDSelectedType = current.threeD.meterOrGroup + if (removedType === threeDSelectedType) { + current.threeD.meterOrGroupID = undefined + current.threeD.meterOrGroup = undefined - // Additional 3d logic - // When a meter or group is selected/added, make it the currently active in 3D state. - if (addedMeterOrGroupID && addedMeterOrGroup && state.chartToRender === ChartTypes.threeD) { + } + } + // When a meter or group is selected/added, make it the currently active in 3D current. + else if (valueAdded && meta.option && current.chartToRender === ChartTypes.threeD) { // TODO Currently only tracks when on 3d, Verify that this is the desired behavior - state.threeD.meterOrGroupID = addedMeterOrGroupID; - state.threeD.meterOrGroup = addedMeterOrGroup; - addedMeterOrGroup === MeterOrGroup.meters ? - state.selectedMeters = newMetersOrGroups - : - state.selectedGroups = newMetersOrGroups + current.threeD.meterOrGroupID = meta.option.value; + current.threeD.meterOrGroup = meta.option.meterOrGroup; } - - // Reset Currently Selected 3D Meter Or Group if it has been removed from any page - if ( - // meterOrGroup was removed - removedMeterOrGroupID && removedMeterOrGroup && - // Removed meterOrGroup is the currently active on the 3D page - removedMeterOrGroupID === state.threeD.meterOrGroupID && removedMeterOrGroup === state.threeD.meterOrGroup - ) { - state.threeD.meterOrGroupID = undefined - state.threeD.meterOrGroup = undefined - + else if (valueRemoved && meta.option) { + const idMatches = meta.removedValue.value === current.threeD.meterOrGroupID + const typeMatches = meta.removedValue.meterOrGroup === current.threeD.meterOrGroup + if (idMatches && typeMatches) { + current.threeD.meterOrGroupID = undefined + current.threeD.meterOrGroup = undefined + } } }, resetTimeInterval: state => { - if (!state.queryTimeInterval.equals(TimeInterval.unbounded())) { - state.queryTimeInterval = TimeInterval.unbounded() + if (!state.current.queryTimeInterval.equals(TimeInterval.unbounded())) { + state.current.queryTimeInterval = TimeInterval.unbounded() + } + }, + setGraphState: (state, action: PayloadAction) => { + state.current = action.payload + }, + updateHistory: (state, action: PayloadAction) => { + state.next = []; + state.prev.push(action.payload) + }, + traversePrevHistory: state => { + const prev = state.prev.pop() + if (prev) { + state.next.push(state.current) + state.current = prev } }, - setGraphState: (_state, action: PayloadAction) => action.payload + traverseNextHistory: state => { + const next = state.next.pop() + if (next) { + state.prev.push(state.current) + state.current = next + } + } }, extraReducers: builder => { - builder.addMatcher(preferencesApi.endpoints.getPreferences.matchFulfilled, (state, action) => { - if (state.selectedAreaUnit === AreaUnitType.none) { - state.selectedAreaUnit = action.payload.defaultAreaUnit; + builder.addMatcher(preferencesApi.endpoints.getPreferences.matchFulfilled, ({ current }, action) => { + if (current.selectedAreaUnit === AreaUnitType.none) { + current.selectedAreaUnit = action.payload.defaultAreaUnit; } - if (!state.hotlinked) { - state.chartToRender = action.payload.defaultChartToRender - state.barStacking = action.payload.defaultBarStacking - state.areaNormalization = action.payload.defaultAreaNormalization + if (!current.hotlinked) { + current.chartToRender = action.payload.defaultChartToRender + current.barStacking = action.payload.defaultBarStacking + current.areaNormalization = action.payload.defaultAreaNormalization } }) }, // New Feature as of 2.0.0 Beta. selectors: { - selectGraphState: state => state, - selectThreeDState: state => state.threeD, - selectBarWidthDays: state => state.barDuration, - selectSelectedUnit: state => state.selectedUnit, - selectAreaUnit: state => state.selectedAreaUnit, - selectChartToRender: state => state.chartToRender, - selectLineGraphRate: state => state.lineGraphRate, - selectComparePeriod: state => state.comparePeriod, - selectSelectedMeters: state => state.selectedMeters, - selectSelectedGroups: state => state.selectedGroups, - selectSortingOrder: state => state.compareSortingOrder, - selectQueryTimeInterval: state => state.queryTimeInterval, - selectThreeDMeterOrGroup: state => state.threeD.meterOrGroup, - selectCompareTimeInterval: state => state.compareTimeInterval, - selectGraphAreaNormalization: state => state.areaNormalization, - selectThreeDMeterOrGroupID: state => state.threeD.meterOrGroupID, - selectThreeDReadingInterval: state => state.threeD.readingInterval + selectGraphState: state => state.current, + selectPrevHistory: state => state.prev, + selectForwardHistory: state => state.next, + selectThreeDState: state => state.current.threeD, + selectShowMinMax: state => state.current.showMinMax, + selectBarStacking: state => state.current.barStacking, + selectBarWidthDays: state => state.current.barDuration, + selectAreaUnit: state => state.current.selectedAreaUnit, + selectSelectedUnit: state => state.current.selectedUnit, + selectChartToRender: state => state.current.chartToRender, + selectLineGraphRate: state => state.current.lineGraphRate, + selectComparePeriod: state => state.current.comparePeriod, + selectSelectedMeters: state => state.current.selectedMeters, + selectSelectedGroups: state => state.current.selectedGroups, + selectSortingOrder: state => state.current.compareSortingOrder, + selectQueryTimeInterval: state => state.current.queryTimeInterval, + selectOptionsVisibility: state => state.current.optionsVisibility, + 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 } }) // Selectors that can be imported and used in 'useAppSelectors' and 'createSelectors' export const { + selectAreaUnit, + selectShowMinMax, + selectGraphState, + selectPrevHistory, selectThreeDState, + selectBarStacking, + selectSortingOrder, selectBarWidthDays, - selectGraphState, + selectSelectedUnit, + selectLineGraphRate, + selectComparePeriod, + selectChartToRender, + selectForwardHistory, selectSelectedMeters, selectSelectedGroups, + selectOptionsVisibility, selectQueryTimeInterval, - selectGraphAreaNormalization, - selectChartToRender, selectThreeDMeterOrGroup, + selectCompareTimeInterval, selectThreeDMeterOrGroupID, selectThreeDReadingInterval, - selectLineGraphRate, - selectAreaUnit, - selectSortingOrder, - selectSelectedUnit, - selectComparePeriod, - selectCompareTimeInterval + selectGraphAreaNormalization } = graphSlice.selectors // actionCreators exports export const { - confirmGraphRenderOnce, - updateSelectedMeters, - updateSelectedGroups, - updateSelectedUnit, - updateSelectedAreaUnit, + setHotlinked, + setShowMinMax, + setGraphState, + updateHistory, + setBarStacking, + toggleShowMinMax, + changeBarStacking, + resetTimeInterval, updateBarDuration, - updateTimeInterval, changeSliderRange, - resetRangeSliderStack, - updateComparePeriod, + updateTimeInterval, + updateSelectedUnit, + traverseNextHistory, + traversePrevHistory, changeChartToRender, - toggleAreaNormalization, + updateComparePeriod, + updateSelectedMeters, + updateLineGraphRate, setAreaNormalization, - toggleShowMinMax, - setShowMinMax, - changeBarStacking, - setBarStacking, - setHotlinked, - changeCompareSortingOrder, - toggleOptionsVisibility, setOptionsVisibility, - updateLineGraphRate, + updateSelectedGroups, + resetRangeSliderStack, + updateSelectedAreaUnit, + confirmGraphRenderOnce, + toggleOptionsVisibility, + toggleAreaNormalization, + updateThreeDMeterOrGroup, + changeCompareSortingOrder, + updateThreeDMeterOrGroupID, updateThreeDReadingInterval, updateThreeDMeterOrGroupInfo, - updateThreeDMeterOrGroupID, - updateThreeDMeterOrGroup, - updateSelectedMetersOrGroups, - resetTimeInterval, - setGraphState + updateSelectedMetersOrGroups } = graphSlice.actions + +export const historyStepBack = createAction('graph/HistoryStepBack') +export const HistoryStepForward = createAction('graph/HistoryStepForward') + + +const createGenericSlice = < + T, + Reducers extends SliceCaseReducers> +>({ + name = '', + initialState, + reducers +}: { + name: string + initialState: History + reducers: ValidateSliceCaseReducers, Reducers> +}) => { + return createSlice({ + name, + initialState, + reducers: { + updateHistory: (state: History, action: PayloadAction) => { + state.next = []; + state.prev.push(action.payload) + }, + traversePrevHistory: state => { + const prev = state.prev.pop() + if (prev) { + state.next.push(state.current) + state.current = prev + } + }, + traverseNextHistory: state => { + const next = state.next.pop() + if (next) { + state.prev.push(state.current) + state.current = next + } + }, + ...reducers + + } + }) +} + +export const wrappedSlice = createGenericSlice({ + name: 'test', + initialState: initialState, + reducers: { + toggleAreaNormalization: state => { + state.current.areaNormalization = !state.current.areaNormalization + } + } +}) \ No newline at end of file diff --git a/src/client/app/redux/middleware/graphHistory.ts b/src/client/app/redux/middleware/graphHistory.ts index b18b3a36a..c853180d4 100644 --- a/src/client/app/redux/middleware/graphHistory.ts +++ b/src/client/app/redux/middleware/graphHistory.ts @@ -1,34 +1,29 @@ // https://redux-toolkit.js.org/api/createListenerMiddleware#typescript-usage import { isAnyOf } from '@reduxjs/toolkit'; -import { clearHistory, forwardHistory, prevHistory, selectBackHistoryTop, updateHistory } from '../../reducers/appStateSlice'; -import { graphSlice, setGraphState, setHotlinked, setOptionsVisibility, toggleOptionsVisibility } from '../../reducers/graph'; import * as _ from 'lodash'; +import { + graphSlice, setGraphState, + setHotlinked, setOptionsVisibility, toggleOptionsVisibility, + traverseNextHistory, traversePrevHistory, updateHistory +} from '../../reducers/graph'; import { AppStartListening } from './middleware'; -// This middleware acts as a mediator between two slices of state. AppState, and GraphState. -// graphSlice cannot 'see' the appStateSlice, the middleware can see both and transact between the two. export const historyMiddleware = (startListening: AppStartListening) => { startListening({ predicate: (action, currentState, previousState) => { // deep compare of previous state added mostly due to potential state triggers from laying on backspace when deleting meters or groups. - return isHistoryTrigger(action) && !_.isEqual(currentState.graph, previousState.graph) + return isHistoryTrigger(action) && + !_.isEqual(currentState.graph, previousState.graph) } , - effect: (_action, { dispatch, getState }) => { - dispatch(updateHistory(getState().graph)) + effect: (action, { dispatch, getOriginalState }) => { + console.log('Running', action) + const prev = getOriginalState().graph.current + dispatch(updateHistory(prev)) } }) - // Listen for calls to traverse history forward or backwards - startListening({ - matcher: isAnyOf(forwardHistory, prevHistory, clearHistory), - effect: (_action, { dispatch, getState }) => { - // History Stack logic written such that after prev,or next, is executed, the history to set is the top of the backStack - const graphStateHistory = selectBackHistoryTop(getState()) - dispatch(setGraphState(graphStateHistory)) - } - }) } // we use updateHistory here, so listening for updateHistory would cause infinite loops etc. @@ -40,6 +35,9 @@ const isHistoryTrigger = isAnyOf( toggleOptionsVisibility.match(action) || setOptionsVisibility.match(action) || setHotlinked.match(action) || - setGraphState.match(action) + setGraphState.match(action) || + updateHistory.match(action) || + traverseNextHistory.match(action) || + traversePrevHistory.match(action) )) ) \ No newline at end of file diff --git a/src/client/index.html b/src/client/index.html index adfd4122c..b50f49007 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -22,6 +22,4 @@

OED requires JavaScript to run correctly. Please enable JavaScript.

- - \ No newline at end of file From 4cc8b9dd72e1f8b70586236f21469ad76fa3ef08 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Mon, 20 Nov 2023 23:09:00 +0000 Subject: [PATCH 046/131] CreateTunkSlice as init sequence --- src/client/app/components/HeaderComponent.tsx | 37 +++++------ src/client/app/index.tsx | 6 +- src/client/app/reducers/admin.ts | 8 ++- src/client/app/reducers/appStateSlice.ts | 62 +++++++++++++++++-- src/client/app/redux/api/unitsApi.ts | 1 - src/client/app/redux/slices/thunkSlice.ts | 5 ++ 6 files changed, 90 insertions(+), 29 deletions(-) create mode 100644 src/client/app/redux/slices/thunkSlice.ts diff --git a/src/client/app/components/HeaderComponent.tsx b/src/client/app/components/HeaderComponent.tsx index a9739a73c..11b563618 100644 --- a/src/client/app/components/HeaderComponent.tsx +++ b/src/client/app/components/HeaderComponent.tsx @@ -3,9 +3,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { useSelector } from 'react-redux'; import { Link, useLocation } from 'react-router-dom-v5-compat'; -import { State } from '../types/redux/state'; +import { selectOptionsVisibility } from '../reducers/graph'; +import { useAppSelector } from '../redux/hooks'; import HeaderButtonsComponent from './HeaderButtonsComponent'; import LogoComponent from './LogoComponent'; import MenuModalComponent from './MenuModalComponent'; @@ -15,20 +15,9 @@ import MenuModalComponent from './MenuModalComponent'; * @returns header element */ export default function HeaderComponent() { - const siteTitle = useSelector((state: State) => state.admin.displayTitle); - const showOptions = useSelector((state: State) => state.graph.optionsVisibility); + const siteTitle = useAppSelector(state => state.admin.displayTitle); + const showOptions = useAppSelector(selectOptionsVisibility); const { pathname } = useLocation() - const divStyle = { - marginTop: '5px', - paddingBottom: '5px' - }; - const largeTitleStyle = { - display: 'inline-block' - }; - const smallTitleStyle = { - display: 'inline-block', - marginTop: '10px' - }; return (
@@ -54,12 +43,24 @@ export default function HeaderComponent() {
{/* collapse menu if optionsVisibility is false */} - {pathname === '/' && !showOptions ? - : - + { + pathname === '/' && !showOptions + ? + : }
); } +const divStyle = { + marginTop: '5px', + paddingBottom: '5px' +}; +const largeTitleStyle = { + display: 'inline-block' +}; +const smallTitleStyle = { + display: 'inline-block', + marginTop: '10px' +}; \ No newline at end of file diff --git a/src/client/app/index.tsx b/src/client/app/index.tsx index 7a16ccf13..87d079b69 100644 --- a/src/client/app/index.tsx +++ b/src/client/app/index.tsx @@ -7,12 +7,12 @@ import * as React from 'react'; import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux'; import { store } from './store'; -// import RouteContainer from './containers/RouteContainer'; import RouteComponentWIP from './components/RouteComponentWIP'; -import { initializeApp } from './initScript'; +import { initApp } from './reducers/appStateSlice'; import './styles/index.css'; -initializeApp() +store.dispatch(initApp()) + // Renders the entire application, starting with RouteComponent, into the root div const container = document.getElementById('root') as HTMLElement; const root = createRoot(container); diff --git a/src/client/app/reducers/admin.ts b/src/client/app/reducers/admin.ts index 232331ab1..db6cabbdc 100644 --- a/src/client/app/reducers/admin.ts +++ b/src/client/app/reducers/admin.ts @@ -4,13 +4,13 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import * as moment from 'moment'; +import { preferencesApi } from '../redux/api/preferencesApi'; import { PreferenceRequestItem } from '../types/items'; import { AdminState } from '../types/redux/admin'; import { ChartTypes } from '../types/redux/graph'; import { LanguageTypes } from '../types/redux/i18n'; import { durationFormat } from '../utils/durationFormat'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; -import { preferencesApi } from '../redux/api/preferencesApi'; const defaultState: AdminState = { selectedMeter: null, @@ -141,7 +141,8 @@ export const adminSlice = createSlice({ })) }, selectors: { - selectAdminState: state => state + selectAdminState: state => state, + selectDisplayTitle: state => state.displayTitle } }); @@ -164,5 +165,6 @@ export const { } = adminSlice.actions export const { - selectAdminState + selectAdminState, + selectDisplayTitle } = adminSlice.selectors \ No newline at end of file diff --git a/src/client/app/reducers/appStateSlice.ts b/src/client/app/reducers/appStateSlice.ts index 0dbabbe57..f736497c7 100644 --- a/src/client/app/reducers/appStateSlice.ts +++ b/src/client/app/reducers/appStateSlice.ts @@ -1,4 +1,15 @@ -import { createSlice } from '@reduxjs/toolkit'; +import { authApi } from '../redux/api/authApi'; +import { conversionsApi } from '../redux/api/conversionsApi'; +import { groupsApi } from '../redux/api/groupsApi'; +import { metersApi } from '../redux/api/metersApi'; +import { preferencesApi } from '../redux/api/preferencesApi'; +import { unitsApi } from '../redux/api/unitsApi'; +import { userApi } from '../redux/api/userApi'; +import { versionApi } from '../redux/api/versionApi'; +import { createThunkSlice } from '../redux/slices/thunkSlice'; +import { deleteToken, getToken, hasToken } from '../utils/token'; +import { currentUserSlice } from './currentUser'; + interface appStateSlice { initComplete: boolean; } @@ -7,7 +18,7 @@ const defaultState: appStateSlice = { initComplete: false } -export const appStateSlice = createSlice({ +export const appStateSlice = createThunkSlice({ name: 'appState', initialState: defaultState, reducers: create => ({ @@ -15,7 +26,49 @@ export const appStateSlice = createSlice({ // Allows thunks inside of reducers, and prepareReducers with 'create' builder notation setInitComplete: create.reducer((state, action) => { state.initComplete = action.payload - }) + }), + initApp: create.asyncThunk( + // Thunk initiates many data fetching calls on startup before react begins to render + async (_unused: 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. + dispatch(versionApi.endpoints.getVersion.initiate()) + dispatch(preferencesApi.endpoints.getPreferences.initiate()) + dispatch(unitsApi.endpoints.getUnitsDetails.initiate()) + dispatch(conversionsApi.endpoints.getConversionsDetails.initiate()) + dispatch(conversionsApi.endpoints.getConversionArray.initiate()) + + // If user is an admin, they receive additional meter details. + // To avoid sending duplicate requests upon startup, verify user then fetch + if (hasToken()) { + // User has a session token verify before requesting meter/group details + try { + await dispatch(authApi.endpoints.verifyToken.initiate(getToken())) + // 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 })) + + } catch { + // User had a token that isn't valid or getUserDetails threw an error. + // Assume token is invalid. Delete if any + deleteToken() + } + + } + // Request meter/group/details post-auth + dispatch(metersApi.endpoints.getMeters.initiate()) + dispatch(groupsApi.endpoints.getGroups.initiate()) + }, + { + settled: state => { + state.initComplete = true + } + } + + ) + }), selectors: { selectInitComplete: state => state.initComplete @@ -23,7 +76,8 @@ export const appStateSlice = createSlice({ }) export const { - setInitComplete + setInitComplete, + initApp } = appStateSlice.actions export const { diff --git a/src/client/app/redux/api/unitsApi.ts b/src/client/app/redux/api/unitsApi.ts index 533434890..77db0ce73 100644 --- a/src/client/app/redux/api/unitsApi.ts +++ b/src/client/app/redux/api/unitsApi.ts @@ -14,7 +14,6 @@ export const unitsApi = baseApi.injectEndpoints({ query: () => 'api/units', transformResponse: (response: UnitData[]) => { return unitsAdapter.setAll(unitsInitialState, response) - return unitsAdapter.setAll(unitsInitialState, response) } }), addUnit: builder.mutation({ diff --git a/src/client/app/redux/slices/thunkSlice.ts b/src/client/app/redux/slices/thunkSlice.ts new file mode 100644 index 000000000..7dc8a7c36 --- /dev/null +++ b/src/client/app/redux/slices/thunkSlice.ts @@ -0,0 +1,5 @@ +import { buildCreateSlice, asyncThunkCreator } from '@reduxjs/toolkit' + +export const createThunkSlice = buildCreateSlice({ + creators: { asyncThunk: asyncThunkCreator } +}) \ No newline at end of file From f3ba7deabb9bd09dfcbff31cb6c16a81e5dd9011 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Mon, 27 Nov 2023 03:39:46 +0000 Subject: [PATCH 047/131] CutUnsaved warning for maps --- .../app/components/MapChartComponent.tsx | 29 ++++++---- .../MeterAndGroupSelectComponent.tsx | 6 +- .../app/components/RouteComponentWIP.tsx | 56 +++++++++++-------- .../app/components/SpinnerComponent.tsx | 17 +++--- .../app/components/UnitSelectComponent.tsx | 9 +-- .../conversion/ConversionsDetailComponent.tsx | 2 - .../maps/MapCalibrationComponent.tsx | 11 ++-- .../components/maps/MapsDetailComponent.tsx | 3 +- .../meters/MetersDetailComponentWIP.tsx | 2 +- .../containers/admin/UsersDetailContainer.tsx | 2 - src/client/app/reducers/appStateSlice.ts | 11 +++- src/client/app/redux/componentHooks.ts | 46 +-------------- .../app/redux/middleware/graphHistory.ts | 16 +++--- src/client/app/redux/selectors/uiSelectors.ts | 8 +++ src/client/app/store.ts | 2 +- 15 files changed, 98 insertions(+), 122 deletions(-) diff --git a/src/client/app/components/MapChartComponent.tsx b/src/client/app/components/MapChartComponent.tsx index ce9c2e26b..d0656a8f6 100644 --- a/src/client/app/components/MapChartComponent.tsx +++ b/src/client/app/components/MapChartComponent.tsx @@ -7,12 +7,17 @@ import * as moment from 'moment'; import * as React from 'react'; import Plot from 'react-plotly.js'; import { useSelector } from 'react-redux'; -import { selectMapChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; +import { + selectAreaUnit, selectBarWidthDays, + selectGraphAreaNormalization, selectSelectedGroups, + selectSelectedMeters, selectSelectedUnit +} from '../reducers/graph'; import { selectGroupDataById } from '../redux/api/groupsApi'; import { selectMeterDataById } from '../redux/api/metersApi'; import { readingsApi } from '../redux/api/readingsApi'; import { selectUnitDataById } from '../redux/api/unitsApi'; import { useAppSelector } from '../redux/hooks'; +import { selectMapChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; import { DataType } from '../types/Datasources'; import { State } from '../types/redux/state'; import { UnitRepresentType } from '../types/redux/units'; @@ -41,19 +46,21 @@ export default function MapChartComponent() { console.log(meterShouldSkip, groupShouldSkip, meterReadings, groupData) // converting maps to RTK has been proving troublesome, therefore using a combination of old/new stateSelectors - const unitID = useSelector((state: State) => state.graph.selectedUnit); - 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 barDuration = useSelector((state: State) => state.graph.barDuration) - const areaNormalization = useSelector((state: State) => state.graph.areaNormalization) - const selectedAreaUnit = useSelector((state: State) => state.graph.selectedAreaUnit) - const selectedMeters = useSelector((state: State) => state.graph.selectedMeters) - const selectedGroups = useSelector((state: State) => state.graph.selectedGroups) - + const unitID = useAppSelector(selectSelectedUnit); + const barDuration = useAppSelector(selectBarWidthDays) + const areaNormalization = useAppSelector(selectGraphAreaNormalization) + const selectedAreaUnit = useAppSelector(selectAreaUnit) + const selectedMeters = useAppSelector(selectSelectedMeters) + const selectedGroups = useAppSelector(selectSelectedGroups) const unitDataById = useAppSelector(selectUnitDataById) const groupDataById = useAppSelector(selectGroupDataById) const meterDataById = useAppSelector(selectMeterDataById) + + // 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) if (meterIsFetching || groupIsFetching) { return } diff --git a/src/client/app/components/MeterAndGroupSelectComponent.tsx b/src/client/app/components/MeterAndGroupSelectComponent.tsx index b37bba7e8..f61733166 100644 --- a/src/client/app/components/MeterAndGroupSelectComponent.tsx +++ b/src/client/app/components/MeterAndGroupSelectComponent.tsx @@ -8,8 +8,7 @@ import makeAnimated from 'react-select/animated'; import { Badge } from 'reactstrap'; import { graphSlice } from '../reducers/graph'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; -import { selectMeterGroupSelectData } from '../redux/selectors/uiSelectors'; -import { useFetchingStates } from '../redux/componentHooks'; +import { selectAnythingLoading, selectMeterGroupSelectData } from '../redux/selectors/uiSelectors'; import { GroupedOption, SelectOption } from '../types/items'; import { MeterOrGroup } from '../types/redux/graph'; import translate from '../utils/translate'; @@ -24,9 +23,8 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; export default function MeterAndGroupSelectComponent(props: MeterAndGroupSelectProps) { const dispatch = useAppDispatch(); const { meterGroupedOptions, groupsGroupedOptions, selectedMeterOptions, selectedGroupOptions } = useAppSelector(selectMeterGroupSelectData); - const { somethingIsFetching } = useFetchingStates(); + const somethingIsFetching = useAppSelector(selectAnythingLoading) const { meterOrGroup } = props; - // Set the current component's appropriate meter or group update from the graphSlice's Payload-Action Creator const value = meterOrGroup === MeterOrGroup.meters ? selectedMeterOptions.compatible : selectedGroupOptions.compatible; diff --git a/src/client/app/components/RouteComponentWIP.tsx b/src/client/app/components/RouteComponentWIP.tsx index 36ee89e7c..5b65181c2 100644 --- a/src/client/app/components/RouteComponentWIP.tsx +++ b/src/client/app/components/RouteComponentWIP.tsx @@ -34,14 +34,27 @@ import MetersDetailComponentWIP from './meters/MetersDetailComponentWIP'; import UnitsDetailComponent from './unit/UnitsDetailComponent'; +const initCompStyles: React.CSSProperties = { + width: '100%', height: '100%', + display: 'flex', flexDirection: 'column', + alignContent: 'center', alignItems: 'center' +} +export const InitializingComponent = () => { + return ( +
+

Initializing

+ +
+ ) +} export const AdminOutlet = () => { const { isAdmin, initComplete } = useWaitForInit(); if (!initComplete) { // Return a spinner until all init queries return and populate cache with data - return + return } // Keeping for now in case changes are desired @@ -54,23 +67,22 @@ export const AdminOutlet = () => { } // Function that returns a JSX element. Either the requested route's Component, as outlet or back to root -export const RoleOutlet = ({ UserRole }: { UserRole: UserRole }) => { +export const RoleOutlet = ({ role }: { role: UserRole }) => { const { userRole, initComplete } = useWaitForInit(); // // If state contains token it has been validated on startup or login. if (!initComplete) { - return + return } - // Keeping for now in case changes are desired - if (userRole === UserRole) { + if (userRole === role || userRole === UserRole.ADMIN) { return } - return + return } export const NotFound = () => { // redirect to home page if non-existent route is requested. - return + return } @@ -81,10 +93,12 @@ export const GraphLink = () => { const { initComplete } = useWaitForInit(); const dispatchQueue: PayloadAction[] = []; if (!initComplete) { - return + return } + try { URLSearchParams.forEach((value, key) => { + // TODO Needs to be refactored into a single dispatch/reducer pair. //TODO validation could be implemented across all cases similar to compare period and sorting order switch (key) { case 'chartType': @@ -171,7 +185,7 @@ export const GraphLink = () => { // All appropriate state updates should've been executed // redirect to clear the link - return + return } @@ -182,12 +196,11 @@ const router = createBrowserRouter([ path: '/', element: , children: [ { index: true, element: }, - { path: '/login', element: }, + { path: 'login', element: }, { path: 'groups', element: }, { path: 'meters', element: }, { path: 'graph', element: }, { - path: '/', element: , children: [ { path: 'admin', element: }, @@ -196,19 +209,16 @@ const router = createBrowserRouter([ { path: 'users/new', element: }, { path: 'units', element: }, { path: 'conversions', element: }, - { path: 'users', element: }, - { - path: '/', - element: , - children: [ - { path: 'csv', element: } - ] - }, - { - path: '*', element: - } + { path: 'users', element: } ] - } + }, + { + element: , + children: [ + { path: 'csv', element: } + ] + }, + { path: '*', element: } ] } ]) diff --git a/src/client/app/components/SpinnerComponent.tsx b/src/client/app/components/SpinnerComponent.tsx index cf0d83058..67f772460 100644 --- a/src/client/app/components/SpinnerComponent.tsx +++ b/src/client/app/components/SpinnerComponent.tsx @@ -24,17 +24,14 @@ function SpinnerComponent(props: SpinnerProps) { backgroundColor: props.color ? props.color : 'black' }; - return ( -
- {props.loading && -
-
-
-
-
- } + return props.loading ? +
+
+
+
- ); + : + null; } export default SpinnerComponent; diff --git a/src/client/app/components/UnitSelectComponent.tsx b/src/client/app/components/UnitSelectComponent.tsx index 014aef904..47846c1a9 100644 --- a/src/client/app/components/UnitSelectComponent.tsx +++ b/src/client/app/components/UnitSelectComponent.tsx @@ -13,9 +13,7 @@ import { Badge } from 'reactstrap'; import { graphSlice, selectSelectedUnit } from '../reducers/graph'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; -import { useFetchingStates } from '../redux/componentHooks'; -import { selectUnitDataById } from '../redux/api/unitsApi'; - +import { selectUnitDataById, unitsApi } from '../redux/api/unitsApi'; /** * @returns A React-Select component for UI Options Panel @@ -26,8 +24,7 @@ export default function UnitSelectComponent() { const selectedUnitID = useAppSelector(selectSelectedUnit); const unitsByID = useAppSelector(selectUnitDataById); - const { endpointsFetchingData } = useFetchingStates(); - + const { isFetching: unitsIsFetching } = unitsApi.endpoints.getUnitsDetails.useQueryState(); let selectedUnitOption: SelectOption | null = null; // Only use if valid/selected unit which means it is not -99. @@ -55,7 +52,7 @@ export default function UnitSelectComponent() { onChange={onChange} formatGroupLabel={formatGroupLabel} isClearable - isLoading={endpointsFetchingData.unitsData.unitsIsLoading} + isLoading={unitsIsFetching} />
) diff --git a/src/client/app/components/conversion/ConversionsDetailComponent.tsx b/src/client/app/components/conversion/ConversionsDetailComponent.tsx index cbb90d835..3af4800a8 100644 --- a/src/client/app/components/conversion/ConversionsDetailComponent.tsx +++ b/src/client/app/components/conversion/ConversionsDetailComponent.tsx @@ -8,7 +8,6 @@ import { FormattedMessage } from 'react-intl'; import { useSelector } from 'react-redux'; import { ConversionData } from 'types/redux/conversions'; import { fetchConversionsDetailsIfNeeded } from '../../actions/conversions'; -import HeaderComponent from '../../components/HeaderComponent'; import SpinnerComponent from '../../components/SpinnerComponent'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { selectConversionsDetails } from '../../redux/api/conversionsApi'; @@ -64,7 +63,6 @@ export default function ConversionsDetailComponent() {
) : (
-
diff --git a/src/client/app/components/maps/MapCalibrationComponent.tsx b/src/client/app/components/maps/MapCalibrationComponent.tsx index 46b6e092d..1d60d51cf 100644 --- a/src/client/app/components/maps/MapCalibrationComponent.tsx +++ b/src/client/app/components/maps/MapCalibrationComponent.tsx @@ -3,13 +3,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { CalibrationModeTypes } from '../../types/redux/map'; -import MapCalibrationInitiateContainer from '../../containers/maps/MapCalibrationInitiateContainer'; import MapCalibrationChartDisplayContainer from '../../containers/maps/MapCalibrationChartDisplayContainer'; import MapCalibrationInfoDisplayContainer from '../../containers/maps/MapCalibrationInfoDisplayContainer'; -import UnsavedWarningContainer from '../../containers/UnsavedWarningContainer'; -import HeaderComponent from '../../components/HeaderComponent'; +import MapCalibrationInitiateContainer from '../../containers/maps/MapCalibrationInitiateContainer'; import MapsDetailContainer from '../../containers/maps/MapsDetailContainer'; +import { CalibrationModeTypes } from '../../types/redux/map'; interface MapCalibrationProps { mode: CalibrationModeTypes; @@ -26,15 +24,14 @@ export default class MapCalibrationComponent extends React.Component - - + {/* */}
); } else if (this.props.mode === CalibrationModeTypes.calibrate) { return (
- + {/* */}
{/* TODO These types of plotly containers expect a lot of passed values and it gives a TS error. Given we plan to replace this diff --git a/src/client/app/components/maps/MapsDetailComponent.tsx b/src/client/app/components/maps/MapsDetailComponent.tsx index 320d0bbf6..c16bb09e0 100644 --- a/src/client/app/components/maps/MapsDetailComponent.tsx +++ b/src/client/app/components/maps/MapsDetailComponent.tsx @@ -7,7 +7,6 @@ import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router-dom-v5-compat'; import { Button, Table } from 'reactstrap'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; -import UnsavedWarningContainer from '../../containers/UnsavedWarningContainer'; import MapViewContainer from '../../containers/maps/MapViewContainer'; import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; import { store } from '../../store'; @@ -58,7 +57,7 @@ export default class MapsDetailComponent extends React.Component - + {/* */}

diff --git a/src/client/app/components/meters/MetersDetailComponentWIP.tsx b/src/client/app/components/meters/MetersDetailComponentWIP.tsx index 96db39216..25839cca1 100644 --- a/src/client/app/components/meters/MetersDetailComponentWIP.tsx +++ b/src/client/app/components/meters/MetersDetailComponentWIP.tsx @@ -20,7 +20,7 @@ import MeterViewComponentWIP from './MeterViewComponentWIP'; export default function MetersDetailComponent() { // Check for admin status - const isAdmin = useAppSelector(state => selectIsAdmin(state)); + const isAdmin = useAppSelector(selectIsAdmin); // We only want displayable meters if non-admins because they still have // non-displayable in state. const { visibleMeters } = useAppSelector(selectVisibleMeterAndGroupData); diff --git a/src/client/app/containers/admin/UsersDetailContainer.tsx b/src/client/app/containers/admin/UsersDetailContainer.tsx index 50c765927..c8deef654 100644 --- a/src/client/app/containers/admin/UsersDetailContainer.tsx +++ b/src/client/app/containers/admin/UsersDetailContainer.tsx @@ -4,7 +4,6 @@ import * as _ from 'lodash'; import * as React from 'react'; -import HeaderComponent from '../../components/HeaderComponent'; import UserDetailComponent from '../../components/admin/UsersDetailComponent'; import { User, UserRole } from '../../types/items'; import { usersApi } from '../../utils/api'; @@ -81,7 +80,6 @@ export default class UsersDetailContainer extends React.Component - { + 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. dispatch(versionApi.endpoints.getVersion.initiate()) @@ -38,22 +39,30 @@ export const appStateSlice = createThunkSlice({ dispatch(conversionsApi.endpoints.getConversionsDetails.initiate()) dispatch(conversionsApi.endpoints.getConversionArray.initiate()) + // Older style thunk fetch cycle for maps until migration + dispatch(fetchMapsDetails()) + // If user is an admin, they receive additional meter details. // To avoid sending duplicate requests upon startup, verify user then fetch 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 }) // 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 }) } 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.setUserToken(null)) } } diff --git a/src/client/app/redux/componentHooks.ts b/src/client/app/redux/componentHooks.ts index 31a9d1fc9..dc3e3c0d4 100644 --- a/src/client/app/redux/componentHooks.ts +++ b/src/client/app/redux/componentHooks.ts @@ -1,12 +1,7 @@ // import * as React from 'react'; -import { groupsApi } from './api/groupsApi'; -import { metersApi } from './api/metersApi'; -import { readingsApi } from './api/readingsApi'; -import { useAppSelector } from './hooks'; -import { selectAllChartQueryArgs } from './selectors/chartQuerySelectors'; -import { unitsApi } from './api/unitsApi'; import { selectInitComplete } from '../reducers/appStateSlice'; -import { selectIsAdmin, selectCurrentUserRole } from '../reducers/currentUser'; +import { selectCurrentUserRole, selectIsAdmin } from '../reducers/currentUser'; +import { useAppSelector } from './hooks'; export const useWaitForInit = () => { @@ -14,39 +9,4 @@ export const useWaitForInit = () => { const userRole = useAppSelector(selectCurrentUserRole); const initComplete = useAppSelector(selectInitComplete); return { isAdmin, userRole, initComplete } -} - -// General purpose custom hook mostly useful for Select component loadingIndicators, and current graph loading state(s) -export const useFetchingStates = () => { - const queryArgs = useAppSelector(state => selectAllChartQueryArgs(state)); - const { isFetching: meterLineIsFetching, isLoading: meterLineIsLoading } = readingsApi.endpoints.line.useQueryState(queryArgs.line.meterArgs); - const { isFetching: groupLineIsFetching, isLoading: groupLineIsLoading } = readingsApi.endpoints.line.useQueryState(queryArgs.line.groupArgs); - const { isFetching: meterBarIsFetching, isLoading: meterBarIsLoading } = readingsApi.endpoints.bar.useQueryState(queryArgs.bar.meterArgs); - const { isFetching: groupBarIsFetching, isLoading: groupBarIsLoading } = readingsApi.endpoints.bar.useQueryState(queryArgs.bar.groupArgs); - const { isFetching: threeDIsFetching, isLoading: threeDIsLoading } = readingsApi.endpoints.threeD.useQueryState(queryArgs.threeD.args); - const { isFetching: metersFetching, isLoading: metersLoading } = metersApi.endpoints.getMeters.useQueryState(); - const { isFetching: groupsFetching, isLoading: groupsLoading } = groupsApi.endpoints.getGroups.useQueryState(); - const { isFetching: unitsIsFetching, isLoading: unitsIsLoading } = unitsApi.endpoints.getUnitsDetails.useQueryState(); - - - return { - endpointsFetchingData: { - lineMeterReadings: { meterLineIsFetching, meterLineIsLoading }, - lineGroupReadings: { groupLineIsFetching, groupLineIsLoading }, - barMeterReadings: { meterBarIsFetching, meterBarIsLoading }, - barGroupReadings: { groupBarIsFetching, groupBarIsLoading }, - threeDReadings: { threeDIsFetching, threeDIsLoading }, - meterData: { metersFetching, metersLoading }, - groupData: { groupsFetching, groupsLoading }, - unitsData: { unitsIsFetching, unitsIsLoading } - }, - somethingIsFetching: meterLineIsFetching || - groupLineIsFetching || - meterBarIsFetching || - groupBarIsFetching || - threeDIsFetching || - metersFetching || - groupsFetching || - unitsIsFetching - } -} +} \ No newline at end of file diff --git a/src/client/app/redux/middleware/graphHistory.ts b/src/client/app/redux/middleware/graphHistory.ts index c853180d4..07e4145ed 100644 --- a/src/client/app/redux/middleware/graphHistory.ts +++ b/src/client/app/redux/middleware/graphHistory.ts @@ -12,13 +12,11 @@ export const historyMiddleware = (startListening: AppStartListening) => { startListening({ predicate: (action, currentState, previousState) => { - // deep compare of previous state added mostly due to potential state triggers from laying on backspace when deleting meters or groups. - return isHistoryTrigger(action) && - !_.isEqual(currentState.graph, previousState.graph) - } - , - effect: (action, { dispatch, getOriginalState }) => { - console.log('Running', action) + // deep compare of previous state added mostly due to potential state triggers/ dispatches that may not actually alter state + // For example 'popping' values from react-select w/ backspace when empty + return isHistoryTrigger(action) && !_.isEqual(currentState.graph, previousState.graph) + }, + effect: (_action, { dispatch, getOriginalState }) => { const prev = getOriginalState().graph.current dispatch(updateHistory(prev)) } @@ -26,12 +24,12 @@ export const historyMiddleware = (startListening: AppStartListening) => { } -// we use updateHistory here, so listening for updateHistory would cause infinite loops etc. +// listen to all graphSlice actions const isHistoryTrigger = isAnyOf( - // listen to all graphSlice actions ...Object.values(graphSlice.actions) .filter(action => !( // filter out the ones don't directly alter the graph, or ones which can cause infinite recursion + // we use updateHistory here, so listening for updateHistory would cause infinite loops etc. toggleOptionsVisibility.match(action) || setOptionsVisibility.match(action) || setHotlinked.match(action) || diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts index 34f735bad..d9af59b5f 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -26,6 +26,9 @@ import { selectVisibleMetersAndGroups, selectVisibleUnitOrSuffixState } from './ import { MeterDataByID } from 'types/redux/meters'; import { GroupDataByID } from 'types/redux/groups'; import { selectMeterDataById } from '../../redux/api/metersApi'; +import { RootState } from '../../store'; +import { QueryStatus } from '@reduxjs/toolkit/query'; + @@ -452,4 +455,9 @@ const isAreaNormCompatible = (id: number, selectedUnit: number, meterOrGroupData const noAreaOrUnitType = meterOrGroupData[id].area === 0 || meterOrGroupData[id].areaUnit === AreaUnitType.none const isAreaNormCompatible = !noUnitAndRaw && !noAreaOrUnitType return isAreaNormCompatible +} + +export const selectAnythingLoading = (state: RootState) => { + const anythingLoading = Object.values(state.api.queries).some(entry => entry?.status === QueryStatus.pending) + return anythingLoading; } \ No newline at end of file diff --git a/src/client/app/store.ts b/src/client/app/store.ts index b02f9e10f..e252d0a92 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -12,7 +12,7 @@ import { setInputStabilityCheckEnabled } from 'reselect' export const store = configureStore({ reducer: rootReducer, middleware: getDefaultMiddleware => getDefaultMiddleware({ - // immutableCheck: false, + immutableCheck: false, serializableCheck: false }) .prepend(listenerMiddleware.middleware) From 3869ec5a73992d21d764227195562e1f328b7319 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Fri, 1 Dec 2023 01:32:23 +0000 Subject: [PATCH 048/131] Multi Select Changes - Fix Remove Values. - Add Disabled indicator w/ tooltip --- .../MeterAndGroupSelectComponent.tsx | 62 ++++++++++++++----- src/client/app/reducers/graph.ts | 14 ++--- 2 files changed, 55 insertions(+), 21 deletions(-) diff --git a/src/client/app/components/MeterAndGroupSelectComponent.tsx b/src/client/app/components/MeterAndGroupSelectComponent.tsx index f61733166..7d7e5d745 100644 --- a/src/client/app/components/MeterAndGroupSelectComponent.tsx +++ b/src/client/app/components/MeterAndGroupSelectComponent.tsx @@ -3,8 +3,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import Select, { ActionMeta, MultiValue, StylesConfig } from 'react-select'; +import Select, { + ActionMeta, MultiValue, + MultiValueGenericProps, MultiValueProps, + StylesConfig, components +} from 'react-select'; import makeAnimated from 'react-select/animated'; +import ReactTooltip from 'react-tooltip'; import { Badge } from 'reactstrap'; import { graphSlice } from '../reducers/graph'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; @@ -22,12 +27,12 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; */ export default function MeterAndGroupSelectComponent(props: MeterAndGroupSelectProps) { const dispatch = useAppDispatch(); - const { meterGroupedOptions, groupsGroupedOptions, selectedMeterOptions, selectedGroupOptions } = useAppSelector(selectMeterGroupSelectData); + const { meterGroupedOptions, groupsGroupedOptions, allSelectedMeterValues, allSelectedGroupValues } = useAppSelector(selectMeterGroupSelectData); const somethingIsFetching = useAppSelector(selectAnythingLoading) const { meterOrGroup } = props; // Set the current component's appropriate meter or group update from the graphSlice's Payload-Action Creator - const value = meterOrGroup === MeterOrGroup.meters ? selectedMeterOptions.compatible : selectedGroupOptions.compatible; + const value = meterOrGroup === MeterOrGroup.meters ? allSelectedMeterValues : allSelectedGroupValues; // Set the current component's appropriate meter or group SelectOption const options = meterOrGroup === MeterOrGroup.meters ? meterGroupedOptions : groupsGroupedOptions; @@ -73,21 +78,42 @@ const formatGroupLabel = (data: GroupedOption) => { {data.label} {data.options.length}

- ) } interface MeterAndGroupSelectProps { meterOrGroup: MeterOrGroup; } -const divBottomPadding: React.CSSProperties = { - paddingBottom: '15px' -}; -const labelStyle: React.CSSProperties = { - fontWeight: 'bold', - margin: 0 -}; -const animatedComponents = makeAnimated(); + +const MultiValueLabel = (props: MultiValueGenericProps) => { + // Types for makeAnimated are generic, and does not offer completion, so type assert + const typedProps = props as MultiValueProps + const ref = React.useRef(null); + // TODO would be nice if relevant message was derived from uiSelectors, which currently only tracks / trims non-compatible ids + // TODO Add meta data along chain? i.e. disabled due to chart type, area norm... etc. and display relevant message. + return typedProps.data.isDisabled ? + // TODO Verify behavior, and set proper message/ translate + < div ref={ref} data-for={'home'} data-tip={'help.home.area.normalize'} + onMouseDown={e => e.stopPropagation()} + onClick={e => { + ReactTooltip.rebuild() + e.stopPropagation() + ref.current && ReactTooltip.show(ref.current) + }} + style={{ overflow: 'hidden' }} + > + +
+ : + +} + +const animatedComponents = makeAnimated({ + ...components, + MultiValueLabel +}); + + const customStyles: StylesConfig = { valueContainer: base => ({ ...base, @@ -99,9 +125,17 @@ const customStyles: StylesConfig = { 'msOverflowStyle': 'none', 'scrollbarWidth': 'none' }), - multiValue: base => ({ - ...base + multiValue: (base, props) => ({ + ...base, + backgroundColor: props.data.isDisabled ? 'hsl(0, 0%, 70%)' : base.backgroundColor }) }; +const divBottomPadding: React.CSSProperties = { + paddingBottom: '15px' +}; +const labelStyle: React.CSSProperties = { + fontWeight: 'bold', + margin: 0 +}; diff --git a/src/client/app/reducers/graph.ts b/src/client/app/reducers/graph.ts index ec814014f..17ce142e1 100644 --- a/src/client/app/reducers/graph.ts +++ b/src/client/app/reducers/graph.ts @@ -150,8 +150,8 @@ export const graphSlice = createSlice({ // Destructure payload const { newMetersOrGroups, meta } = action.payload; const cleared = meta.action === 'clear' - const valueRemoved = meta.action === 'pop-value' || meta.action === 'remove-value' - const valueAdded = meta.action === 'select-option' + const valueRemoved = (meta.action === 'pop-value' || meta.action === 'remove-value') && meta.removedValue + const valueAdded = meta.action === 'select-option' && meta.option let isAMeter = true if (cleared) { @@ -163,8 +163,8 @@ export const graphSlice = createSlice({ isAMeter ? current.selectedMeters = [] : current.selectedGroups = [] } - if (valueRemoved && meta.option) { - const isAMeter = meta.removedValue.meterOrGroup === MeterOrGroup.meters; + if (valueRemoved) { + isAMeter = meta.removedValue.meterOrGroup === MeterOrGroup.meters; // An entry was deleted. // Update either selected meters or groups @@ -173,9 +173,9 @@ export const graphSlice = createSlice({ : current.selectedGroups = newMetersOrGroups } - if (valueAdded && meta.option) { - isAMeter = meta.option.meterOrGroup === MeterOrGroup.meters; - const addedMeterOrGroupUnit = meta.option.defaultGraphicUnit; + if (valueAdded) { + isAMeter = meta.option?.meterOrGroup === MeterOrGroup.meters; + const addedMeterOrGroupUnit = meta.option?.defaultGraphicUnit; // An entry was added, // Update either selected meters or groups isAMeter ? From fc13310ac7c7f51c242abe92f245828617c0dc72 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Wed, 10 Jan 2024 00:38:21 +0000 Subject: [PATCH 049/131] Legacy Router Cleanup --- .../app/components/DashboardComponent.tsx | 2 - src/client/app/components/RouteComponent.tsx | 379 +++--------------- .../app/components/RouteComponentWIP.tsx | 237 ----------- .../app/components/router/AdminOutlet.tsx | 22 + .../app/components/router/ErrorComponent.tsx | 31 ++ .../components/router/GraphLinkComponent.tsx | 112 ++++++ .../router/InitializingComponent.tsx | 23 ++ .../app/components/router/NotFoundOutlet.tsx | 14 + .../app/components/router/RoleOutlet.tsx | 32 ++ src/client/app/index.tsx | 4 +- 10 files changed, 297 insertions(+), 559 deletions(-) delete mode 100644 src/client/app/components/RouteComponentWIP.tsx create mode 100644 src/client/app/components/router/AdminOutlet.tsx create mode 100644 src/client/app/components/router/ErrorComponent.tsx create mode 100644 src/client/app/components/router/GraphLinkComponent.tsx create mode 100644 src/client/app/components/router/InitializingComponent.tsx create mode 100644 src/client/app/components/router/NotFoundOutlet.tsx create mode 100644 src/client/app/components/router/RoleOutlet.tsx diff --git a/src/client/app/components/DashboardComponent.tsx b/src/client/app/components/DashboardComponent.tsx index a1e34627d..6dde7f5e0 100644 --- a/src/client/app/components/DashboardComponent.tsx +++ b/src/client/app/components/DashboardComponent.tsx @@ -24,8 +24,6 @@ export default function DashboardComponent() { const optionsVisibility = useAppSelector(selectOptionsVisibility); const optionsClassName = optionsVisibility ? 'col-2 d-none d-lg-block' : 'd-none'; const chartClassName = optionsVisibility ? 'col-12 col-lg-10' : 'col-12'; - // const optionsClassName = optionsVisibility ? 'col-3 d-none d-lg-block' : 'd-none'; - // const chartClassName = optionsVisibility ? 'col-12 col-lg-9' : 'col-12'; return (
diff --git a/src/client/app/components/RouteComponent.tsx b/src/client/app/components/RouteComponent.tsx index 25f098753..e6178eaf5 100644 --- a/src/client/app/components/RouteComponent.tsx +++ b/src/client/app/components/RouteComponent.tsx @@ -1,331 +1,74 @@ -/* 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 */ /* 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 { Route, Router, Switch, Redirect } from 'react-router-dom'; import { IntlProvider } from 'react-intl'; -import LocaleTranslationData from '../translations/data'; -import { browserHistory } from '../utils/history'; -import * as _ from 'lodash'; -import * as moment from 'moment'; -import HomeComponent from './HomeComponent'; -import AdminComponent from './admin/AdminComponent'; -import { LinkOptions } from '../actions/graph'; -import { hasToken, deleteToken } from '../utils/token'; -import { showErrorNotification } from '../utils/notifications'; -import { ChartTypes, LineGraphRate, MeterOrGroup } from '../types/redux/graph'; -import { LanguageTypes } from '../types/redux/i18n'; -import { verificationApi } from '../utils/api'; -import translate from '../utils/translate'; -import { validateComparePeriod, validateSortingOrder } from '../utils/calculateCompare'; -import UsersDetailContainer from '../containers/admin/UsersDetailContainer'; +import { RouterProvider, createBrowserRouter } from 'react-router-dom'; import CreateUserContainer from '../containers/admin/CreateUserContainer'; -import { TimeInterval } from '../../../common/TimeInterval'; -import MapsDetailContainer from '../containers/maps/MapsDetailContainer'; -import MapCalibrationContainer from '../containers/maps/MapCalibrationContainer'; import UploadCSVContainer from '../containers/csv/UploadCSVContainer'; +import MapCalibrationContainer from '../containers/maps/MapCalibrationContainer'; +import MapsDetailContainer from '../containers/maps/MapsDetailContainer'; +import { useAppSelector } from '../redux/hooks'; +import LocaleTranslationData from '../translations/data'; import { UserRole } from '../types/items'; -import { hasPermissions } from '../utils/hasPermissions'; -import UnitsDetailComponent from './unit/UnitsDetailComponent'; -import MetersDetailComponent from './meters/MetersDetailComponent'; -import GroupsDetailComponent from './groups/GroupsDetailComponent'; -import ConversionsDetailComponent from './conversion/ConversionsDetailComponent'; -import queryString from 'query-string'; +import AppLayout from './AppLayout'; +import HomeComponent from './HomeComponent'; import LoginComponent from './LoginComponent'; +import AdminComponent from './admin/AdminComponent'; +import UsersDetailComponentWIP from './admin/UsersDetailComponentWIP'; +import ConversionsDetailComponentWIP from './conversion/ConversionsDetailComponentWIP'; +import GroupsDetailComponentWIP from './groups/GroupsDetailComponentWIP'; +import MetersDetailComponentWIP from './meters/MetersDetailComponentWIP'; +import AdminOutlet from './router/AdminOutlet'; +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'; -interface RouteProps { - barStacking: boolean; - selectedLanguage: LanguageTypes; - loggedInAsAdmin: boolean; - role: UserRole; - renderOnce: boolean; - areaNormalization: boolean; - minMax: boolean; - changeOptionsFromLink(options: LinkOptions): Promise; - clearCurrentUser(): any; - changeRenderOnce(): any; +/** + * @returns the router component Responsible for client side routing. + */ +export default function RouteComponent() { + const lang = useAppSelector(state => state.options.selectedLanguage) + const messages = (LocaleTranslationData)[lang]; + return ( + + + + ) } -export default class RouteComponent extends React.Component { - constructor(props: RouteProps) { - super(props); - this.requireAuth = this.requireAuth.bind(this); - this.linkToGraph = this.linkToGraph.bind(this); - this.requireRole = this.requireRole.bind(this); +// Router Responsible for client side routing. +const router = createBrowserRouter([ + { + // TODO Error Component needs to be implemented, Its currently a bare bones placeholder + path: '/', element: , errorElement: , + children: [ + { index: true, element: }, + { path: 'login', element: }, + { path: 'groups', element: }, + { path: 'meters', element: }, + { path: 'graph', element: }, + { + element: , + children: [ + { path: 'admin', element: }, + { path: 'calibration', element: }, + { path: 'maps', element: }, + { path: 'users/new', element: }, + { path: 'units', element: }, + { path: 'conversions', element: }, + { path: 'users', element: } + ] + }, + { + element: , + children: [ + { path: 'csv', element: } + ] + }, + { path: '*', element: } + ] } - - /** - * TODO The following three functions, requireRole, requireAuth, and checkAuth, do not work exactly as intended. - * Their async blocks evaluate properly, but the returns inside of them are never honored. The end return statement is always what is evaluated. - * Fixing this may require some major changes to how page redirects are done. This is detailed more in issue #817. - * The errors can be obtained by putting breakpoints on all returns and then stepping through a page load in a debugger. - */ - - /** - * Generates middleware that requires proper role and authentication for a page route - * @param requiredRole The role that is necessary to access a page route - * @param component The component of the page redirecting - * @returns The page route to continue to (component or home) - */ - public requireRole(requiredRole: UserRole, component: JSX.Element) { - // Redirect route to login page if the auth token does not exist or if the user is not the required role - if (!hasToken() || !hasPermissions(this.props.role, requiredRole)) { - return ; - } - // Verify that the auth token is valid. - // Needs to be async because of the network request - (async () => { - if (!(await verificationApi.checkTokenValid())) { - showErrorNotification(translate('invalid.token.login.admin')); - // We should delete the token when we know that it is expired. Ensures that we don't not leave any unwanted tokens around. - deleteToken(); - // This ensures that if there is no token then there is no stale profile in the redux store. - this.props.clearCurrentUser(); - // Route to home page if the auth token is not valid - // this is never properly honored - return ; - } - return component; - })(); - - return component; - } - - /** - * Middleware function that requires proper authentication for a page route - * @param component The component of the page redirecting - * @returns The page route to continue to (component or home) - */ - public requireAuth(component: JSX.Element) { - // Redirect route to home page if the auth token does not exist or if the user is not an admin - if (!hasToken() || !this.props.loggedInAsAdmin) { - return ; - } - // Verify that the auth token is valid. - // Needs to be async because of the network request - (async () => { - if (!(await verificationApi.checkTokenValid())) { - showErrorNotification(translate('invalid.token.login.admin')); - // We should delete the token when we know that it is expired. Ensures that we don't not leave any unwanted tokens around. - deleteToken(); - // This ensures that if there is no token then there is no stale profile in the redux store. - this.props.clearCurrentUser(); - // Route to login page since the auth token is not valid - // this is never properly honored - return ; - } - return component; - })(); - return component; - } - - /** - * Middleware function that checks proper authentication for a page route - * @param component The component of the page redirecting - * @returns component - */ - public checkAuth(component: JSX.Element) { - // Only check the token if the auth token exists - if (hasToken()) { - // Verify that the auth token is valid. - // Needs to be async because of the network request - (async () => { - if (!(await verificationApi.checkTokenValid())) { - showErrorNotification(translate('invalid.token.login')); - // We should delete the token when we know that it is expired. Ensures that we don't not leave any unwanted tokens around. - deleteToken(); - // This ensures that if there is no token then there is no stale profile in the redux store. - this.props.clearCurrentUser(); - } - // redundant return, not needed even if it did work - return component; - })(); - } - return component; - } - - /** - * Middleware function that allows hotlinking to a graph with options - * @param component The component of the page redirecting - * @param search The string of queries in the path - * @returns component - */ - public linkToGraph(component: JSX.Element, search: string) { - /* - * This stops the chart links from processing more than once. Initially renderOnce is false - * so the code executes but then it is set to true near the end so it will not do it again. - * This is somewhat more efficient but, more importantly, it fixed a bug. The URL did not clear - * until a different page was loaded. While most selections did not route /graph, some do - * so this function is called. The bug was that when the user clicked on bar stacking, that - * action caused the action to happen but then it happened again here. This caused the boolean - * to flip twice so it was unchanged in the end. It is possible that other issues could exist - * but should be gone now. - */ - if (!this.props.renderOnce) { - const queries: any = queryString.parse(search); - if (!_.isEmpty(queries)) { - try { - const options: LinkOptions = {}; - for (const [key, infoObj] of _.entries(queries)) { - // TODO The upgrade of TypeScript lead to it giving an error for the type of infoObj - // which it thinks is unknown. I'm not sure why and this is code from the history - // package (see modules/@types/history/index.d.ts). What follows is a hack where - // the type is cast to any. This removes the problem and also allowed the removal - // of the ! to avoid calling toString when it is a bad value. I think this is okay - // because the toString documentation indicates it works fine with any type including - // null and unknown. If it does convert then the default case will catch it as an error. - // I want to get rid of this issue so Travis testing is not stopped by this. However, - // we should look into this typing issue more to see what might be a better fix. - const fixTypeIssue: any = infoObj as any; - const info: string = fixTypeIssue.toString(); - // ESLint does not want const params in the one case it is used so put here. - let params; - //TODO validation could be implemented across all cases similar to compare period and sorting order - switch (key) { - case 'meterIDs': - options.meterIDs = info.split(',').map(s => parseInt(s)); - break; - case 'groupIDs': - options.groupIDs = info.split(',').map(s => parseInt(s)); - break; - case 'chartType': - options.chartType = info as ChartTypes; - break; - case 'unitID': - options.unitID = parseInt(info); - break; - case 'rate': - params = info.split(','); - options.rate = { label: params[0], rate: parseFloat(params[1]) } as LineGraphRate; - break; - case 'barDuration': - options.barDuration = moment.duration(parseInt(info), 'days'); - break; - case 'barStacking': - if (this.props.barStacking.toString() !== info) { - options.toggleBarStacking = true; - } - break; - case 'areaNormalization': - if (this.props.areaNormalization.toString() !== info) { - options.toggleAreaNormalization = true; - } - break; - case 'areaUnit': - options.areaUnit = info; - break; - case 'minMax': - if (this.props.minMax.toString() !== info) { - options.toggleMinMax = true; - } - break; - case 'comparePeriod': - options.comparePeriod = validateComparePeriod(info); - break; - case 'compareSortingOrder': - options.compareSortingOrder = validateSortingOrder(info); - break; - case 'optionsVisibility': - options.optionsVisibility = (info === 'true'); - break; - case 'mapID': - options.mapID = (parseInt(info)); - break; - case 'serverRange': - options.serverRange = TimeInterval.fromString(info); - /** - * commented out since days from present feature is not currently used - */ - // const index = info.indexOf('dfp'); - // if (index === -1) { - // options.serverRange = TimeInterval.fromString(info); - // } else { - // const message = info.substring(0, index); - // const stringField = this.getNewIntervalFromMessage(message); - // options.serverRange = TimeInterval.fromString(stringField); - // } - break; - case 'sliderRange': - options.sliderRange = TimeInterval.fromString(info); - break; - case 'meterOrGroupID': - options.meterOrGroupID = parseInt(info); - break; - case 'meterOrGroup': - options.meterOrGroup = info as MeterOrGroup; - break; - case 'readingInterval': - options.readingInterval = parseInt(info); - break; - default: - throw new Error('Unknown query parameter'); - } - } - // The chartlink was processed so note so will not be done again. - this.props.changeRenderOnce(); - if (Object.keys(options).length > 0) { - this.props.changeOptionsFromLink(options); - } - } catch (err) { - showErrorNotification(translate('failed.to.link.graph')); - } - } - } - return component; - } - - /** - * React component that controls the app's routes - * Note that '/admin', '/editGroup', and '/createGroup' requires authentication - * @returns JSX to create the RouteComponent - */ - public render() { - const lang = this.props.selectedLanguage; - const messages = (LocaleTranslationData as any)[lang]; - return ( -
- - <> - - - - this.requireAuth(AdminComponent())} /> - this.requireRole(UserRole.CSV, )} /> - this.checkAuth()} /> - this.checkAuth()} /> - this.linkToGraph(, location.search)} /> - this.requireAuth()} /> - this.requireAuth()} /> - this.requireAuth()} /> - this.requireAuth( []} />)} /> - this.requireAuth()} /> - this.requireAuth()} /> - - - - - -
- ); - } - - /** - * Generates new time interval based on current time and user selected amount to trace back; - * @param {string} message currently able to accept how many days to go back in time; - * @returns {string} interval as a string - */ - // private getNewIntervalFromMessage(message: string) { - // const numDays = parseInt(message); - // If we ever use this code we might need to fix up moment for UTC as elsewhere in the code. - // const current = moment(); - // const newMinTimeStamp = current.clone(); - // newMinTimeStamp.subtract(numDays, 'days'); - // return newMinTimeStamp.toISOString().substring(0, 19) + 'Z_' + current.toISOString().substring(0, 19) + 'Z'; - // } -} +]) \ No newline at end of file diff --git a/src/client/app/components/RouteComponentWIP.tsx b/src/client/app/components/RouteComponentWIP.tsx deleted file mode 100644 index 6b3270c9f..000000000 --- a/src/client/app/components/RouteComponentWIP.tsx +++ /dev/null @@ -1,237 +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 { PayloadAction } from '@reduxjs/toolkit'; -import * as moment from 'moment'; -import * as React from 'react'; -import { IntlProvider } from 'react-intl'; -import { Navigate, Outlet, RouterProvider, createBrowserRouter, useSearchParams } from 'react-router-dom'; -import { TimeInterval } from '../../../common/TimeInterval'; -import CreateUserContainer from '../containers/admin/CreateUserContainer'; -import UploadCSVContainer from '../containers/csv/UploadCSVContainer'; -import MapCalibrationContainer from '../containers/maps/MapCalibrationContainer'; -import MapsDetailContainer from '../containers/maps/MapsDetailContainer'; -import { graphSlice } from '../reducers/graph'; -import { useWaitForInit } from '../redux/componentHooks'; -import { useAppDispatch, useAppSelector } from '../redux/hooks'; -import LocaleTranslationData from '../translations/data'; -import { UserRole } from '../types/items'; -import { ChartTypes, LineGraphRate, MeterOrGroup } from '../types/redux/graph'; -import { validateComparePeriod, validateSortingOrder } from '../utils/calculateCompare'; -import { AreaUnitType } from '../utils/getAreaUnitConversion'; -import { showErrorNotification } from '../utils/notifications'; -import translate from '../utils/translate'; -import AppLayout from './AppLayout'; -import HomeComponent from './HomeComponent'; -import LoginComponent from './LoginComponent'; -import SpinnerComponent from './SpinnerComponent'; -import AdminComponent from './admin/AdminComponent'; -import UsersDetailComponentWIP from './admin/UsersDetailComponentWIP'; -import ConversionsDetailComponentWIP from './conversion/ConversionsDetailComponentWIP'; -import GroupsDetailComponentWIP from './groups/GroupsDetailComponentWIP'; -import MetersDetailComponentWIP from './meters/MetersDetailComponentWIP'; -import UnitsDetailComponent from './unit/UnitsDetailComponent'; - - -const initCompStyles: React.CSSProperties = { - width: '100%', height: '100%', - display: 'flex', flexDirection: 'column', - alignContent: 'center', alignItems: 'center' -} -export const InitializingComponent = () => { - return ( -
-

Initializing

- -
- - ) -} - -export const AdminOutlet = () => { - const { isAdmin, initComplete } = useWaitForInit(); - - if (!initComplete) { - // Return a spinner until all init queries return and populate cache with data - return - } - - // Keeping for now in case changes are desired - if (isAdmin) { - return - } - - return - -} - -// Function that returns a JSX element. Either the requested route's Component, as outlet or back to root -export const RoleOutlet = ({ role }: { role: UserRole }) => { - const { userRole, initComplete } = useWaitForInit(); - // // If state contains token it has been validated on startup or login. - if (!initComplete) { - return - } - if (userRole === role || userRole === UserRole.ADMIN) { - return - } - - return -} - -export const NotFound = () => { - // redirect to home page if non-existent route is requested. - return -} - - -// TODO fix this route -export const GraphLink = () => { - const dispatch = useAppDispatch(); - const [URLSearchParams] = useSearchParams(); - const { initComplete } = useWaitForInit(); - const dispatchQueue: PayloadAction[] = []; - if (!initComplete) { - return - } - - try { - URLSearchParams.forEach((value, key) => { - // TODO Needs to be refactored into a single dispatch/reducer pair. - //TODO validation could be implemented across all cases similar to compare period and sorting order - switch (key) { - case 'chartType': - dispatchQueue.push(graphSlice.actions.changeChartToRender(value as ChartTypes)) - break; - case 'unitID': - dispatchQueue.push(graphSlice.actions.updateSelectedUnit(parseInt(value))) - break; - case 'rate': - { - const params = value.split(','); - const rate = { label: params[0], rate: parseFloat(params[1]) } as LineGraphRate; - dispatchQueue.push(graphSlice.actions.updateLineGraphRate(rate)) - } - break; - case 'barDuration': - dispatchQueue.push(graphSlice.actions.updateBarDuration(moment.duration(parseInt(value), 'days'))) - break; - case 'barStacking': - dispatchQueue.push(graphSlice.actions.setBarStacking(Boolean(value))) - break; - case 'areaNormalization': - dispatchQueue.push(graphSlice.actions.setAreaNormalization(value === 'true' ? true : false)) - break; - case 'areaUnit': - dispatchQueue.push(graphSlice.actions.updateSelectedAreaUnit(value as AreaUnitType)) - break; - case 'minMax': - dispatchQueue.push(graphSlice.actions.setShowMinMax(value === 'true' ? true : false)) - break; - case 'comparePeriod': - dispatchQueue.push(graphSlice.actions.updateComparePeriod({ comparePeriod: validateComparePeriod(value), currentTime: moment() })) - break; - case 'compareSortingOrder': - dispatchQueue.push(graphSlice.actions.changeCompareSortingOrder(validateSortingOrder(value))) - break; - case 'optionsVisibility': - dispatchQueue.push(graphSlice.actions.setOptionsVisibility(value === 'true' ? true : false)) - break; - case 'mapID': - // dispatchQueue.push(graphSlice.actions.map) - console.log('Todo, FIXME! Maplink not working') - break; - case 'serverRange': - dispatchQueue.push(graphSlice.actions.updateTimeInterval(TimeInterval.fromString(value))); - /** - * commented out since days from present feature is not currently used - */ - // const index = info.indexOf('dfp'); - // if (index === -1) { - // options.serverRange = TimeInterval.fromString(info); - // } else { - // const message = info.substring(0, index); - // const stringField = this.getNewIntervalFromMessage(message); - // options.serverRange = TimeInterval.fromString(stringField); - // } - break; - case 'sliderRange': - dispatchQueue.push(graphSlice.actions.changeSliderRange(TimeInterval.fromString(value))); - break; - case 'meterOrGroupID': - dispatchQueue.push(graphSlice.actions.updateThreeDMeterOrGroupID(parseInt(value))); - break; - case 'meterOrGroup': - dispatchQueue.push(graphSlice.actions.updateThreeDMeterOrGroup(value as MeterOrGroup)); - break; - case 'readingInterval': - dispatchQueue.push(graphSlice.actions.updateThreeDReadingInterval(parseInt(value))); - break; - case 'meterIDs': - dispatchQueue.push(graphSlice.actions.updateSelectedMeters(value.split(',').map(s => parseInt(s)))) - break; - case 'groupIDs': - dispatchQueue.push(graphSlice.actions.updateSelectedGroups(value.split(',').map(s => parseInt(s)))) - break; - default: - throw new Error('Unknown query parameter'); - } - }) - } catch (err) { - showErrorNotification(translate('failed.to.link.graph')); - } - dispatchQueue.forEach(dispatch) - // All appropriate state updates should've been executed - // redirect to clear the link - - return - -} - - -/// Router -const router = createBrowserRouter([ - { - path: '/', element: , - children: [ - { index: true, element: }, - { path: 'login', element: }, - { path: 'groups', element: }, - { path: 'meters', element: }, - { path: 'graph', element: }, - { - element: , - children: [ - { path: 'admin', element: }, - { path: 'calibration', element: }, - { path: 'maps', element: }, - { path: 'users/new', element: }, - { path: 'units', element: }, - { path: 'conversions', element: }, - { path: 'users', element: } - ] - }, - { - element: , - children: [ - { path: 'csv', element: } - ] - }, - { path: '*', element: } - ] - } -]) - -/** - * @returns the router component Currently under migration! - */ -export default function RouteComponentWIP() { - const lang = useAppSelector(state => state.options.selectedLanguage) - const messages = (LocaleTranslationData as any)[lang]; - return ( - - - - ); -} \ No newline at end of file diff --git a/src/client/app/components/router/AdminOutlet.tsx b/src/client/app/components/router/AdminOutlet.tsx new file mode 100644 index 000000000..7f77bbcd0 --- /dev/null +++ b/src/client/app/components/router/AdminOutlet.tsx @@ -0,0 +1,22 @@ +/* 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 { Navigate, Outlet } from 'react-router-dom'; +import { useWaitForInit } from '../../redux/componentHooks'; +import InitializingComponent from './InitializingComponent'; + +/** + * @returns An outlet that is responsible for Admin Routes. Routes non-admin users away from certain routes. + */ +export default function AdminOutlet() { + const { isAdmin, initComplete } = useWaitForInit(); + + if (!initComplete) { + // Return a spinner until all init queries return and populate cache with data + return + } + // if user is an admin return requested route, otherwise redirect to root + return isAdmin ? : +} diff --git a/src/client/app/components/router/ErrorComponent.tsx b/src/client/app/components/router/ErrorComponent.tsx new file mode 100644 index 000000000..fb4c52705 --- /dev/null +++ b/src/client/app/components/router/ErrorComponent.tsx @@ -0,0 +1,31 @@ +/* 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 { useNavigate } from 'react-router-dom'; +import { Button } from 'reactstrap'; + +/** + * @returns A simple loading spinner used to indicate that the startup init sequence is in progress + */ +export default function ErrorComponent() { + const nav = useNavigate(); + return ( +
+ {/* TODO make a good looking error page. This is a placeholder for now. */} +

+ Oops! An error has occurred. +

+ +
+ + ) +} diff --git a/src/client/app/components/router/GraphLinkComponent.tsx b/src/client/app/components/router/GraphLinkComponent.tsx new file mode 100644 index 000000000..1cd43670c --- /dev/null +++ b/src/client/app/components/router/GraphLinkComponent.tsx @@ -0,0 +1,112 @@ +/* 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 { PayloadAction } from '@reduxjs/toolkit'; +import InitializingComponent from 'components/InitializingComponent'; +import moment from 'moment'; +import * as React from 'react'; +import { Navigate, useSearchParams } from 'react-router-dom'; +import { graphSlice } from 'reducers/graph'; +import { useWaitForInit } from 'redux/componentHooks'; +import { useAppDispatch } from 'redux/hooks'; +import { validateComparePeriod, validateSortingOrder } from 'utils/calculateCompare'; +import { AreaUnitType } from 'utils/getAreaUnitConversion'; +import { showErrorNotification } from 'utils/notifications'; +import translate from 'utils/translate'; +import { TimeInterval } from '../../../../common/TimeInterval'; +import { ChartTypes, LineGraphRate, MeterOrGroup } from '../../types/redux/graph'; +import { changeSelectedMap } from 'actions/map'; + +export const GraphLink = () => { + const dispatch = useAppDispatch(); + const [URLSearchParams] = useSearchParams(); + const { initComplete } = useWaitForInit(); + const dispatchQueue: PayloadAction[] = []; + + if (!initComplete) { + return + } + + try { + URLSearchParams.forEach((value, key) => { + // TODO Needs to be refactored into a single dispatch/reducer pair. + // It is a best practice to reduce the number of dispatch calls, so this logic should be converted into a single reducer for the graphSlice + // TODO validation could be implemented across all cases similar to compare period and sorting order + switch (key) { + case 'chartType': + dispatchQueue.push(graphSlice.actions.changeChartToRender(value as ChartTypes)) + break; + case 'unitID': + dispatchQueue.push(graphSlice.actions.updateSelectedUnit(parseInt(value))) + break; + case 'rate': + { + const params = value.split(','); + const rate = { label: params[0], rate: parseFloat(params[1]) } as LineGraphRate; + dispatchQueue.push(graphSlice.actions.updateLineGraphRate(rate)) + } + break; + case 'barDuration': + dispatchQueue.push(graphSlice.actions.updateBarDuration(moment.duration(parseInt(value), 'days'))) + break; + case 'barStacking': + dispatchQueue.push(graphSlice.actions.setBarStacking(Boolean(value))) + break; + case 'areaNormalization': + dispatchQueue.push(graphSlice.actions.setAreaNormalization(value === 'true' ? true : false)) + break; + case 'areaUnit': + dispatchQueue.push(graphSlice.actions.updateSelectedAreaUnit(value as AreaUnitType)) + break; + case 'minMax': + dispatchQueue.push(graphSlice.actions.setShowMinMax(value === 'true' ? true : false)) + break; + case 'comparePeriod': + dispatchQueue.push(graphSlice.actions.updateComparePeriod({ comparePeriod: validateComparePeriod(value), currentTime: moment() })) + break; + case 'compareSortingOrder': + dispatchQueue.push(graphSlice.actions.changeCompareSortingOrder(validateSortingOrder(value))) + break; + case 'optionsVisibility': + dispatchQueue.push(graphSlice.actions.setOptionsVisibility(value === 'true' ? true : false)) + break; + case 'mapID': + // 'TODO, Verify Behavior & FIXME! MapLink not working as expected + dispatch(changeSelectedMap(parseInt(value))) + break; + case 'serverRange': + dispatchQueue.push(graphSlice.actions.updateTimeInterval(TimeInterval.fromString(value))); + break; + case 'sliderRange': + dispatchQueue.push(graphSlice.actions.changeSliderRange(TimeInterval.fromString(value))); + break; + case 'meterOrGroupID': + dispatchQueue.push(graphSlice.actions.updateThreeDMeterOrGroupID(parseInt(value))); + break; + case 'meterOrGroup': + dispatchQueue.push(graphSlice.actions.updateThreeDMeterOrGroup(value as MeterOrGroup)); + break; + case 'readingInterval': + dispatchQueue.push(graphSlice.actions.updateThreeDReadingInterval(parseInt(value))); + break; + case 'meterIDs': + dispatchQueue.push(graphSlice.actions.updateSelectedMeters(value.split(',').map(s => parseInt(s)))) + break; + case 'groupIDs': + dispatchQueue.push(graphSlice.actions.updateSelectedGroups(value.split(',').map(s => parseInt(s)))) + break; + default: + throw new Error('Unknown query parameter'); + } + }) + + dispatchQueue.forEach(dispatch) + } catch (err) { + showErrorNotification(translate('failed.to.link.graph')); + } + // All appropriate state updates should've been executed + // redirect to root clear the link in the search bar + return + +} \ No newline at end of file diff --git a/src/client/app/components/router/InitializingComponent.tsx b/src/client/app/components/router/InitializingComponent.tsx new file mode 100644 index 000000000..54493cdd3 --- /dev/null +++ b/src/client/app/components/router/InitializingComponent.tsx @@ -0,0 +1,23 @@ +/* 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 SpinnerComponent from '../SpinnerComponent'; + +/** + * @returns A simple loading spinner used to indicate that the startup init sequence is in progress + */ +export default function InitializingComponent() { + return ( +
+

Initializing

+ +
+ + ) +} diff --git a/src/client/app/components/router/NotFoundOutlet.tsx b/src/client/app/components/router/NotFoundOutlet.tsx new file mode 100644 index 000000000..2f918f9d2 --- /dev/null +++ b/src/client/app/components/router/NotFoundOutlet.tsx @@ -0,0 +1,14 @@ +/* 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 { Navigate } from 'react-router-dom'; + +/** + * @returns A component that redirect to the root directory when an unknown route is requested. + */ +export default function NotFound() { + // redirect to home page if non-existent route is requested. + return +} \ No newline at end of file diff --git a/src/client/app/components/router/RoleOutlet.tsx b/src/client/app/components/router/RoleOutlet.tsx new file mode 100644 index 000000000..6bedac8cb --- /dev/null +++ b/src/client/app/components/router/RoleOutlet.tsx @@ -0,0 +1,32 @@ +/* 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 { Navigate, Outlet } from 'react-router-dom'; +import { useWaitForInit } from '../../redux/componentHooks'; +import InitializingComponent from './InitializingComponent'; +import { UserRole } from 'types/items'; + +interface RoleOutletProps { + role: UserRole +} +/** + * @param props role to check for before routing user. + * @returns An outlet that is responsible for Role Routes. Routes users away from certain routes if they don't have permissions. + */ +export default function RoleOutlet(props: RoleOutletProps) { + + // Function that returns a JSX element. Either the requested route's Component, as outlet or back to root + const { userRole, initComplete } = useWaitForInit(); + // // If state contains token it has been validated on startup or login. + if (!initComplete) { + return + } + + if (userRole === props.role || userRole === UserRole.ADMIN) { + return + } + + return +} \ No newline at end of file diff --git a/src/client/app/index.tsx b/src/client/app/index.tsx index 87d079b69..a800cee28 100644 --- a/src/client/app/index.tsx +++ b/src/client/app/index.tsx @@ -7,7 +7,7 @@ import * as React from 'react'; import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux'; import { store } from './store'; -import RouteComponentWIP from './components/RouteComponentWIP'; +import RouteComponent from './components/RouteComponent'; import { initApp } from './reducers/appStateSlice'; import './styles/index.css'; @@ -20,6 +20,6 @@ const root = createRoot(container); root.render( // Provides the Redux store to all child components < Provider store={store} stabilityCheck='always' > - < RouteComponentWIP /> + < RouteComponent /> ); \ No newline at end of file From 813d30d7ca69791254a5b7b4d136337631db4f54 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Sat, 13 Jan 2024 04:16:47 +0000 Subject: [PATCH 050/131] Simplify History --- src/client/app/actions/admin.ts | 42 +++--- .../app/components/DashboardComponent.tsx | 3 +- .../app/components/DateRangeComponent.tsx | 8 +- .../app/components/HeaderButtonsComponent.tsx | 2 +- src/client/app/components/HeaderComponent.tsx | 2 +- .../app/components/HistoryComponent.tsx | 6 +- .../components/router/GraphLinkComponent.tsx | 21 +-- .../app/components/router/RoleOutlet.tsx | 2 +- src/client/app/reducers/appStateSlice.ts | 21 ++- src/client/app/reducers/graph.ts | 141 +++++------------- .../app/redux/middleware/graphHistory.ts | 28 +--- src/client/app/types/redux/graph.ts | 2 - 12 files changed, 101 insertions(+), 177 deletions(-) diff --git a/src/client/app/actions/admin.ts b/src/client/app/actions/admin.ts index ea3e71a0d..6d399819f 100644 --- a/src/client/app/actions/admin.ts +++ b/src/client/app/actions/admin.ts @@ -21,26 +21,28 @@ function fetchPreferences(): Thunk { const preferences = await preferencesApi.getPreferences(); dispatch(adminSlice.actions.receivePreferences(preferences)); moment.locale(getState().admin.defaultLanguage); - if (!getState().graph.hotlinked) { - dispatch((dispatch2: Dispatch) => { - const state = getState(); - dispatch2(graphSlice.actions.changeChartToRender(state.admin.defaultChartToRender)); - if (preferences.defaultBarStacking !== state.graph.barStacking) { - dispatch2(graphSlice.actions.changeBarStacking()); - } - if (preferences.defaultAreaNormalization !== state.graph.areaNormalization) { - dispatch2(graphSlice.actions.toggleAreaNormalization()); - } - if (preferences.defaultLanguage !== state.options.selectedLanguage) { - // if the site default differs from the selected language, update the selected language and the locale - dispatch2(updateSelectedLanguage(preferences.defaultLanguage)); - moment.locale(preferences.defaultLanguage); - } else { - // else set moment locale to site default - moment.locale(getState().admin.defaultLanguage); - } - }); - } + // TODO reference only DELETE ME + // if (!getState().graph.hotlinked) { + // hotlink removed in rtk migration + dispatch((dispatch2: Dispatch) => { + const state = getState(); + dispatch2(graphSlice.actions.changeChartToRender(state.admin.defaultChartToRender)); + if (preferences.defaultBarStacking !== state.graph.barStacking) { + dispatch2(graphSlice.actions.changeBarStacking()); + } + if (preferences.defaultAreaNormalization !== state.graph.areaNormalization) { + dispatch2(graphSlice.actions.toggleAreaNormalization()); + } + if (preferences.defaultLanguage !== state.options.selectedLanguage) { + // if the site default differs from the selected language, update the selected language and the locale + dispatch2(updateSelectedLanguage(preferences.defaultLanguage)); + moment.locale(preferences.defaultLanguage); + } else { + // else set moment locale to site default + moment.locale(getState().admin.defaultLanguage); + } + }); + // } }; } // TODO: Add warning for invalid data in admin panel src/client/app/components/admin/PreferencesComponent.tsx diff --git a/src/client/app/components/DashboardComponent.tsx b/src/client/app/components/DashboardComponent.tsx index 6dde7f5e0..8de6a1102 100644 --- a/src/client/app/components/DashboardComponent.tsx +++ b/src/client/app/components/DashboardComponent.tsx @@ -12,7 +12,8 @@ import MapChartComponent from './MapChartComponent'; import MultiCompareChartComponentWIP from './MultiCompareChartComponentWIP'; import ThreeDComponent from './ThreeDComponent'; import UIOptionsComponent from './UIOptionsComponent'; -import { selectChartToRender, selectOptionsVisibility } from '../reducers/graph'; +import { selectChartToRender } from '../reducers/graph'; +import { selectOptionsVisibility } from '../reducers/appStateSlice'; import RadarChartComponent from '../containers/RadarChartComponent'; /** diff --git a/src/client/app/components/DateRangeComponent.tsx b/src/client/app/components/DateRangeComponent.tsx index 8f9163693..b749ef7a4 100644 --- a/src/client/app/components/DateRangeComponent.tsx +++ b/src/client/app/components/DateRangeComponent.tsx @@ -27,13 +27,7 @@ export default function DateRangeComponent() { const queryTimeInterval = useAppSelector(selectQueryTimeInterval); const locale = useAppSelector(state => state.options.selectedLanguage); - const handleChange = (value: Value) => { - // Dispatch in all cases except when value have been cleared and time interval already unbounded - // A null value indicates that the picker has been cleared - if (!(!value && !queryTimeInterval.getIsBounded())) { - dispatch(updateTimeInterval(dateRangeToTimeInterval(value))) - } - } + const handleChange = (value: Value) => { dispatch(updateTimeInterval(dateRangeToTimeInterval(value))) } return ( diff --git a/src/client/app/components/HeaderButtonsComponent.tsx b/src/client/app/components/HeaderButtonsComponent.tsx index 162096c47..a0fdf422c 100644 --- a/src/client/app/components/HeaderButtonsComponent.tsx +++ b/src/client/app/components/HeaderButtonsComponent.tsx @@ -8,7 +8,7 @@ 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 { selectOptionsVisibility, toggleOptionsVisibility } from '../reducers/graph'; +import { selectOptionsVisibility, toggleOptionsVisibility } from '../reducers/appStateSlice'; import { unsavedWarningSlice } from '../reducers/unsavedWarning'; import { authApi } from '../redux/api/authApi'; import { selectOEDVersion } from '../redux/api/versionApi'; diff --git a/src/client/app/components/HeaderComponent.tsx b/src/client/app/components/HeaderComponent.tsx index 876e0c2d2..8528a26cb 100644 --- a/src/client/app/components/HeaderComponent.tsx +++ b/src/client/app/components/HeaderComponent.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { Link, useLocation } from 'react-router-dom'; -import { selectOptionsVisibility } from '../reducers/graph'; +import { selectOptionsVisibility } from '../reducers/appStateSlice'; import { useAppSelector } from '../redux/hooks'; import HeaderButtonsComponent from './HeaderButtonsComponent'; import LogoComponent from './LogoComponent'; diff --git a/src/client/app/components/HistoryComponent.tsx b/src/client/app/components/HistoryComponent.tsx index 2ac60bdb6..5cb2d3d09 100644 --- a/src/client/app/components/HistoryComponent.tsx +++ b/src/client/app/components/HistoryComponent.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { selectForwardHistory, selectPrevHistory, - traverseNextHistory, traversePrevHistory + historyStepBack, historyStepForward } from '../reducers/graph'; /** * @returns Renders a history component with previous and next buttons. @@ -16,13 +16,13 @@ export default function HistoryComponent() {
dispatch(traversePrevHistory())} + onClick={() => dispatch(historyStepBack())} > dispatch(traverseNextHistory())} + onClick={() => dispatch(historyStepForward())} > diff --git a/src/client/app/components/router/GraphLinkComponent.tsx b/src/client/app/components/router/GraphLinkComponent.tsx index 1cd43670c..e7e83d9f5 100644 --- a/src/client/app/components/router/GraphLinkComponent.tsx +++ b/src/client/app/components/router/GraphLinkComponent.tsx @@ -3,20 +3,21 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { PayloadAction } from '@reduxjs/toolkit'; -import InitializingComponent from 'components/InitializingComponent'; +import InitializingComponent from '../router/InitializingComponent'; import moment from 'moment'; import * as React from 'react'; import { Navigate, useSearchParams } from 'react-router-dom'; -import { graphSlice } from 'reducers/graph'; -import { useWaitForInit } from 'redux/componentHooks'; -import { useAppDispatch } from 'redux/hooks'; -import { validateComparePeriod, validateSortingOrder } from 'utils/calculateCompare'; -import { AreaUnitType } from 'utils/getAreaUnitConversion'; -import { showErrorNotification } from 'utils/notifications'; -import translate from 'utils/translate'; +import { graphSlice } from '../../reducers/graph'; +import { useWaitForInit } from '../../redux/componentHooks'; +import { useAppDispatch } from '../../redux/hooks'; +import { validateComparePeriod, validateSortingOrder } from '../../utils/calculateCompare'; +import { AreaUnitType } from '../../utils/getAreaUnitConversion'; +import { showErrorNotification } from '../../utils/notifications'; +import translate from '../../utils/translate'; import { TimeInterval } from '../../../../common/TimeInterval'; import { ChartTypes, LineGraphRate, MeterOrGroup } from '../../types/redux/graph'; -import { changeSelectedMap } from 'actions/map'; +import { changeSelectedMap } from '../../actions/map'; +import { appStateSlice } from '../../reducers/appStateSlice'; export const GraphLink = () => { const dispatch = useAppDispatch(); @@ -69,7 +70,7 @@ export const GraphLink = () => { dispatchQueue.push(graphSlice.actions.changeCompareSortingOrder(validateSortingOrder(value))) break; case 'optionsVisibility': - dispatchQueue.push(graphSlice.actions.setOptionsVisibility(value === 'true' ? true : false)) + dispatchQueue.push(appStateSlice.actions.setOptionsVisibility(value === 'true' ? true : false)) break; case 'mapID': // 'TODO, Verify Behavior & FIXME! MapLink not working as expected diff --git a/src/client/app/components/router/RoleOutlet.tsx b/src/client/app/components/router/RoleOutlet.tsx index 6bedac8cb..dd8d83482 100644 --- a/src/client/app/components/router/RoleOutlet.tsx +++ b/src/client/app/components/router/RoleOutlet.tsx @@ -6,7 +6,7 @@ import * as React from 'react'; import { Navigate, Outlet } from 'react-router-dom'; import { useWaitForInit } from '../../redux/componentHooks'; import InitializingComponent from './InitializingComponent'; -import { UserRole } from 'types/items'; +import { UserRole } from '../../types/items'; interface RoleOutletProps { role: UserRole diff --git a/src/client/app/reducers/appStateSlice.ts b/src/client/app/reducers/appStateSlice.ts index 9e74caeb5..3f617aa68 100644 --- a/src/client/app/reducers/appStateSlice.ts +++ b/src/client/app/reducers/appStateSlice.ts @@ -13,10 +13,13 @@ import { currentUserSlice } from './currentUser'; interface appStateSlice { initComplete: boolean; + optionsVisibility: boolean; + } const defaultState: appStateSlice = { - initComplete: false + initComplete: false, + optionsVisibility: true } export const appStateSlice = createThunkSlice({ @@ -28,6 +31,12 @@ export const appStateSlice = createThunkSlice({ setInitComplete: create.reducer((state, action) => { state.initComplete = action.payload }), + toggleOptionsVisibility: create.reducer(state => { + state.optionsVisibility = !state.optionsVisibility + }), + setOptionsVisibility: create.reducer((state, action) => { + state.optionsVisibility = action.payload + }), initApp: create.asyncThunk( // Thunk initiates many data fetching calls on startup before react begins to render async (_: void, { dispatch }) => { @@ -80,15 +89,19 @@ export const appStateSlice = createThunkSlice({ }), selectors: { - selectInitComplete: state => state.initComplete + selectInitComplete: state => state.initComplete, + selectOptionsVisibility: state => state.optionsVisibility } }) export const { + initApp, setInitComplete, - initApp + toggleOptionsVisibility, + setOptionsVisibility } = appStateSlice.actions export const { - selectInitComplete + selectInitComplete, + selectOptionsVisibility } = appStateSlice.selectors diff --git a/src/client/app/reducers/graph.ts b/src/client/app/reducers/graph.ts index 17ce142e1..0fda16e05 100644 --- a/src/client/app/reducers/graph.ts +++ b/src/client/app/reducers/graph.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, SliceCaseReducers, ValidateSliceCaseReducers, createAction, createSlice } from '@reduxjs/toolkit'; +import { PayloadAction, createAction, createSlice } from '@reduxjs/toolkit'; import * as moment from 'moment'; import { ActionMeta } from 'react-select'; import { TimeInterval } from '../../../common/TimeInterval'; @@ -26,8 +26,6 @@ const defaultState: GraphState = { chartToRender: ChartTypes.line, barStacking: false, areaNormalization: false, - hotlinked: false, - optionsVisibility: true, lineGraphRate: { label: 'hour', rate: 1 }, renderOnce: false, showMinMax: false, @@ -81,7 +79,11 @@ export const graphSlice = createSlice({ state.current.barDuration = action.payload }, updateTimeInterval: (state, action: PayloadAction) => { - state.current.queryTimeInterval = action.payload + // 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 + } }, changeSliderRange: (state, action: PayloadAction) => { state.current.rangeSliderInterval = action.payload @@ -114,18 +116,9 @@ export const graphSlice = createSlice({ setBarStacking: (state, action: PayloadAction) => { state.current.barStacking = action.payload }, - setHotlinked: (state, action: PayloadAction) => { - state.current.hotlinked = action.payload - }, changeCompareSortingOrder: (state, action: PayloadAction) => { state.current.compareSortingOrder = action.payload }, - toggleOptionsVisibility: state => { - state.current.optionsVisibility = !state.current.optionsVisibility - }, - setOptionsVisibility: (state, action: PayloadAction) => { - state.current.optionsVisibility = action.payload - }, updateLineGraphRate: (state, action: PayloadAction) => { state.current.lineGraphRate = action.payload }, @@ -150,7 +143,7 @@ export const graphSlice = createSlice({ // Destructure payload const { newMetersOrGroups, meta } = action.payload; const cleared = meta.action === 'clear' - const valueRemoved = (meta.action === 'pop-value' || meta.action === 'remove-value') && meta.removedValue + const valueRemoved = (meta.action === 'pop-value' || meta.action === 'remove-value') && meta.removedValue !== undefined const valueAdded = meta.action === 'select-option' && meta.option let isAMeter = true @@ -225,39 +218,37 @@ export const graphSlice = createSlice({ }, setGraphState: (state, action: PayloadAction) => { state.current = action.payload - }, - updateHistory: (state, action: PayloadAction) => { - state.next = []; - state.prev.push(action.payload) - }, - traversePrevHistory: state => { - const prev = state.prev.pop() - if (prev) { - state.next.push(state.current) - state.current = prev - } - }, - traverseNextHistory: state => { - const next = state.next.pop() - if (next) { - state.prev.push(state.current) - state.current = next - } } + }, extraReducers: builder => { - builder.addMatcher(preferencesApi.endpoints.getPreferences.matchFulfilled, ({ current }, action) => { - if (current.selectedAreaUnit === AreaUnitType.none) { - current.selectedAreaUnit = action.payload.defaultAreaUnit; - } - if (!current.hotlinked) { - current.chartToRender = action.payload.defaultChartToRender - current.barStacking = action.payload.defaultBarStacking - current.areaNormalization = action.payload.defaultAreaNormalization - } - }) + builder + .addCase(updateHistory, (state, action) => { + state.next = []; + state.prev.push(action.payload) + }) + .addCase(historyStepBack, state => { + const prev = state.prev.pop() + if (prev) { + state.next.push(state.current) + state.current = prev + } + }) + .addCase(historyStepForward, state => { + const next = state.next.pop() + if (next) { + state.prev.push(state.current) + state.current = next + } + }) + .addMatcher(preferencesApi.endpoints.getPreferences.matchFulfilled, ({ current }, action) => { + const { defaultAreaUnit, defaultChartToRender, defaultBarStacking, defaultAreaNormalization } = action.payload + current.selectedAreaUnit = defaultAreaUnit + current.chartToRender = defaultChartToRender + current.barStacking = defaultBarStacking + current.areaNormalization = defaultAreaNormalization + }) }, - // New Feature as of 2.0.0 Beta. selectors: { selectGraphState: state => state.current, selectPrevHistory: state => state.prev, @@ -275,7 +266,6 @@ export const graphSlice = createSlice({ selectSelectedGroups: state => state.current.selectedGroups, selectSortingOrder: state => state.current.compareSortingOrder, selectQueryTimeInterval: state => state.current.queryTimeInterval, - selectOptionsVisibility: state => state.current.optionsVisibility, selectThreeDMeterOrGroup: state => state.current.threeD.meterOrGroup, selectCompareTimeInterval: state => state.current.compareTimeInterval, selectGraphAreaNormalization: state => state.current.areaNormalization, @@ -301,7 +291,6 @@ export const { selectForwardHistory, selectSelectedMeters, selectSelectedGroups, - selectOptionsVisibility, selectQueryTimeInterval, selectThreeDMeterOrGroup, selectCompareTimeInterval, @@ -312,10 +301,8 @@ export const { // actionCreators exports export const { - setHotlinked, setShowMinMax, setGraphState, - updateHistory, setBarStacking, toggleShowMinMax, changeBarStacking, @@ -324,19 +311,15 @@ export const { changeSliderRange, updateTimeInterval, updateSelectedUnit, - traverseNextHistory, - traversePrevHistory, changeChartToRender, updateComparePeriod, updateSelectedMeters, updateLineGraphRate, setAreaNormalization, - setOptionsVisibility, updateSelectedGroups, resetRangeSliderStack, updateSelectedAreaUnit, confirmGraphRenderOnce, - toggleOptionsVisibility, toggleAreaNormalization, updateThreeDMeterOrGroup, changeCompareSortingOrder, @@ -346,56 +329,6 @@ export const { updateSelectedMetersOrGroups } = graphSlice.actions -export const historyStepBack = createAction('graph/HistoryStepBack') -export const HistoryStepForward = createAction('graph/HistoryStepForward') - - -const createGenericSlice = < - T, - Reducers extends SliceCaseReducers> ->({ - name = '', - initialState, - reducers -}: { - name: string - initialState: History - reducers: ValidateSliceCaseReducers, Reducers> -}) => { - return createSlice({ - name, - initialState, - reducers: { - updateHistory: (state: History, action: PayloadAction) => { - state.next = []; - state.prev.push(action.payload) - }, - traversePrevHistory: state => { - const prev = state.prev.pop() - if (prev) { - state.next.push(state.current) - state.current = prev - } - }, - traverseNextHistory: state => { - const next = state.next.pop() - if (next) { - state.prev.push(state.current) - state.current = next - } - }, - ...reducers - - } - }) -} - -export const wrappedSlice = createGenericSlice({ - name: 'test', - initialState: initialState, - reducers: { - toggleAreaNormalization: state => { - state.current.areaNormalization = !state.current.areaNormalization - } - } -}) \ No newline at end of file +export const historyStepBack = createAction('graph/historyStepBack') +export const historyStepForward = createAction('graph/historyStepForward') +export const updateHistory = createAction('graph/updateHistory') \ No newline at end of file diff --git a/src/client/app/redux/middleware/graphHistory.ts b/src/client/app/redux/middleware/graphHistory.ts index 7b411ed33..ec08583f4 100644 --- a/src/client/app/redux/middleware/graphHistory.ts +++ b/src/client/app/redux/middleware/graphHistory.ts @@ -1,20 +1,15 @@ // https://redux-toolkit.js.org/api/createListenerMiddleware#typescript-usage import { isAnyOf } from '@reduxjs/toolkit'; -import * as _ from 'lodash'; -import { - graphSlice, setGraphState, - setHotlinked, setOptionsVisibility, toggleOptionsVisibility, - traverseNextHistory, traversePrevHistory, updateHistory -} from '../../reducers/graph'; +import { graphSlice, updateHistory } from '../../reducers/graph'; import { AppStartListening } from './middleware'; export const historyMiddleware = (startListening: AppStartListening) => { startListening({ predicate: (action, currentState, previousState) => { - // deep compare of previous state added mostly due to potential state triggers/ dispatches that may not actually alter state - // For example 'popping' values from react-select w/ backspace when empty - return isHistoryTrigger(action) && !_.isEqual(currentState.graph, previousState.graph) + // compare of previous state added due to potential no-op dispatches + // i.e. 'popping' values from react-select w/ backspace when empty, or clearing already unbounded time interval, etc. + return isHistoryTrigger(action) && currentState.graph !== previousState.graph }, effect: (_action, { dispatch, getOriginalState }) => { const prev = getOriginalState().graph.current @@ -25,17 +20,4 @@ export const historyMiddleware = (startListening: AppStartListening) => { } // listen to all graphSlice actions -const isHistoryTrigger = isAnyOf( - ...Object.values(graphSlice.actions) - .filter(action => !( - // filter out the ones don't directly alter the graph, or ones which can cause infinite recursion - // we use updateHistory here, so listening for updateHistory would cause infinite loops etc. - action === toggleOptionsVisibility || - action === setOptionsVisibility || - action === setHotlinked || - action === setGraphState || - action === updateHistory || - action === traverseNextHistory || - action === traversePrevHistory - )) -) \ No newline at end of file +const isHistoryTrigger = isAnyOf(...Object.values(graphSlice.actions)) \ No newline at end of file diff --git a/src/client/app/types/redux/graph.ts b/src/client/app/types/redux/graph.ts index 50556ee99..e9e526e19 100644 --- a/src/client/app/types/redux/graph.ts +++ b/src/client/app/types/redux/graph.ts @@ -69,8 +69,6 @@ export interface GraphState { compareSortingOrder: SortingOrder; chartToRender: ChartTypes; barStacking: boolean; - hotlinked: boolean; - optionsVisibility: boolean; lineGraphRate: LineGraphRate; renderOnce: boolean; showMinMax: boolean; From 9d1691a76bf9ff29d716ea351c181155c6008a7d Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Sun, 14 Jan 2024 03:56:48 +0000 Subject: [PATCH 051/131] Purge Duped Files --- src/client/app/components/AppLayout.tsx | 9 +- .../app/components/DashboardComponent.tsx | 6 +- .../components/MultiCompareChartComponent.tsx | 139 ++- .../MultiCompareChartComponentWIP.tsx | 189 --- .../app/components/RadarChartComponent.tsx | 338 +++++ src/client/app/components/RouteComponent.tsx | 8 +- .../conversion/ConversionViewComponent.tsx | 22 +- .../conversion/ConversionViewComponentWIP.tsx | 91 -- .../conversion/ConversionsDetailComponent.tsx | 77 +- .../ConversionsDetailComponentWIP.tsx | 94 -- .../CreateConversionModalComponent.tsx | 198 +-- .../CreateConversionModalComponentWIP.tsx | 246 ---- .../EditConversionModalComponent.tsx | 49 +- .../EditConversionModalComponentWIP.tsx | 258 ---- .../groups/CreateGroupModalComponent.tsx | 148 ++- .../groups/CreateGroupModalComponentWIP.tsx | 564 --------- .../groups/EditGroupModalComponent.tsx | 74 +- .../groups/EditGroupModalComponentWIP.tsx | 1087 ----------------- .../components/groups/GroupViewComponent.tsx | 27 +- .../groups/GroupViewComponentWIP.tsx | 88 -- .../groups/GroupsDetailComponent.tsx | 31 +- .../groups/GroupsDetailComponentWIP.tsx | 72 -- .../meters/CreateMeterModalComponent.tsx | 475 +++---- .../meters/CreateMeterModalComponentWIP.tsx | 804 ------------ .../meters/EditMeterModalComponent.tsx | 541 +++----- .../meters/EditMeterModalComponentWIP.tsx | 748 ------------ .../components/meters/MeterViewComponent.tsx | 59 +- .../meters/MeterViewComponentWIP.tsx | 92 -- .../meters/MetersDetailComponent.tsx | 89 +- .../meters/MetersDetailComponentWIP.tsx | 74 -- .../app/components/router/ErrorComponent.tsx | 31 +- src/client/app/translations/data.ts | 4 +- 32 files changed, 1170 insertions(+), 5562 deletions(-) delete mode 100644 src/client/app/components/MultiCompareChartComponentWIP.tsx create mode 100644 src/client/app/components/RadarChartComponent.tsx delete mode 100644 src/client/app/components/conversion/ConversionViewComponentWIP.tsx delete mode 100644 src/client/app/components/conversion/ConversionsDetailComponentWIP.tsx delete mode 100644 src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx delete mode 100644 src/client/app/components/conversion/EditConversionModalComponentWIP.tsx delete mode 100644 src/client/app/components/groups/CreateGroupModalComponentWIP.tsx delete mode 100644 src/client/app/components/groups/EditGroupModalComponentWIP.tsx delete mode 100644 src/client/app/components/groups/GroupViewComponentWIP.tsx delete mode 100644 src/client/app/components/groups/GroupsDetailComponentWIP.tsx delete mode 100644 src/client/app/components/meters/CreateMeterModalComponentWIP.tsx delete mode 100644 src/client/app/components/meters/EditMeterModalComponentWIP.tsx delete mode 100644 src/client/app/components/meters/MeterViewComponentWIP.tsx delete mode 100644 src/client/app/components/meters/MetersDetailComponentWIP.tsx diff --git a/src/client/app/components/AppLayout.tsx b/src/client/app/components/AppLayout.tsx index 4f7a61ad5..9944e1615 100644 --- a/src/client/app/components/AppLayout.tsx +++ b/src/client/app/components/AppLayout.tsx @@ -4,15 +4,20 @@ import { Slide, ToastContainer } from 'react-toastify' import 'react-toastify/dist/ReactToastify.css' import FooterComponent from './FooterComponent' import HeaderComponent from './HeaderComponent' + +interface LayoutProps { + children?: React.ReactNode | undefined +} /** + * @param props Optional Children prop to render instead of using Outlet * @returns The OED Application Layout. The current route as the outlet Wrapped in the header, and footer components */ -export default function AppLayout() { +export default function AppLayout(props: LayoutProps) { return ( <> - + {props.children ?? } ) diff --git a/src/client/app/components/DashboardComponent.tsx b/src/client/app/components/DashboardComponent.tsx index 8de6a1102..977be964d 100644 --- a/src/client/app/components/DashboardComponent.tsx +++ b/src/client/app/components/DashboardComponent.tsx @@ -9,12 +9,12 @@ import BarChartComponent from './BarChartComponent'; import HistoryComponent from './HistoryComponent'; import LineChartComponent from './LineChartComponent'; import MapChartComponent from './MapChartComponent'; -import MultiCompareChartComponentWIP from './MultiCompareChartComponentWIP'; +import MultiCompareChartComponent from './MultiCompareChartComponent'; import ThreeDComponent from './ThreeDComponent'; import UIOptionsComponent from './UIOptionsComponent'; import { selectChartToRender } from '../reducers/graph'; import { selectOptionsVisibility } from '../reducers/appStateSlice'; -import RadarChartComponent from '../containers/RadarChartComponent'; +import RadarChartComponent from './RadarChartComponent'; /** * React component that controls the dashboard @@ -37,7 +37,7 @@ export default function DashboardComponent() { {chartToRender === ChartTypes.line && } {chartToRender === ChartTypes.bar && } - {chartToRender === ChartTypes.compare && } + {chartToRender === ChartTypes.compare && } {chartToRender === ChartTypes.map && } {chartToRender === ChartTypes.threeD && } {chartToRender === ChartTypes.radar && } diff --git a/src/client/app/components/MultiCompareChartComponent.tsx b/src/client/app/components/MultiCompareChartComponent.tsx index 2562a0453..a2397ebf1 100644 --- a/src/client/app/components/MultiCompareChartComponent.tsx +++ b/src/client/app/components/MultiCompareChartComponent.tsx @@ -3,24 +3,87 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { UncontrolledAlert } from 'reactstrap'; import { FormattedMessage } from 'react-intl'; +import { UncontrolledAlert } from 'reactstrap'; import CompareChartContainer, { CompareEntity } from '../containers/CompareChartContainer'; +import { selectGraphAreaNormalization, selectSelectedGroups, selectSelectedMeters, selectSortingOrder } from '../reducers/graph'; +import { selectGroupDataById } from '../redux/api/groupsApi'; +import { selectMeterDataById } from '../redux/api/metersApi'; +import { readingsApi } from '../redux/api/readingsApi'; +import { useAppSelector } from '../redux/hooks'; +import { selectCompareChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; +import { SortingOrder } from '../utils/calculateCompare'; +import { AreaUnitType } from '../utils/getAreaUnitConversion'; -interface MultiCompareChartProps { +export interface MultiCompareChartProps { selectedCompareEntities: CompareEntity[]; errorEntities: string[]; } /** * Component that defines compare chart - * @param props defined above * @returns Multi Compare Chart element */ -export default function MultiCompareChartComponent(props: MultiCompareChartProps) { +export default function MultiCompareChartComponent() { + const { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } = useAppSelector(selectCompareChartQueryArgs) + const { data: meterReadings = {} } = readingsApi.useCompareQuery(meterArgs, { skip: meterShouldSkip }) + const { data: groupReadings = {} } = readingsApi.useCompareQuery(groupArgs, { skip: groupShouldSkip }) + + const areaNormalization = useAppSelector(selectGraphAreaNormalization) + const sortingOrder = useAppSelector(selectSortingOrder) + const selectedMeters = useAppSelector(selectSelectedMeters) + const selectedGroups = useAppSelector(selectSelectedGroups) + + const meterDataByID = useAppSelector(selectMeterDataById) + const groupDataById = useAppSelector(selectGroupDataById) + + // TODO SEEMS UNUSED, kept due to uncertainty when migrating to RTK VERIFY BEHAVIOR + const errorEntities: string[] = []; + let selectedCompareEntities: CompareEntity[] = [] + + Object.entries(meterReadings).forEach(([key, value]) => { + const name = meterDataByID[Number(key)].name + const identifier = meterDataByID[Number(key)].identifier + + const areaNormValid = (!areaNormalization || (meterDataByID[Number(key)].area > 0 && meterDataByID[Number(key)].areaUnit !== AreaUnitType.none)) + if (areaNormValid && selectedMeters.includes(Number(key))) { + const change = calculateChange(value.curr_use, value.prev_use); + const entity: CompareEntity = { + id: Number(key), + isGroup: false, + name, + identifier, + change, + currUsage: value.curr_use, + prevUsage: value.prev_use + }; + selectedCompareEntities.push(entity); + } + }) + Object.entries(groupReadings).forEach(([key, value]) => { + const identifier = groupDataById[Number(key)].name + const areaNormValid = (!areaNormalization || (groupDataById[Number(key)].area > 0 && groupDataById[Number(key)].areaUnit !== AreaUnitType.none)) + if (areaNormValid && selectedGroups.includes(Number(key))) { + const change = calculateChange(value.curr_use, value.prev_use); + const entity: CompareEntity = { + id: Number(key), + isGroup: false, + name: identifier, + identifier, + change, + currUsage: value.curr_use, + prevUsage: value.prev_use + }; + selectedCompareEntities.push(entity); + } + }) + + selectedCompareEntities = sortIDs(selectedCompareEntities, sortingOrder) + + // Compute how much space should be used in the bootstrap grid system let size = 3; - const numSelectedItems = props.selectedCompareEntities.length; + const numSelectedItems = selectedCompareEntities.length; if (numSelectedItems < 3) { size = numSelectedItems; } @@ -32,7 +95,7 @@ export default function MultiCompareChartComponent(props: MultiCompareChartProps return (
- {props.errorEntities.map(name => + {errorEntities.map(name =>
{name} @@ -41,7 +104,7 @@ export default function MultiCompareChartComponent(props: MultiCompareChartProps )}
- {props.selectedCompareEntities.map(compareEntity => + {selectedCompareEntities.map(compareEntity =>
{/* TODO These types of plotly containers expect a lot of passed values and it gives a TS error. Given we plan to replace this @@ -56,7 +119,7 @@ export default function MultiCompareChartComponent(props: MultiCompareChartProps
)}
- {props.selectedCompareEntities.length === 0 && + {selectedCompareEntities.length === 0 &&
@@ -64,3 +127,63 @@ export default function MultiCompareChartComponent(props: MultiCompareChartProps
); } + +/** + * + * @param currentPeriodUsage TODO temp to appease linter fix Later + * @param usedToThisPointLastTimePeriod TODO temp to appease linter fix Later + * @returns TODO temp to appease linter fix Later + */ +function calculateChange(currentPeriodUsage: number, usedToThisPointLastTimePeriod: number): number { + return -1 + (currentPeriodUsage / usedToThisPointLastTimePeriod); +} + + + +/** + * @param ids TODO temp to appease linter fix Later + * @param sortingOrder TODO temp to appease linter fix Later + * @returns TODO temp to appease linter fix Later + */ +function sortIDs(ids: CompareEntity[], sortingOrder: SortingOrder): CompareEntity[] { + switch (sortingOrder) { + case SortingOrder.Alphabetical: + ids.sort((a, b) => { + const identifierA = a.identifier.toLowerCase(); + const identifierB = b.identifier.toLowerCase(); + if (identifierA < identifierB) { + return -1; + } + if (identifierA > identifierB) { + return 1; + } + return 0; + }); + break; + case SortingOrder.Ascending: + ids.sort((a, b) => { + if (a.change < b.change) { + return -1; + } + if (a.change > b.change) { + return 1; + } + return 0; + }); + break; + case SortingOrder.Descending: + ids.sort((a, b) => { + if (a.change > b.change) { + return -1; + } + if (a.change < b.change) { + return 1; + } + return 0; + }); + break; + default: + throw new Error(`Unknown sorting order: ${sortingOrder}`); + } + return ids; +} diff --git a/src/client/app/components/MultiCompareChartComponentWIP.tsx b/src/client/app/components/MultiCompareChartComponentWIP.tsx deleted file mode 100644 index 8fa7e686c..000000000 --- a/src/client/app/components/MultiCompareChartComponentWIP.tsx +++ /dev/null @@ -1,189 +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 * as React from 'react'; -import { FormattedMessage } from 'react-intl'; -import { UncontrolledAlert } from 'reactstrap'; -import CompareChartContainer, { CompareEntity } from '../containers/CompareChartContainer'; -import { selectGraphAreaNormalization, selectSelectedGroups, selectSelectedMeters, selectSortingOrder } from '../reducers/graph'; -import { selectGroupDataById } from '../redux/api/groupsApi'; -import { selectMeterDataById } from '../redux/api/metersApi'; -import { readingsApi } from '../redux/api/readingsApi'; -import { useAppSelector } from '../redux/hooks'; -import { selectCompareChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; -import { SortingOrder } from '../utils/calculateCompare'; -import { AreaUnitType } from '../utils/getAreaUnitConversion'; - -export interface MultiCompareChartProps { - selectedCompareEntities: CompareEntity[]; - errorEntities: string[]; -} - -/** - * Component that defines compare chart - * @returns Multi Compare Chart element - */ -export default function MultiCompareChartComponentWIP() { - const { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } = useAppSelector(selectCompareChartQueryArgs) - const { data: meterReadings = {} } = readingsApi.useCompareQuery(meterArgs, { skip: meterShouldSkip }) - const { data: groupReadings = {} } = readingsApi.useCompareQuery(groupArgs, { skip: groupShouldSkip }) - - const areaNormalization = useAppSelector(selectGraphAreaNormalization) - const sortingOrder = useAppSelector(selectSortingOrder) - const selectedMeters = useAppSelector(selectSelectedMeters) - const selectedGroups = useAppSelector(selectSelectedGroups) - - const meterDataByID = useAppSelector(selectMeterDataById) - const groupDataById = useAppSelector(selectGroupDataById) - - // TODO SEEMS UNUSED, kept due to uncertainty when migrating to RTK VERIFY BEHAVIOR - const errorEntities: string[] = []; - let selectedCompareEntities: CompareEntity[] = [] - - Object.entries(meterReadings).forEach(([key, value]) => { - const name = meterDataByID[Number(key)].name - const identifier = meterDataByID[Number(key)].identifier - - const areaNormValid = (!areaNormalization || (meterDataByID[Number(key)].area > 0 && meterDataByID[Number(key)].areaUnit !== AreaUnitType.none)) - if (areaNormValid && selectedMeters.includes(Number(key))) { - const change = calculateChange(value.curr_use, value.prev_use); - const entity: CompareEntity = { - id: Number(key), - isGroup: false, - name, - identifier, - change, - currUsage: value.curr_use, - prevUsage: value.prev_use - }; - selectedCompareEntities.push(entity); - } - }) - Object.entries(groupReadings).forEach(([key, value]) => { - const identifier = groupDataById[Number(key)].name - const areaNormValid = (!areaNormalization || (groupDataById[Number(key)].area > 0 && groupDataById[Number(key)].areaUnit !== AreaUnitType.none)) - if (areaNormValid && selectedGroups.includes(Number(key))) { - const change = calculateChange(value.curr_use, value.prev_use); - const entity: CompareEntity = { - id: Number(key), - isGroup: false, - name: identifier, - identifier, - change, - currUsage: value.curr_use, - prevUsage: value.prev_use - }; - selectedCompareEntities.push(entity); - } - }) - - selectedCompareEntities = sortIDs(selectedCompareEntities, sortingOrder) - - - // Compute how much space should be used in the bootstrap grid system - let size = 3; - const numSelectedItems = selectedCompareEntities.length; - if (numSelectedItems < 3) { - size = numSelectedItems; - } - const childClassName = `col-12 col-lg-${12 / size}`; - const centeredStyle = { - marginTop: '20%' - }; - - return ( -
-
- {errorEntities.map(name => -
- - {name} - -
- )} -
-
- {selectedCompareEntities.map(compareEntity => -
- {/* 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 */} - -
- )} -
- {selectedCompareEntities.length === 0 && -
- -
- } -
- ); -} - -/** - * - * @param currentPeriodUsage TODO temp to appease linter fix Later - * @param usedToThisPointLastTimePeriod TODO temp to appease linter fix Later - * @returns TODO temp to appease linter fix Later - */ -function calculateChange(currentPeriodUsage: number, usedToThisPointLastTimePeriod: number): number { - return -1 + (currentPeriodUsage / usedToThisPointLastTimePeriod); -} - - - -/** - * @param ids TODO temp to appease linter fix Later - * @param sortingOrder TODO temp to appease linter fix Later - * @returns TODO temp to appease linter fix Later - */ -function sortIDs(ids: CompareEntity[], sortingOrder: SortingOrder): CompareEntity[] { - switch (sortingOrder) { - case SortingOrder.Alphabetical: - ids.sort((a, b) => { - const identifierA = a.identifier.toLowerCase(); - const identifierB = b.identifier.toLowerCase(); - if (identifierA < identifierB) { - return -1; - } - if (identifierA > identifierB) { - return 1; - } - return 0; - }); - break; - case SortingOrder.Ascending: - ids.sort((a, b) => { - if (a.change < b.change) { - return -1; - } - if (a.change > b.change) { - return 1; - } - return 0; - }); - break; - case SortingOrder.Descending: - ids.sort((a, b) => { - if (a.change > b.change) { - return -1; - } - if (a.change < b.change) { - return 1; - } - return 0; - }); - break; - default: - throw new Error(`Unknown sorting order: ${sortingOrder}`); - } - return ids; -} diff --git a/src/client/app/components/RadarChartComponent.tsx b/src/client/app/components/RadarChartComponent.tsx new file mode 100644 index 000000000..3f93cfb9d --- /dev/null +++ b/src/client/app/components/RadarChartComponent.tsx @@ -0,0 +1,338 @@ +/* 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 _ from 'lodash'; +import * as moment from 'moment'; +import * as React from 'react' +import getGraphColor from '../utils/getGraphColor'; +import translate from '../utils/translate'; +import Plot from 'react-plotly.js'; +import { Layout } from 'plotly.js'; +import Locales from '../types/locales'; +import { DataType } from '../types/Datasources'; +import { lineUnitLabel } from '../utils/graphics'; +import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; +import { useAppSelector } from '../redux/hooks'; +import { + selectAreaUnit, selectGraphAreaNormalization, selectLineGraphRate, + selectSelectedGroups, selectSelectedMeters, selectSelectedUnit +} from '../reducers/graph'; +import { selectUnitDataById } from '../redux/api/unitsApi'; +import { selectMeterDataById } from '../redux/api/metersApi'; +import { selectRadarChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; +import { readingsApi } from '../redux/api/readingsApi'; +import { selectGroupDataById } from '../redux/api/groupsApi'; +import LogoSpinner from './LogoSpinner'; + +/** + * @returns radar plotly component + */ +export default function RadarChartComponent() { + const { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } = useAppSelector(selectRadarChartQueryArgs) + const { data: meterReadings, isLoading: meterIsLoading } = readingsApi.useLineQuery(meterArgs, { skip: meterShouldSkip }); + const { data: groupData, isLoading: groupIsLoading } = readingsApi.useLineQuery(groupArgs, { skip: groupShouldSkip }); + const datasets: any[] = []; + // Time range selected + // graphic unit selected + const graphingUnit = useAppSelector(selectSelectedUnit); + // The current selected rate + const currentSelectedRate = useAppSelector(selectLineGraphRate); + const unitDataById = useAppSelector(selectUnitDataById); + + const areaNormalization = useAppSelector(selectGraphAreaNormalization); + const selectedAreaUnit = useAppSelector(selectAreaUnit); + const selectedMeters = useAppSelector(selectSelectedMeters); + const selectedGroups = useAppSelector(selectSelectedGroups); + const meterDataById = useAppSelector(selectMeterDataById); + const groupDataById = useAppSelector(selectGroupDataById); + + if (meterIsLoading || groupIsLoading) { + return + // return + } + + let unitLabel = ''; + let needsRateScaling = false; + // 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 = unitDataById[graphingUnit]; + if (selectUnitState !== undefined) { + // Determine the r-axis label and if the rate needs to be scaled. + const returned = lineUnitLabel(selectUnitState, currentSelectedRate, areaNormalization, selectedAreaUnit); + unitLabel = returned.unitLabel + needsRateScaling = returned.needsRateScaling; + } + } + // The rate will be 1 if it is per hour (since state readings are per hour) or no rate scaling so no change. + const rateScaling = needsRateScaling ? currentSelectedRate.rate : 1; + + // Add all valid data from existing meters to the radar plot + for (const meterID of selectedMeters) { + if (meterReadings) { + const 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)) { + // Convert the meter area into the proper unit if normalizing by area or use 1 if not so won't change reading values. + const areaScaling = areaNormalization ? + meterArea * getAreaUnitConversion(meterDataById[meterID].areaUnit, selectedAreaUnit) : 1; + // Divide areaScaling into the rate so have complete scaling factor for readings. + const scaling = rateScaling / areaScaling; + const readingsData = meterReadings[meterID] + if (readingsData) { + const label = meterDataById[meterID].identifier; + const colorID = meterID; + // if (readingsData.readings === undefined) { + // throw new Error('Unacceptable condition: readingsData.readings is undefined.'); + // } + // Create two arrays for the distance (rData) and angle (thetaData) values. Fill the array with the data from the line readings. + // HoverText is the popup value show for each reading. + const thetaData: string[] = []; + const rData: number[] = []; + const hoverText: string[] = []; + const readings = _.values(readingsData); + readings.forEach(reading => { + // As usual, we want to interpret the readings in UTC. We lose the timezone as these start/endTimestamp + // are equivalent to Unix timestamp in milliseconds. + const st = moment.utc(reading.startTimestamp); + // Time reading is in the middle of the start and end timestamp + const timeReading = st.add(moment.utc(reading.endTimestamp).diff(st) / 2); + // The angular value is the date, internationalized. + thetaData.push(timeReading.format('ddd, ll LTS')); + // The scaling is the factor to change the reading by. + const readingValue = reading.reading * scaling; + rData.push(readingValue); + hoverText.push(` ${timeReading.format('ddd, ll LTS')}
${label}: ${readingValue.toPrecision(6)} ${unitLabel}`); + }); + + // This variable contains all the elements (plot values, line type, etc.) assigned to the data parameter of the Plotly object + datasets.push({ + name: label, + theta: thetaData, + r: rData, + text: hoverText, + hoverinfo: 'text', + type: 'scatterpolar', + mode: 'lines', + line: { + shape: 'spline', + width: 2, + color: getGraphColor(colorID, DataType.Meter) + } + }); + } + } + } + } + + // Add all valid data from existing groups to the radar plot + for (const groupID of selectedGroups) { + // const byGroupID = state.readings.line.byGroupID[groupID]; + if (groupData) { + const groupArea = groupDataById[groupID].area; + // We either don't care about area, or we do in which case there needs to be a nonzero area. + if (!areaNormalization || (groupArea > 0 && groupDataById[groupID].areaUnit != AreaUnitType.none)) { + // Convert the group area into the proper unit if normalizing by area or use 1 if not so won't change reading values. + const areaScaling = areaNormalization ? + groupArea * getAreaUnitConversion(groupDataById[groupID].areaUnit, selectedAreaUnit) : 1; + // Divide areaScaling into the rate so have complete scaling factor for readings. + const scaling = rateScaling / areaScaling; + const readingsData = groupData[groupID] + if (readingsData) { + const label = groupDataById[groupID].name; + const colorID = groupID; + // if (readingsData.readings === undefined) { + // throw new Error('Unacceptable condition: readingsData.readings is undefined.'); + // } + // Create two arrays for the distance (rData) and angle (thetaData) values. Fill the array with the data from the line readings. + // HoverText is the popup value show for each reading. + const thetaData: string[] = []; + const rData: number[] = []; + const hoverText: string[] = []; + const readings = _.values(readingsData); + readings.forEach(reading => { + // As usual, we want to interpret the readings in UTC. We lose the timezone as these start/endTimestamp + // are equivalent to Unix timestamp in milliseconds. + const st = moment.utc(reading.startTimestamp); + // Time reading is in the middle of the start and end timestamp + const timeReading = st.add(moment.utc(reading.endTimestamp).diff(st) / 2); + // The angular value is the date, internationalized. + thetaData.push(timeReading.format('ddd, ll LTS')); + // The scaling is the factor to change the reading by. + const readingValue = reading.reading * scaling; + rData.push(readingValue); + hoverText.push(` ${timeReading.format('ddd, ll LTS')}
${label}: ${readingValue.toPrecision(6)} ${unitLabel}`); + }); + + // This variable contains all the elements (plot values, line type, etc.) assigned to the data parameter of the Plotly object + datasets.push({ + name: label, + theta: thetaData, + r: rData, + text: hoverText, + hoverinfo: 'text', + type: 'scatterpolar', + mode: 'lines', + line: { + shape: 'spline', + width: 2, + color: getGraphColor(colorID, DataType.Meter) + } + }); + } + } + } + } + + let layout: Partial; + // TODO See 3D code for functions that can be used for layout and notices. + if (datasets.length === 0) { + // There are no meters so tell user. + // Customize the layout of the plot + // See https://community.plotly.com/t/replacing-an-empty-graph-with-a-message/31497 for showing text not plot. + layout = { + 'xaxis': { + 'visible': false + }, + 'yaxis': { + 'visible': false + }, + 'annotations': [ + { + 'text': `${translate('select.meter.group')}`, + 'xref': 'paper', + 'yref': 'paper', + 'showarrow': false, + 'font': { + 'size': 28 + } + } + ] + } + } else { + // Plotly scatterpolar plots have the unfortunate attribute that if a smaller number of plotting + // points is done first then that impacts the labeling of the polar coordinate where you can get + // duplicated labels and the points on the separate lines are separated. It is unclear if this is + // intentional or a bug that will go away. To deal with this, the lines are ordered by size. + // Descending (reverse) sort datasets by size of readings. Use r but theta should be the same. + datasets.sort((a, b) => { + return b.r.length - a.r.length; + }); + if (datasets[0].r.length === 0) { + // The longest line (first one) has no data so there is no data in any of the lines. + // Customize the layout of the plot + // See https://community.plotly.com/t/replacing-an-empty-graph-with-a-message/31497 for showing text not plot. + // There is no data so tell user - likely due to date range outside where readings. + // Remove plotting data even though none there is an empty r & theta that gives empty graphic. + datasets.splice(0, datasets.length); + layout = { + 'xaxis': { + 'visible': false + }, + 'yaxis': { + 'visible': false + }, + 'annotations': [ + { + 'text': `${translate('radar.no.data')}`, + 'xref': 'paper', + 'yref': 'paper', + 'showarrow': false, + 'font': { + 'size': 28 + } + } + ] + } + } else { + // Check if all the values for the dates are compatible. Plotly does not like having different dates in different + // scatterpolar lines. Lots of attempts to get this to work failed so not going to allow since not that common. + // Compare the dates (theta) for line with the max points (index 0) to see if it has all the points in all other lines. + let ok = true; + for (let i = 1; i < datasets.length; i++) { + // Current line to consider. + const currentLine: string[] = datasets[i].theta; + // See if all points in current line are in max length line. && means get false if any false. + ok = ok && currentLine.every(v => datasets[0].theta.includes(v)); + } + if (!ok) { + // Not all points align on all lines so inform user. + // Remove plotting data. + datasets.splice(0, datasets.length); + // The lines are not compatible so tell user. + layout = { + 'xaxis': { + 'visible': false + }, + 'yaxis': { + 'visible': false + }, + 'annotations': [ + { + 'text': `${translate('radar.lines.incompatible')}`, + 'xref': 'paper', + 'yref': 'paper', + 'showarrow': false, + 'font': { + 'size': 28 + } + } + ] + } + } else { + // Data available and okay so plot. + // Maximum number of ticks, represents 12 months. Too many is cluttered so this seems good value. + // Plotly shows less if only a few points. + const maxTicks = 12; + layout = { + autosize: true, + showlegend: true, + height: 800, + legend: { + x: 0, + y: 1.1, + orientation: 'h' + }, + polar: { + radialaxis: { + title: unitLabel, + showgrid: true, + gridcolor: '#ddd' + }, + angularaxis: { + // TODO Attempts to format the dates to remove the time did not work with plotly + // choosing the tick values which is desirable. Also want time if limited time range. + direction: 'clockwise', + showgrid: true, + gridcolor: '#ddd', + nticks: maxTicks + } + }, + margin: { + t: 10, + b: -20 + } + }; + } + } + } + + // props.config.locale = state.options.selectedLanguage; + return ( +
+ console.log(e)} + style={{ width: '100%', height: '80%' }} + useResizeHandler={true} + config={{ + displayModeBar: true, + responsive: true, + locales: Locales // makes locales available for use + }} + layout={layout} + /> +
+ ) +} \ No newline at end of file diff --git a/src/client/app/components/RouteComponent.tsx b/src/client/app/components/RouteComponent.tsx index e6178eaf5..10bc42bc1 100644 --- a/src/client/app/components/RouteComponent.tsx +++ b/src/client/app/components/RouteComponent.tsx @@ -16,9 +16,9 @@ import HomeComponent from './HomeComponent'; import LoginComponent from './LoginComponent'; import AdminComponent from './admin/AdminComponent'; import UsersDetailComponentWIP from './admin/UsersDetailComponentWIP'; -import ConversionsDetailComponentWIP from './conversion/ConversionsDetailComponentWIP'; -import GroupsDetailComponentWIP from './groups/GroupsDetailComponentWIP'; -import MetersDetailComponentWIP from './meters/MetersDetailComponentWIP'; +import ConversionsDetailComponentWIP from './conversion/ConversionsDetailComponent'; +import GroupsDetailComponent from './groups/GroupsDetailComponent'; +import MetersDetailComponentWIP from './meters/MetersDetailComponent'; import AdminOutlet from './router/AdminOutlet'; import { GraphLink } from './router/GraphLinkComponent'; import NotFound from './router/NotFoundOutlet'; @@ -47,7 +47,7 @@ const router = createBrowserRouter([ children: [ { index: true, element: }, { path: 'login', element: }, - { path: 'groups', element: }, + { path: 'groups', element: }, { path: 'meters', element: }, { path: 'graph', element: }, { diff --git a/src/client/app/components/conversion/ConversionViewComponent.tsx b/src/client/app/components/conversion/ConversionViewComponent.tsx index 2e1d51cc8..3f1ba7adb 100644 --- a/src/client/app/components/conversion/ConversionViewComponent.tsx +++ b/src/client/app/components/conversion/ConversionViewComponent.tsx @@ -6,16 +6,16 @@ import * as React from 'react'; // Realize that * is already imported from react import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import EditConversionModalComponent from './EditConversionModalComponent'; -import '../../styles/card-page.css'; +import { Button } from 'reactstrap'; import { ConversionData } from 'types/redux/conversions'; -import { UnitDataById } from 'types/redux/units'; +import '../../styles/card-page.css'; import translate from '../../utils/translate'; -import { Button } from 'reactstrap'; +import EditConversionModalComponentWIP from './EditConversionModalComponent'; +import { useAppSelector } from '../../redux/hooks'; +import { selectUnitDataById } from '../../redux/api/unitsApi'; interface ConversionViewComponentProps { conversion: ConversionData; - units: UnitDataById; } /** @@ -28,6 +28,7 @@ export default function ConversionViewComponent(props: ConversionViewComponentPr // Edit Modal Show const [showEditModal, setShowEditModal] = useState(false); + const unitDataById = useAppSelector(selectUnitDataById) const handleShow = () => { setShowEditModal(true); @@ -36,7 +37,7 @@ export default function ConversionViewComponent(props: ConversionViewComponentPr const handleClose = () => { setShowEditModal(false); } - + React.useEffect(() => undefined, [props.conversion]) // Create header from sourceId, destinationId identifiers // Arrow is bidirectional if conversion is bidirectional and one way if not. let arrowShown: string; @@ -45,7 +46,7 @@ export default function ConversionViewComponent(props: ConversionViewComponentPr } else { arrowShown = ' → '; } - const header = String(props.units[props.conversion.sourceId].identifier + arrowShown + props.units[props.conversion.destinationId].identifier); + const header = String(unitDataById[props.conversion.sourceId].identifier + arrowShown + unitDataById[props.conversion.destinationId].identifier); // Unlike the details component, we don't check if units are loaded since must come through that page. @@ -55,10 +56,10 @@ export default function ConversionViewComponent(props: ConversionViewComponentPr {header}
- {props.units[props.conversion.sourceId].identifier} + {unitDataById[props.conversion.sourceId].identifier}
- {props.units[props.conversion.destinationId].identifier} + {unitDataById[props.conversion.destinationId].identifier}
{translate(`TrueFalseType.${props.conversion.bidirectional.toString()}`)} @@ -78,10 +79,9 @@ export default function ConversionViewComponent(props: ConversionViewComponentPr {/* Creates a child ConversionModalEditComponent */} - diff --git a/src/client/app/components/conversion/ConversionViewComponentWIP.tsx b/src/client/app/components/conversion/ConversionViewComponentWIP.tsx deleted file mode 100644 index 7e7d25331..000000000 --- a/src/client/app/components/conversion/ConversionViewComponentWIP.tsx +++ /dev/null @@ -1,91 +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 * as React from 'react'; -// Realize that * is already imported from react -import { useState } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Button } from 'reactstrap'; -import { ConversionData } from 'types/redux/conversions'; -import '../../styles/card-page.css'; -import translate from '../../utils/translate'; -import EditConversionModalComponentWIP from './EditConversionModalComponentWIP'; -import { useAppSelector } from '../../redux/hooks'; -import { selectUnitDataById } from '../../redux/api/unitsApi'; - -interface ConversionViewComponentProps { - conversion: ConversionData; -} - -/** - * Defines the conversion info card - * @param props defined above - * @returns Single conversion element - */ -export default function ConversionViewComponent(props: ConversionViewComponentProps) { - // Don't check if admin since only an admin is allow to route to this page. - - // Edit Modal Show - const [showEditModal, setShowEditModal] = useState(false); - const unitDataById = useAppSelector(selectUnitDataById) - - const handleShow = () => { - setShowEditModal(true); - } - - const handleClose = () => { - setShowEditModal(false); - } - React.useEffect(() => undefined, [props.conversion]) - // Create header from sourceId, destinationId identifiers - // Arrow is bidirectional if conversion is bidirectional and one way if not. - let arrowShown: string; - if (props.conversion.bidirectional) { - arrowShown = ' ↔ '; - } else { - arrowShown = ' → '; - } - const header = String(unitDataById[props.conversion.sourceId].identifier + arrowShown + unitDataById[props.conversion.destinationId].identifier); - - // Unlike the details component, we don't check if units are loaded since must come through that page. - - return ( -
-
- {header} -
-
- {unitDataById[props.conversion.sourceId].identifier} -
-
- {unitDataById[props.conversion.destinationId].identifier} -
-
- {translate(`TrueFalseType.${props.conversion.bidirectional.toString()}`)} -
-
- {props.conversion.slope} -
-
- {props.conversion.intercept} -
-
- {/* Only show first 30 characters so card does not get too big. Should limit to one line */} - {props.conversion.note.slice(0, 29)} -
-
- - {/* Creates a child ConversionModalEditComponent */} - -
-
- ); -} diff --git a/src/client/app/components/conversion/ConversionsDetailComponent.tsx b/src/client/app/components/conversion/ConversionsDetailComponent.tsx index 3af4800a8..9b92cead9 100644 --- a/src/client/app/components/conversion/ConversionsDetailComponent.tsx +++ b/src/client/app/components/conversion/ConversionsDetailComponent.tsx @@ -3,19 +3,15 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useSelector } from 'react-redux'; -import { ConversionData } from 'types/redux/conversions'; -import { fetchConversionsDetailsIfNeeded } from '../../actions/conversions'; -import SpinnerComponent from '../../components/SpinnerComponent'; -import TooltipHelpComponent from '../../components/TooltipHelpComponent'; -import { selectConversionsDetails } from '../../redux/api/conversionsApi'; -import { useAppDispatch, useAppSelector } from '../../redux/hooks'; -import { State } from '../../types/redux/state'; +import SpinnerComponent from '../SpinnerComponent'; +import TooltipHelpComponent from '../TooltipHelpComponent'; +import { conversionsApi } from '../../redux/api/conversionsApi'; +import { unitsAdapter, unitsApi } from '../../redux/api/unitsApi'; +import { ConversionData } from '../../types/redux/conversions'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import ConversionViewComponent from './ConversionViewComponent'; -import CreateConversionModalComponent from './CreateConversionModalComponent'; +import ConversionViewComponentWIP from './ConversionViewComponent'; +import CreateConversionModalComponentWIP from './CreateConversionModalComponent'; /** * Defines the conversions page card view @@ -24,24 +20,22 @@ import CreateConversionModalComponent from './CreateConversionModalComponent'; export default function ConversionsDetailComponent() { // The route stops you from getting to this page if not an admin. - const dispatch = useAppDispatch(); - - useEffect(() => { - // Makes async call to conversions API for conversions details if one has not already been made somewhere else, stores conversion by ids in state - dispatch(fetchConversionsDetailsIfNeeded()); - }, [dispatch]); - // Conversions state - const conversionsState = useAppSelector(selectConversionsDetails); + const { data: conversionsState = [], isFetching: conversionsFetching } = conversionsApi.useGetConversionsDetailsQuery(); + // Units DataById + const { unitDataById = {}, isFetching: unitsFetching } = unitsApi.useGetUnitsDetailsQuery(undefined, { + selectFromResult: ({ data, ...result }) => ({ + ...result, + unitDataById: data && unitsAdapter.getSelectors().selectEntities(data) + }) + }) + // const x = useAppSelector(state => conversionsApi.endpoints.refresh.select()(state)) + // unnecessary? Currently this occurs as a side effect of the mutation which will invalidate meters/group + // unused for now, until decided + // const isUpdatingCikAndDBViews = useAppSelector(state => state.admin.isUpdatingCikAndDBViews); - const isUpdatingCikAndDBViews = useSelector((state: State) => state.admin.isUpdatingCikAndDBViews); - - // Units state - const unitsState = useSelector((state: State) => state.units.units); - const unitsFetchedOnce = useSelector((state: State) => state.units.hasBeenFetchedOnce); // Check if the units state is fully loaded - const unitsStateLoaded = unitsFetchedOnce && Object.keys(unitsState).length > 0; const titleStyle: React.CSSProperties = { textAlign: 'center' @@ -56,7 +50,7 @@ export default function ConversionsDetailComponent() { return (
- {isUpdatingCikAndDBViews ? ( + {(conversionsFetching || unitsFetching) ? (
@@ -72,24 +66,25 @@ export default function ConversionsDetailComponent() {

- {unitsStateLoaded && -
- -
} +
+ +
{/* Attempt to create a ConversionViewComponent for each ConversionData in Conversions State after sorting by the combination of the identifier of the source and destination of the conversion. */} - {unitsStateLoaded && Object.values(conversionsState) - .sort((conversionA: ConversionData, conversionB: ConversionData) => - ((unitsState[conversionA.sourceId].identifier + unitsState[conversionA.destinationId].identifier).toLowerCase() > - (unitsState[conversionB.sourceId].identifier + unitsState[conversionB.destinationId].identifier).toLowerCase()) ? 1 : - (((unitsState[conversionB.sourceId].identifier + unitsState[conversionB.destinationId].identifier).toLowerCase() > - (unitsState[conversionA.sourceId].identifier + unitsState[conversionA.destinationId].identifier).toLowerCase()) ? -1 : 0)) - .map(conversionData => (' + (conversionData as ConversionData).destinationId)} - units={unitsState} />))} + { + Object.values(conversionsState) + .sort((conversionA: ConversionData, conversionB: ConversionData) => + ((unitDataById[conversionA.sourceId].identifier + unitDataById[conversionA.destinationId].identifier).toLowerCase() > + (unitDataById[conversionB.sourceId].identifier + unitDataById[conversionB.destinationId].identifier).toLowerCase()) ? 1 : + (((unitDataById[conversionB.sourceId].identifier + unitDataById[conversionB.destinationId].identifier).toLowerCase() > + (unitDataById[conversionA.sourceId].identifier + unitDataById[conversionA.destinationId].identifier).toLowerCase()) ? -1 : 0)) + .map(conversionData => ( + ' + conversionData.destinationId} + /> + ))}
diff --git a/src/client/app/components/conversion/ConversionsDetailComponentWIP.tsx b/src/client/app/components/conversion/ConversionsDetailComponentWIP.tsx deleted file mode 100644 index 2929ad31c..000000000 --- a/src/client/app/components/conversion/ConversionsDetailComponentWIP.tsx +++ /dev/null @@ -1,94 +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 * as React from 'react'; -import { FormattedMessage } from 'react-intl'; -import SpinnerComponent from '../../components/SpinnerComponent'; -import TooltipHelpComponent from '../../components/TooltipHelpComponent'; -import { conversionsApi } from '../../redux/api/conversionsApi'; -import { unitsAdapter, unitsApi } from '../../redux/api/unitsApi'; -import { ConversionData } from '../../types/redux/conversions'; -import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import ConversionViewComponentWIP from './ConversionViewComponentWIP'; -import CreateConversionModalComponentWIP from './CreateConversionModalComponentWIP'; - -/** - * Defines the conversions page card view - * @returns Conversion page element - */ -export default function ConversionsDetailComponent() { - // The route stops you from getting to this page if not an admin. - - // Conversions state - const { data: conversionsState = [], isFetching: conversionsFetching } = conversionsApi.useGetConversionsDetailsQuery(); - // Units DataById - const { unitDataById = {}, isFetching: unitsFetching } = unitsApi.useGetUnitsDetailsQuery(undefined, { - selectFromResult: ({ data, ...result }) => ({ - ...result, - unitDataById: data && unitsAdapter.getSelectors().selectEntities(data) - }) - }) - // const x = useAppSelector(state => conversionsApi.endpoints.refresh.select()(state)) - - // unnecessary? Currently this occurs as a side effect of the mutation which will invalidate meters/group - // unused for now, until decided - // const isUpdatingCikAndDBViews = useAppSelector(state => state.admin.isUpdatingCikAndDBViews); - - // Check if the units state is fully loaded - - const titleStyle: React.CSSProperties = { - textAlign: 'center' - }; - - const tooltipStyle = { - display: 'inline-block', - fontSize: '50%', - // For now, only an admin can see the conversion page. - tooltipConversionView: 'help.admin.conversionview' - }; - - return ( -
- {(conversionsFetching || unitsFetching) ? ( -
- - -
- ) : ( -
- - -
-

- -
- -
-

-
- -
-
- {/* Attempt to create a ConversionViewComponent for each ConversionData in Conversions State after sorting by - the combination of the identifier of the source and destination of the conversion. */} - { - Object.values(conversionsState) - .sort((conversionA: ConversionData, conversionB: ConversionData) => - ((unitDataById[conversionA.sourceId].identifier + unitDataById[conversionA.destinationId].identifier).toLowerCase() > - (unitDataById[conversionB.sourceId].identifier + unitDataById[conversionB.destinationId].identifier).toLowerCase()) ? 1 : - (((unitDataById[conversionB.sourceId].identifier + unitDataById[conversionB.destinationId].identifier).toLowerCase() > - (unitDataById[conversionA.sourceId].identifier + unitDataById[conversionA.destinationId].identifier).toLowerCase()) ? -1 : 0)) - .map(conversionData => ( - ' + conversionData.destinationId} - /> - ))} -
-
-
- )} -
- ); -} diff --git a/src/client/app/components/conversion/CreateConversionModalComponent.tsx b/src/client/app/components/conversion/CreateConversionModalComponent.tsx index 72862ad94..e97258270 100644 --- a/src/client/app/components/conversion/CreateConversionModalComponent.tsx +++ b/src/client/app/components/conversion/CreateConversionModalComponent.tsx @@ -2,53 +2,31 @@ * 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 _ from 'lodash'; import * as React from 'react'; -import { useDispatch } from 'react-redux'; -import { useState, useEffect } from 'react'; -import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; +import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import translate from '../../utils/translate'; +import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; +import TooltipHelpComponent from '../TooltipHelpComponent'; +import { conversionsApi } from '../../redux/api/conversionsApi'; +import { useAppSelector } from '../../redux/hooks'; +import { selectDefaultCreateConversionValues, selectIsValidConversion } from '../../redux/selectors/adminSelectors'; import '../../styles/modal.css'; +import { tooltipBaseStyle } from '../../styles/modalStyle'; import { TrueFalseType } from '../../types/items'; +import { showErrorNotification } from '../../utils/notifications'; +import translate from '../../utils/translate'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import TooltipHelpComponent from '../../components/TooltipHelpComponent'; -import { addConversion } from '../../actions/conversions'; -import { UnitData, UnitDataById } from 'types/redux/units'; -import { ConversionData } from 'types/redux/conversions'; -import { UnitType } from '../../types/redux/units'; -import { notifyUser } from '../../utils/input' -import * as _ from 'lodash'; -import { tooltipBaseStyle } from '../../styles/modalStyle'; -import { Dispatch } from 'types/redux/actions'; -interface CreateConversionModalComponentProps { - conversionsState: ConversionData[]; - unitsState: UnitDataById; -} /** * Defines the create conversion modal form - * @param props Props for the component * @returns Conversion create element */ -export default function CreateConversionModalComponent(props: CreateConversionModalComponentProps) { - - const dispatch: Dispatch = useDispatch(); - +export default function CreateConversionModalComponent() { + const [addConversionMutation] = conversionsApi.useAddConversionMutation() // Want units in sorted order by identifier regardless of case. - const unitsSorted = _.sortBy(Object.values(props.unitsState), unit => unit.identifier.toLowerCase(), 'asc'); - const defaultValues = { - // Invalid source/destination ids arbitrarily set to -999. - // Meter Units are not allowed to be a destination. - sourceId: -999, - sourceOptions: unitsSorted as UnitData[], - destinationId: -999, - destinationOptions: unitsSorted.filter(unit => unit.typeOfUnit !== 'meter') as UnitData[], - bidirectional: true, - slope: 0, - intercept: 0, - note: '' - } + const defaultValues = useAppSelector(selectDefaultCreateConversionValues) /* State */ // Modal show @@ -60,120 +38,42 @@ export default function CreateConversionModalComponent(props: CreateConversionMo const handleShow = () => setShowModal(true); // Handlers for each type of input change - const [state, setState] = useState(defaultValues); + const [conversionState, setConversionState] = useState(defaultValues); + + // If the currently selected conversion is valid + const [validConversion, reason] = useAppSelector(state => selectIsValidConversion(state, conversionState)) const handleStringChange = (e: React.ChangeEvent) => { - setState({ ...state, [e.target.name]: e.target.value }); + setConversionState({ ...conversionState, [e.target.name]: e.target.value }); } const handleBooleanChange = (e: React.ChangeEvent) => { - setState({ ...state, [e.target.name]: JSON.parse(e.target.value) }); + setConversionState({ ...conversionState, [e.target.name]: JSON.parse(e.target.value) }); } const handleNumberChange = (e: React.ChangeEvent) => { // once a source or destination is selected, it will be removed from the other options. if (e.target.name === 'sourceId') { - setState({ + setConversionState(state => ({ ...state, sourceId: Number(e.target.value), destinationOptions: defaultValues.destinationOptions.filter(destination => destination.id !== Number(e.target.value)) - }); + })); } else if (e.target.name === 'destinationId') { - setState({ + setConversionState(state => ({ ...state, destinationId: Number(e.target.value), sourceOptions: defaultValues.sourceOptions.filter(source => source.id !== Number(e.target.value)) - }); + })); } else { - setState({ ...state, [e.target.name]: Number(e.target.value) }); + setConversionState(state => ({ ...state, [e.target.name]: Number(e.target.value) })); } } - - // If the currently selected conversion is valid - const [validConversion, setValidConversion] = useState(false); /* End State */ - //Update the valid conversion state any time the source id, destination id, or bidirectional status changes - useEffect(() => { - setValidConversion(isValidConversion(state.sourceId, state.destinationId, state.bidirectional)); - }, [state.sourceId, state.destinationId, state.bidirectional]); - // Reset the state to default values const resetState = () => { - setState(defaultValues); - } - - /** - * Checks if conversion is valid - * @param sourceId New conversion sourceId - * @param destinationId New conversion destinationId - * @param bidirectional New conversion bidirectional status - * @returns boolean representing if new conversion is valid or not - */ - const isValidConversion = (sourceId: number, destinationId: number, bidirectional: boolean) => { - /* Create Conversion Validation: - Source equals destination: invalid conversion - Conversion exists: invalid conversion - Conversion does not exist: - Inverse exists: - Conversion is bidirectional: invalid conversion - Destination cannot be a meter - Cannot mix unit represent - - TODO Some of these can go away when we make the menus dynamic. - */ - - // The destination cannot be a meter unit. - if (destinationId !== -999 && props.unitsState[destinationId].typeOfUnit === UnitType.meter) { - notifyUser(translate('conversion.create.destination.meter')); - return false; - } - - // Source or destination not set - if (sourceId === -999 || destinationId === -999) { - return false - } - - // Conversion already exists - if ((props.conversionsState.findIndex(conversionData => (( - conversionData.sourceId === state.sourceId) && - conversionData.destinationId === state.destinationId))) !== -1) { - notifyUser(translate('conversion.create.exists')); - return false; - } - - // You cannot have a conversion between units that differ in unit_represent. - // This means you cannot mix quantity, flow & raw. - if (props.unitsState[sourceId].unitRepresent !== props.unitsState[destinationId].unitRepresent) { - notifyUser(translate('conversion.create.mixed.represent')); - return false; - } - - - let isValid = true; - // Loop over conversions and check for existence of inverse of conversion passed in - // If there exists an inverse that is bidirectional, then there is no point in making a conversion since it is essentially a duplicate. - // If there is a non bidirectional inverse, then it is a valid conversion - Object.values(props.conversionsState).forEach(conversion => { - // Inverse exists - if ((conversion.sourceId === destinationId) && (conversion.destinationId === sourceId)) { - // Inverse is bidirectional - if (conversion.bidirectional) { - isValid = false; - } - // Inverse is not bidirectional - else { - if (bidirectional) { - // The new conversion is bidirectional - isValid = false; - } - } - } - }); - if (!isValid) { - notifyUser(translate('conversion.create.exists.inverse')); - } - return isValid; + setConversionState(defaultValues); } // Unlike edit, we decided to discard and inputs when you choose to leave the page. The reasoning is @@ -181,11 +81,18 @@ export default function CreateConversionModalComponent(props: CreateConversionMo // Submit const handleSubmit = () => { - // Close modal first to avoid repeat clicks - setShowModal(false); - // Add the new conversion and update the store - dispatch(addConversion(state)); - resetState(); + if (validConversion) { + // Close modal first to avoid repeat clicks + setShowModal(false); + //5 Add the new conversion and update the store + // Omit the source options , do not need to send in request so remove here. + // + addConversionMutation(_.omit(conversionState, 'sourceOptions')) + // dispatch(addConversion(conversionState)); + resetState(); + } else { + showErrorNotification(reason) + } }; const tooltipStyle = { @@ -220,17 +127,17 @@ export default function CreateConversionModalComponent(props: CreateConversionMo id='sourceId' name='sourceId' type='select' - value={state.sourceId} + value={conversionState.sourceId} onChange={e => handleNumberChange(e)} - invalid={state.sourceId === -999}> + invalid={conversionState.sourceId === -999}> {} - {Object.values(state.sourceOptions).map(unitData => { + {Object.values(conversionState.sourceOptions).map(unitData => { return () })} @@ -247,17 +154,17 @@ export default function CreateConversionModalComponent(props: CreateConversionMo id='destinationId' name='destinationId' type='select' - value={state.destinationId} + value={conversionState.destinationId} onChange={e => handleNumberChange(e)} - invalid={state.destinationId === -999}> + invalid={conversionState.destinationId === -999}> {} - {Object.values(state.destinationOptions).map(unitData => { + {Object.values(conversionState.destinationOptions).map(unitData => { return () })} @@ -289,7 +196,7 @@ export default function CreateConversionModalComponent(props: CreateConversionMo id='slope' name='slope' type='number' - value={state.slope} + value={conversionState.slope} onChange={e => handleNumberChange(e)} /> @@ -301,7 +208,7 @@ export default function CreateConversionModalComponent(props: CreateConversionMo id='intercept' name='intercept' type='number' - value={state.intercept} + value={conversionState.intercept} onChange={e => handleNumberChange(e)} /> @@ -314,21 +221,26 @@ export default function CreateConversionModalComponent(props: CreateConversionMo name='note' type='textarea' onChange={e => handleStringChange(e)} - value={state.note} /> + value={conversionState.note} /> + { + // Todo looks kind of bad make a better visible notification + !validConversion &&

{reason}

+ } + {/* Hides the modal */} {/* On click calls the function handleSaveChanges in this component */} -
); -} +} \ No newline at end of file diff --git a/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx b/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx deleted file mode 100644 index 7abd1d71a..000000000 --- a/src/client/app/components/conversion/CreateConversionModalComponentWIP.tsx +++ /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/. */ - -import * as _ from 'lodash'; -import * as React from 'react'; -import { useState } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; -import TooltipHelpComponent from '../../components/TooltipHelpComponent'; -import { conversionsApi } from '../../redux/api/conversionsApi'; -import { useAppSelector } from '../../redux/hooks'; -import { selectDefaultCreateConversionValues, selectIsValidConversion } from '../../redux/selectors/adminSelectors'; -import '../../styles/modal.css'; -import { tooltipBaseStyle } from '../../styles/modalStyle'; -import { TrueFalseType } from '../../types/items'; -import { showErrorNotification } from '../../utils/notifications'; -import translate from '../../utils/translate'; -import TooltipMarkerComponent from '../TooltipMarkerComponent'; - -/** - * Defines the create conversion modal form - * @returns Conversion create element - */ -export default function CreateConversionModalComponent() { - const [addConversionMutation] = conversionsApi.useAddConversionMutation() - // Want units in sorted order by identifier regardless of case. - - const defaultValues = useAppSelector(selectDefaultCreateConversionValues) - - /* State */ - // Modal show - const [showModal, setShowModal] = useState(false); - const handleClose = () => { - setShowModal(false); - resetState(); - }; - const handleShow = () => setShowModal(true); - - // Handlers for each type of input change - const [conversionState, setConversionState] = useState(defaultValues); - - // If the currently selected conversion is valid - const [validConversion, reason] = useAppSelector(state => selectIsValidConversion(state, conversionState)) - - const handleStringChange = (e: React.ChangeEvent) => { - setConversionState({ ...conversionState, [e.target.name]: e.target.value }); - } - - const handleBooleanChange = (e: React.ChangeEvent) => { - setConversionState({ ...conversionState, [e.target.name]: JSON.parse(e.target.value) }); - } - - const handleNumberChange = (e: React.ChangeEvent) => { - // once a source or destination is selected, it will be removed from the other options. - if (e.target.name === 'sourceId') { - setConversionState(state => ({ - ...state, - sourceId: Number(e.target.value), - destinationOptions: defaultValues.destinationOptions.filter(destination => destination.id !== Number(e.target.value)) - })); - } else if (e.target.name === 'destinationId') { - setConversionState(state => ({ - ...state, - destinationId: Number(e.target.value), - sourceOptions: defaultValues.sourceOptions.filter(source => source.id !== Number(e.target.value)) - })); - } else { - setConversionState(state => ({ ...state, [e.target.name]: Number(e.target.value) })); - } - } - /* End State */ - - // Reset the state to default values - const resetState = () => { - setConversionState(defaultValues); - } - - // Unlike edit, we decided to discard and inputs when you choose to leave the page. The reasoning is - // that create starts from an empty template. - - // Submit - const handleSubmit = () => { - if (validConversion) { - // Close modal first to avoid repeat clicks - setShowModal(false); - //5 Add the new conversion and update the store - // Omit the source options , do not need to send in request so remove here. - // - addConversionMutation(_.omit(conversionState, 'sourceOptions')) - // dispatch(addConversion(conversionState)); - resetState(); - } else { - showErrorNotification(reason) - } - }; - - const tooltipStyle = { - ...tooltipBaseStyle, - tooltipCreateConversionView: 'help.admin.conversioncreate' - }; - - return ( - <> - {/* Show modal button */} - - - - - - -
- -
-
- {/* when any of the conversion are changed call one of the functions. */} - - - - - {/* Source unit input*/} - - - handleNumberChange(e)} - invalid={conversionState.sourceId === -999}> - {} - {Object.values(conversionState.sourceOptions).map(unitData => { - return () - })} - - - - - - - - {/* Destination unit input*/} - - - handleNumberChange(e)} - invalid={conversionState.destinationId === -999}> - {} - {Object.values(conversionState.destinationOptions).map(unitData => { - return () - })} - - - - - - - - {/* Bidirectional Y/N input*/} - - - handleBooleanChange(e)}> - {Object.keys(TrueFalseType).map(key => { - return () - })} - - - - - {/* Slope input*/} - - - handleNumberChange(e)} /> - - - - {/* Intercept input*/} - - - handleNumberChange(e)} /> - - - - {/* Note input*/} - - - handleStringChange(e)} - value={conversionState.note} /> - - - - - { - // Todo looks kind of bad make a better visible notification - !validConversion &&

{reason}

- } - - {/* Hides the modal */} - - {/* On click calls the function handleSaveChanges in this component */} - -
-
- - ); -} \ No newline at end of file diff --git a/src/client/app/components/conversion/EditConversionModalComponent.tsx b/src/client/app/components/conversion/EditConversionModalComponent.tsx index a999b4136..ae10f916a 100644 --- a/src/client/app/components/conversion/EditConversionModalComponent.tsx +++ b/src/client/app/components/conversion/EditConversionModalComponent.tsx @@ -4,27 +4,24 @@ import * as React from 'react'; // Realize that * is already imported from react import { useState } from 'react'; -import { useDispatch } from 'react-redux'; -import { Button, Col, Container, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; import { FormattedMessage } from 'react-intl'; -import translate from '../../utils/translate'; -import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import TooltipHelpComponent from '../../components/TooltipHelpComponent'; +import { Button, Col, Container, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; +import TooltipHelpComponent from '../TooltipHelpComponent'; +import { conversionsApi } from '../../redux/api/conversionsApi'; +import { selectUnitDataById } from '../../redux/api/unitsApi'; +import { useAppSelector } from '../../redux/hooks'; import '../../styles/modal.css'; -import { submitEditedConversion, deleteConversion } from '../../actions/conversions'; +import { tooltipBaseStyle } from '../../styles/modalStyle'; import { TrueFalseType } from '../../types/items'; import { ConversionData } from '../../types/redux/conversions'; -import { UnitDataById } from 'types/redux/units'; -import ConfirmActionModalComponent from '../ConfirmActionModalComponent' -import { tooltipBaseStyle } from '../../styles/modalStyle'; -import { Dispatch } from 'types/redux/actions'; -import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; +import translate from '../../utils/translate'; +import ConfirmActionModalComponent from '../ConfirmActionModalComponent'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; interface EditConversionModalComponentProps { show: boolean; conversion: ConversionData; - unitsState: UnitDataById; header: string; // passed in to handle opening the modal handleShow: () => void; @@ -38,17 +35,12 @@ interface EditConversionModalComponentProps { * @returns Conversion edit element */ export default function EditConversionModalComponent(props: EditConversionModalComponentProps) { - const dispatch: Dispatch = useDispatch(); + const [editConversion] = conversionsApi.useEditConversionMutation() + const [deleteConversion] = conversionsApi.useDeleteConversionMutation() + const unitDataById = useAppSelector(selectUnitDataById) // Set existing conversion values - const values = { - sourceId: props.conversion.sourceId, - destinationId: props.conversion.destinationId, - bidirectional: props.conversion.bidirectional, - slope: props.conversion.slope, - intercept: props.conversion.intercept, - note: props.conversion.note - } + const values = { ...props.conversion } /* State */ // Handlers for each type of input change @@ -90,8 +82,10 @@ export default function EditConversionModalComponent(props: EditConversionModalC // Closes the warning modal // Do not call the handler function because we do not want to open the parent modal setShowDeleteConfirmationModal(false); + // Delete the conversion using the state object, it should only require the source and destination ids set - dispatch(deleteConversion(state as ConversionData)); + deleteConversion({ sourceId: state.sourceId, destinationId: state.destinationId }) + } /* End Confirm Delete Modal */ @@ -109,8 +103,8 @@ export default function EditConversionModalComponent(props: EditConversionModalC } const handleClose = () => { - props.handleClose(); resetState(); + props.handleClose(); } // Save changes @@ -131,8 +125,9 @@ export default function EditConversionModalComponent(props: EditConversionModalC // Only do work if there are changes if (conversionHasChanges) { // Save our changes by dispatching the submitEditedConversion action - dispatch(submitEditedConversion(state, shouldRedoCik)); - dispatch(unsavedWarningSlice.actions.removeUnsavedChanges()); + // dispatch(submitEditedConversion(state, shouldRedoCik)); + editConversion({ conversionData: state, shouldRedoCik }) + // dispatch(unsavedWarningSlice.actions.removeUnsavedChanges()); } } @@ -170,7 +165,7 @@ export default function EditConversionModalComponent(props: EditConversionModalC id='sourceId' name='sourceId' type='text' - defaultValue={props.unitsState[state.sourceId].identifier} + defaultValue={unitDataById[state.sourceId].identifier} // Disable input to prevent changing ID value disabled> @@ -184,7 +179,7 @@ export default function EditConversionModalComponent(props: EditConversionModalC id='destinationId' name='destinationId' type='text' - defaultValue={props.unitsState[state.destinationId].identifier} + defaultValue={unitDataById[state.destinationId].identifier} // Disable input to prevent changing ID value disabled> diff --git a/src/client/app/components/conversion/EditConversionModalComponentWIP.tsx b/src/client/app/components/conversion/EditConversionModalComponentWIP.tsx deleted file mode 100644 index 4ff03a296..000000000 --- a/src/client/app/components/conversion/EditConversionModalComponentWIP.tsx +++ /dev/null @@ -1,258 +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 * as React from 'react'; -// Realize that * is already imported from react -import { useState } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Button, Col, Container, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; -import TooltipHelpComponent from '../../components/TooltipHelpComponent'; -import { conversionsApi } from '../../redux/api/conversionsApi'; -import { selectUnitDataById } from '../../redux/api/unitsApi'; -import { useAppSelector } from '../../redux/hooks'; -import '../../styles/modal.css'; -import { tooltipBaseStyle } from '../../styles/modalStyle'; -import { TrueFalseType } from '../../types/items'; -import { ConversionData } from '../../types/redux/conversions'; -import translate from '../../utils/translate'; -import ConfirmActionModalComponent from '../ConfirmActionModalComponent'; -import TooltipMarkerComponent from '../TooltipMarkerComponent'; - - -interface EditConversionModalComponentProps { - show: boolean; - conversion: ConversionData; - header: string; - // passed in to handle opening the modal - handleShow: () => void; - // passed in to handle closing the modal - handleClose: () => void; -} - -/** - * Defines the edit conversion modal form - * @param props Props for the component - * @returns Conversion edit element - */ -export default function EditConversionModalComponent(props: EditConversionModalComponentProps) { - const [editConversion] = conversionsApi.useEditConversionMutation() - const [deleteConversion] = conversionsApi.useDeleteConversionMutation() - const unitDataById = useAppSelector(selectUnitDataById) - - // Set existing conversion values - const values = { ...props.conversion } - - /* State */ - // Handlers for each type of input change - const [state, setState] = useState(values); - - const handleStringChange = (e: React.ChangeEvent) => { - setState({ ...state, [e.target.name]: e.target.value }); - } - - const handleBooleanChange = (e: React.ChangeEvent) => { - setState({ ...state, [e.target.name]: JSON.parse(e.target.value) }); - } - - const handleNumberChange = (e: React.ChangeEvent) => { - setState({ ...state, [e.target.name]: Number(e.target.value) }); - } - /* End State */ - - /* Confirm Delete Modal */ - // Separate from state comment to keep everything related to the warning confirmation modal together - const [showDeleteConfirmationModal, setShowDeleteConfirmationModal] = useState(false); - const deleteConfirmationMessage = translate('conversion.delete.conversion') + ' [' + props.header + '] ?'; - const deleteConfirmText = translate('conversion.delete.conversion'); - const deleteRejectText = translate('cancel'); - // The first two handle functions below are required because only one Modal can be open at a time (properly) - const handleDeleteConfirmationModalClose = () => { - // Hide the warning modal - setShowDeleteConfirmationModal(false); - // Show the edit modal - handleShow(); - } - const handleDeleteConfirmationModalOpen = () => { - // Hide the edit modal - handleClose(); - // Show the warning modal - setShowDeleteConfirmationModal(true); - } - const handleDeleteConversion = () => { - // Closes the warning modal - // Do not call the handler function because we do not want to open the parent modal - setShowDeleteConfirmationModal(false); - - // Delete the conversion using the state object, it should only require the source and destination ids set - deleteConversion({ sourceId: state.sourceId, destinationId: state.destinationId }) - - } - /* End Confirm Delete Modal */ - - // Reset the state to default values - // To be used for the discard changes button - // Different use case from CreateConversionModalComponent's resetState - // This allows us to reset our state to match the store in the event of an edit failure - // Failure to edit conversions will not trigger a re-render, as no state has changed. Therefore, we must manually reset the values - const resetState = () => { - setState(values); - } - - const handleShow = () => { - props.handleShow(); - } - - const handleClose = () => { - resetState(); - props.handleClose(); - } - - // Save changes - // Currently using the old functionality which is to compare inherited prop values to state values - // If there is a difference between props and state, then a change was made - // Side note, we could probably just set a boolean when any input i - // Edit Conversion Validation: is not needed as no breaking edits can be made - const handleSaveChanges = () => { - // Close the modal first to avoid repeat clicks - props.handleClose(); - - // Need to redo Cik if slope, intercept, or bidirectional changes. - const shouldRedoCik = props.conversion.slope !== state.slope - || props.conversion.intercept !== state.intercept - || props.conversion.bidirectional !== state.bidirectional; - // Check for changes by comparing state to props - const conversionHasChanges = shouldRedoCik || props.conversion.note != state.note; - // Only do work if there are changes - if (conversionHasChanges) { - // Save our changes by dispatching the submitEditedConversion action - // dispatch(submitEditedConversion(state, shouldRedoCik)); - editConversion({ conversionData: state, shouldRedoCik }) - // dispatch(unsavedWarningSlice.actions.removeUnsavedChanges()); - } - } - - const tooltipStyle = { - ...tooltipBaseStyle, - tooltipEditConversionView: 'help.admin.conversionedit' - }; - - return ( - <> - - - - - -
- -
-
- {/* when any of the conversion are changed call one of the functions. */} - - - - - {/* Source unit - display only */} - - - - - - - - {/* Destination unit - display only */} - - - - - - - - {/* Bidirectional Y/N input */} - - - handleBooleanChange(e)}> - {Object.keys(TrueFalseType).map(key => { - return () - })} - - - - - {/* Slope input */} - - - handleNumberChange(e)} /> - - - - {/* Intercept input */} - - - handleNumberChange(e)} /> - - - - {/* Note input */} - - - handleStringChange(e)} /> - - - - - - {/* Hides the modal */} - - {/* On click calls the function handleSaveChanges in this component */} - - -
- - ); -} diff --git a/src/client/app/components/groups/CreateGroupModalComponent.tsx b/src/client/app/components/groups/CreateGroupModalComponent.tsx index 229fa8938..6d3dc364e 100644 --- a/src/client/app/components/groups/CreateGroupModalComponent.tsx +++ b/src/client/app/components/groups/CreateGroupModalComponent.tsx @@ -5,16 +5,18 @@ import * as _ from 'lodash'; import * as React from 'react'; import { useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useSelector } from 'react-redux'; import { Button, Col, Container, FormFeedback, FormGroup, Input, InputGroup, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; -import { State } from 'types/redux/state'; -import TooltipHelpComponent from '../../components/TooltipHelpComponent'; +import { GroupData } from 'types/redux/groups'; +import TooltipHelpComponent from '../TooltipHelpComponent'; +import { groupsApi, selectGroupDataById } from '../../redux/api/groupsApi'; +import { selectUnitDataById } from '../../redux/api/unitsApi'; +import { useAppSelector } from '../../redux/hooks'; +import { selectPossibleGraphicUnits } from '../../redux/selectors/adminSelectors'; import '../../styles/modal.css'; import { tooltipBaseStyle } from '../../styles/modalStyle'; -import { ConversionArray } from '../../types/conversionArray'; import { SelectOption, TrueFalseType } from '../../types/items'; import { UnitData } from '../../types/redux/units'; import { GPSPoint, isValidGPSInput } from '../../utils/calibration'; @@ -25,37 +27,34 @@ import { unitsCompatibleWithMeters } from '../../utils/determineCompatibleUnits'; import { AreaUnitType, getAreaUnitConversion } from '../../utils/getAreaUnitConversion'; -import { isRoleAdmin } from '../../utils/hasPermissions'; import { getGPSString, notifyUser } from '../../utils/input'; import translate from '../../utils/translate'; import ListDisplayComponent from '../ListDisplayComponent'; import MultiSelectComponent from '../MultiSelectComponent'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; - -interface CreateGroupModalComponentProps { - possibleGraphicUnits: Set; -} +import { selectMeterDataById } from '../../redux/api/metersApi'; /** * Defines the create group modal form - * @param props pass in graphic units * @returns Group create element */ -export default function CreateGroupModalComponent(props: CreateGroupModalComponentProps) { +export default function CreateGroupModalComponentWIP() { + const [createGroup] = groupsApi.useCreateGroupMutation() - // Meter state - const metersState = useSelector((state: State) => state.meters.byMeterID); - // Group state - const groupsState = useSelector((state: State) => state.groups.byGroupID); - // Unit state - const unitsState = useSelector((state: State) => state.units.units); + // Meters state + const metersDataById = useAppSelector(selectMeterDataById); + // Groups state + const groupDataById = useAppSelector(selectGroupDataById); + // Units state + const unitsDataById = useAppSelector(selectUnitDataById); // Check for admin status - const currentUser = useSelector((state: State) => state.currentUser.profile); - const loggedInAsAdmin = (currentUser !== null) && isRoleAdmin(currentUser.role); + const possibleGraphicUnits = useAppSelector(selectPossibleGraphicUnits) // Since creating group the initial values are effectively nothing or the desired defaults. - const defaultValues = { + const defaultValues: GroupData = { + // ID not needed, assigned by DB, add here for TS + id: -1, name: '', childMeters: [] as number[], childGroups: [] as number[], @@ -80,8 +79,8 @@ export default function CreateGroupModalComponent(props: CreateGroupModalCompone // Information on the default graphic unit values. const graphicUnitsStateDefaults = { - possibleGraphicUnits: props.possibleGraphicUnits, - compatibleGraphicUnits: props.possibleGraphicUnits, + possibleGraphicUnits: possibleGraphicUnits, + compatibleGraphicUnits: possibleGraphicUnits, incompatibleGraphicUnits: new Set() } @@ -108,7 +107,7 @@ export default function CreateGroupModalComponent(props: CreateGroupModalCompone const [showModal, setShowModal] = useState(false); // Dropdowns state - const [groupChildrenState, setGroupChildrenState] = useState(groupChildrenDefaults) + const [groupChildrenState, setGroupChildrenState] = useState(groupChildrenDefaults); const [graphicUnitsState, setGraphicUnitsState] = useState(graphicUnitsStateDefaults); /* Create Group Validation: @@ -135,7 +134,7 @@ export default function CreateGroupModalComponent(props: CreateGroupModalCompone let areaSum = 0; let notifyMsg = ''; state.deepMeters.forEach(meterID => { - const meter = metersState[meterID]; + const meter = metersDataById[meterID]; if (meter.area > 0) { if (meter.areaUnit != AreaUnitType.none) { areaSum += meter.area * getAreaUnitConversion(meter.areaUnit, state.areaUnit); @@ -219,7 +218,8 @@ export default function CreateGroupModalComponent(props: CreateGroupModalCompone // GPS may have been updated so create updated state to submit. const submitState = { ...state, gps: gps }; console.log('removeMe', submitState) - // dispatch(submitNewGroup(submitState)); + + createGroup(submitState) resetState(); } else { // Tell user that not going to update due to input issues. @@ -230,61 +230,57 @@ export default function CreateGroupModalComponent(props: CreateGroupModalCompone // Determine allowed child meters/groups for menu. useEffect(() => { // Can only vary if admin and only used then. - if (loggedInAsAdmin) { - // This is the current deep meters of this group including any changes. - // The id is not really needed so set to -1 since same function for edit. - const groupDeepMeter = metersInChangedGroup({ ...state, id: -1 }); - // Get meters that okay for this group in a format the component can display. - const possibleMeters = getMeterMenuOptionsForGroup(state.defaultGraphicUnit, groupDeepMeter); - // Get groups okay for this group. Similar to meters. - // Since creating a group, the group cannot yet exist in the Redux state. Thus, the id is not used - // in this case so set to -1 so it never matches in this function. - const possibleGroups = getGroupMenuOptionsForGroup(-1, state.defaultGraphicUnit, groupDeepMeter); - // Update the state - setGroupChildrenState({ - ...groupChildrenState, - meterSelectOptions: possibleMeters, - groupSelectOptions: possibleGroups - }); - } + // This is the current deep meters of this group including any changes. + // The id is not really needed so set to -1 since same function for edit. + const groupDeepMeter = metersInChangedGroup(state); + // Get meters that okay for this group in a format the component can display. + const possibleMeters = getMeterMenuOptionsForGroup(state.defaultGraphicUnit, groupDeepMeter); + // Get groups okay for this group. Similar to meters. + // Since creating a group, the group cannot yet exist in the Redux state. Thus, the id is not used + // in this case so set to -1 so it never matches in this function. + const possibleGroups = getGroupMenuOptionsForGroup(-1, state.defaultGraphicUnit, groupDeepMeter); + // Update the state + setGroupChildrenState(groupChildrenState => ({ + ...groupChildrenState, + meterSelectOptions: possibleMeters, + groupSelectOptions: possibleGroups + })); // pik is needed since the compatible units is not correct until pik is available. // metersState normally does not change but can so include. // groupState can change if another group is created/edited and this can change ones displayed in menus. - }, [metersState, groupsState, state.defaultGraphicUnit, state.deepMeters]); + }, [state]); // Update compatible default graphic units set. useEffect(() => { - if (loggedInAsAdmin) { - // Graphic units compatible with currently selected meters/groups. - const compatibleGraphicUnits = new Set(); - // Graphic units incompatible with currently selected meters/groups. - const incompatibleGraphicUnits = new Set(); - // First must get a set from the array of deep meter numbers which is all meters currently in this group. - const deepMetersSet = new Set(state.deepMeters); - // Get the units that are compatible with this set of meters. - const allowedDefaultGraphicUnit = unitsCompatibleWithMeters(deepMetersSet); - // No unit allowed so modify allowed ones. Should not be there but will be fine if is. - allowedDefaultGraphicUnit.add(-99); - graphicUnitsState.possibleGraphicUnits.forEach(unit => { - // If current graphic unit exists in the set of allowed graphic units then compatible and not otherwise. - if (allowedDefaultGraphicUnit.has(unit.id)) { - compatibleGraphicUnits.add(unit); - } - else { - incompatibleGraphicUnits.add(unit); - } - }); - // Update the state - setGraphicUnitsState({ - ...graphicUnitsState, - compatibleGraphicUnits: compatibleGraphicUnits, - incompatibleGraphicUnits: incompatibleGraphicUnits - }); - } + // Graphic units compatible with currently selected meters/groups. + const compatibleGraphicUnits = new Set(); + // Graphic units incompatible with currently selected meters/groups. + const incompatibleGraphicUnits = new Set(); + // First must get a set from the array of deep meter numbers which is all meters currently in this group. + const deepMetersSet = new Set(state.deepMeters); + // Get the units that are compatible with this set of meters. + const allowedDefaultGraphicUnit = unitsCompatibleWithMeters(deepMetersSet); + // No unit allowed so modify allowed ones. Should not be there but will be fine if is. + allowedDefaultGraphicUnit.add(-99); + graphicUnitsState.possibleGraphicUnits.forEach(unit => { + // If current graphic unit exists in the set of allowed graphic units then compatible and not otherwise. + if (allowedDefaultGraphicUnit.has(unit.id)) { + compatibleGraphicUnits.add(unit); + } + else { + incompatibleGraphicUnits.add(unit); + } + }); + // Update the state + setGraphicUnitsState(graphicUnitsState => ({ + ...graphicUnitsState, + compatibleGraphicUnits: compatibleGraphicUnits, + incompatibleGraphicUnits: incompatibleGraphicUnits + })); // If any of these change then it needs to be updated. // metersState normally does not change but can so include. // pik is needed since the compatible units is not correct until pik is available. - }, [ConversionArray.pikAvailable(), metersState, state.deepMeters]); + }, [graphicUnitsState.possibleGraphicUnits, state.deepMeters]); const tooltipStyle = { ...tooltipBaseStyle, @@ -446,7 +442,7 @@ export default function CreateGroupModalComponent(props: CreateGroupModalCompone let dgu = state.defaultGraphicUnit; if (!newAllowedDGU.has(dgu)) { // The current default graphic unit is not compatible so set to no unit and warn admin. - notifyUser(`${translate('group.create.nounit')} "${unitsState[dgu].identifier}"`); + notifyUser(`${translate('group.create.nounit')} "${unitsDataById[dgu].identifier}"`); dgu = -99; } // Update the deep meter, child meter & default graphic unit state based on the changes. @@ -482,7 +478,7 @@ export default function CreateGroupModalComponent(props: CreateGroupModalCompone let dgu = state.defaultGraphicUnit; if (!newAllowedDGU.has(dgu)) { // The current default graphic unit is not compatible so set to no unit and warn admin. - notifyUser(`${translate('group.create.nounit')} "${unitsState[dgu].identifier}"`); + notifyUser(`${translate('group.create.nounit')} "${unitsDataById[dgu].identifier}"`); dgu = -99; } // Update the deep meter, child meter & default graphic unit state based on the changes. @@ -524,7 +520,7 @@ export default function CreateGroupModalComponent(props: CreateGroupModalCompone state.childMeters.forEach(groupId => { selectedMetersUnsorted.push({ value: groupId, - label: metersState[groupId].identifier + label: metersDataById[groupId].identifier // isDisabled not needed since only used for selected and not display. } as SelectOption ); @@ -543,7 +539,7 @@ export default function CreateGroupModalComponent(props: CreateGroupModalCompone state.childGroups.forEach(groupId => { selectedGroupsUnsorted.push({ value: groupId, - label: groupsState[groupId].name + label: groupDataById[groupId].name // isDisabled not needed since only used for selected and not display. } as SelectOption ); @@ -560,7 +556,7 @@ export default function CreateGroupModalComponent(props: CreateGroupModalCompone // Create list of meter identifiers. const listedDeepMeters: string[] = []; state.deepMeters.forEach(meterId => { - listedDeepMeters.push(metersState[meterId].identifier); + listedDeepMeters.push(metersDataById[meterId].identifier); }); // Sort for display. return listedDeepMeters.sort(); diff --git a/src/client/app/components/groups/CreateGroupModalComponentWIP.tsx b/src/client/app/components/groups/CreateGroupModalComponentWIP.tsx deleted file mode 100644 index dafc16010..000000000 --- a/src/client/app/components/groups/CreateGroupModalComponentWIP.tsx +++ /dev/null @@ -1,564 +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 * as _ from 'lodash'; -import * as React from 'react'; -import { useEffect, useState } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { - Button, Col, Container, FormFeedback, FormGroup, Input, InputGroup, - Label, Modal, ModalBody, ModalFooter, ModalHeader, Row -} from 'reactstrap'; -import { GroupData } from 'types/redux/groups'; -import TooltipHelpComponent from '../../components/TooltipHelpComponent'; -import { groupsApi, selectGroupDataById } from '../../redux/api/groupsApi'; -import { selectUnitDataById } from '../../redux/api/unitsApi'; -import { useAppSelector } from '../../redux/hooks'; -import { selectPossibleGraphicUnits } from '../../redux/selectors/adminSelectors'; -import '../../styles/modal.css'; -import { tooltipBaseStyle } from '../../styles/modalStyle'; -import { SelectOption, TrueFalseType } from '../../types/items'; -import { UnitData } from '../../types/redux/units'; -import { GPSPoint, isValidGPSInput } from '../../utils/calibration'; -import { - getGroupMenuOptionsForGroup, - getMeterMenuOptionsForGroup, - metersInChangedGroup, - unitsCompatibleWithMeters -} from '../../utils/determineCompatibleUnits'; -import { AreaUnitType, getAreaUnitConversion } from '../../utils/getAreaUnitConversion'; -import { getGPSString, notifyUser } from '../../utils/input'; -import translate from '../../utils/translate'; -import ListDisplayComponent from '../ListDisplayComponent'; -import MultiSelectComponent from '../MultiSelectComponent'; -import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import { selectMeterDataById } from '../../redux/api/metersApi'; - -/** - * Defines the create group modal form - * @returns Group create element - */ -export default function CreateGroupModalComponentWIP() { - const [createGroup] = groupsApi.useCreateGroupMutation() - - // Meters state - const metersDataById = useAppSelector(selectMeterDataById); - // Groups state - const groupDataById = useAppSelector(selectGroupDataById); - // Units state - const unitsDataById = useAppSelector(selectUnitDataById); - - // Check for admin status - const possibleGraphicUnits = useAppSelector(selectPossibleGraphicUnits) - - // Since creating group the initial values are effectively nothing or the desired defaults. - const defaultValues: GroupData = { - // ID not needed, assigned by DB, add here for TS - id: -1, - name: '', - childMeters: [] as number[], - childGroups: [] as number[], - deepMeters: [] as number[], - gps: null, - displayable: false, - note: '', - area: 0, - // default is no unit or -99. - defaultGraphicUnit: -99, - areaUnit: AreaUnitType.none - } - - // The information on the children of this group for state. Except for selected, the - // values are set by the useEffect functions. - const groupChildrenDefaults = { - // The meter selections in format for selection dropdown and initially empty. - meterSelectOptions: [] as SelectOption[], - // The group selections in format for selection dropdown and initially empty. - groupSelectOptions: [] as SelectOption[] - } - - // Information on the default graphic unit values. - const graphicUnitsStateDefaults = { - possibleGraphicUnits: possibleGraphicUnits, - compatibleGraphicUnits: possibleGraphicUnits, - incompatibleGraphicUnits: new Set() - } - - /* State */ - // State for the created group. - const [state, setState] = useState(defaultValues); - - // Handlers for each type of input change - - const handleStringChange = (e: React.ChangeEvent) => { - setState({ ...state, [e.target.name]: e.target.value }); - } - - const handleBooleanChange = (e: React.ChangeEvent) => { - setState({ ...state, [e.target.name]: JSON.parse(e.target.value) }); - } - - const handleNumberChange = (e: React.ChangeEvent) => { - setState({ ...state, [e.target.name]: Number(e.target.value) }); - } - - // Unlike EditGroupsModalComponent, we don't pass show and close via props. - // Modal show - const [showModal, setShowModal] = useState(false); - - // Dropdowns state - const [groupChildrenState, setGroupChildrenState] = useState(groupChildrenDefaults) - const [graphicUnitsState, setGraphicUnitsState] = useState(graphicUnitsStateDefaults); - - /* Create Group Validation: - Name cannot be blank - Area must be positive or zero - If area is nonzero, area unit must be set - Group must have at least one child (i.e has deep child meters) - */ - const [validGroup, setValidGroup] = useState(false); - useEffect(() => { - setValidGroup( - state.name !== '' && - (state.area === 0 || (state.area > 0 && state.areaUnit !== AreaUnitType.none)) && - (state.deepMeters.length > 0) - ); - }, [state.area, state.areaUnit, state.name, state.deepMeters]); - /* End State */ - - // Sums the area of the group's deep meters. It will tell the admin if any meters are omitted from the calculation, - // or if any other errors are encountered. - const handleAutoCalculateArea = () => { - if (state.deepMeters.length > 0) { - if (state.areaUnit != AreaUnitType.none) { - let areaSum = 0; - let notifyMsg = ''; - state.deepMeters.forEach(meterID => { - const meter = metersDataById[meterID]; - if (meter.area > 0) { - if (meter.areaUnit != AreaUnitType.none) { - areaSum += meter.area * getAreaUnitConversion(meter.areaUnit, state.areaUnit); - } else { - // This shouldn't happen because of the other checks in place when editing/creating a meter. - // However, there could still be edge cases (i.e meters from before area units were added) that could violate this. - notifyMsg += '\n"' + meter.identifier + '"' + translate('group.area.calculate.error.unit'); - } - } else { - notifyMsg += '\n"' + meter.identifier + '"' + translate('group.area.calculate.error.zero'); - } - }); - let msg = translate('group.area.calculate.header') + areaSum + ' ' + translate(`AreaUnitType.${state.areaUnit}`); - if (notifyMsg != '') { - msg += '\n' + translate('group.area.calculate.error.header') + notifyMsg; - } - if (window.confirm(msg)) { - // the + here converts back into a number - setState({ ...state, ['area']: +areaSum.toPrecision(6) }); - } - } else { - notifyUser(translate('group.area.calculate.error.group.unit')); - } - } else { - notifyUser(translate('group.area.calculate.error.no.meters')); - } - } - - const handleClose = () => { - setShowModal(false); - resetState(); - }; - const handleShow = () => { setShowModal(true); } - - // Reset the state to default value so each time starts from scratch. - const resetState = () => { - setState(defaultValues); - setGroupChildrenState(groupChildrenDefaults); - setGraphicUnitsState(graphicUnitsStateDefaults); - } - - // Unlike edit, we decided to discard inputs when you choose to leave the page. The reasoning is - // that create starts from an empty template. - - // Save changes - const handleSubmit = () => { - // Close modal first to avoid repeat clicks - setShowModal(false); - - // true if inputted values are okay. Then can submit. - let inputOk = true; - - // Check GPS entered. - const gpsInput = state.gps; - let gps: GPSPoint | null = null; - const latitudeIndex = 0; - const longitudeIndex = 1; - // If the user input a value then gpsInput should be a string. - // null came from the DB and it is okay to just leave it - Not a string. - if (typeof gpsInput === 'string') { - if (isValidGPSInput(gpsInput)) { - // Clearly gpsInput is a string but TS complains about the split so cast. - const gpsValues = (gpsInput as string).split(',').map((value: string) => parseFloat(value)); - // It is valid and needs to be in this format for routing. - gps = { - longitude: gpsValues[longitudeIndex], - latitude: gpsValues[latitudeIndex] - }; - // gpsInput must be of type string but TS does not think so so cast. - } else if ((gpsInput as string).length !== 0) { - // GPS not okay. Only true if some input. - // TODO isValidGPSInput currently pops up an alert so not doing it here, may change - // so leaving code commented out. - // notifyUser(translate('input.gps.range') + state.gps + '.'); - inputOk = false; - } - } - - if (inputOk) { - // The input passed validation. - // GPS may have been updated so create updated state to submit. - const submitState = { ...state, gps: gps }; - console.log('removeMe', submitState) - - createGroup(submitState) - resetState(); - } else { - // Tell user that not going to update due to input issues. - notifyUser(translate('group.input.error')); - } - }; - - // Determine allowed child meters/groups for menu. - useEffect(() => { - // Can only vary if admin and only used then. - // This is the current deep meters of this group including any changes. - // The id is not really needed so set to -1 since same function for edit. - const groupDeepMeter = metersInChangedGroup(state); - // Get meters that okay for this group in a format the component can display. - const possibleMeters = getMeterMenuOptionsForGroup(state.defaultGraphicUnit, groupDeepMeter); - // Get groups okay for this group. Similar to meters. - // Since creating a group, the group cannot yet exist in the Redux state. Thus, the id is not used - // in this case so set to -1 so it never matches in this function. - const possibleGroups = getGroupMenuOptionsForGroup(-1, state.defaultGraphicUnit, groupDeepMeter); - // Update the state - setGroupChildrenState(groupChildrenState => ({ - ...groupChildrenState, - meterSelectOptions: possibleMeters, - groupSelectOptions: possibleGroups - })); - // pik is needed since the compatible units is not correct until pik is available. - // metersState normally does not change but can so include. - // groupState can change if another group is created/edited and this can change ones displayed in menus. - }, [state]); - - // Update compatible default graphic units set. - useEffect(() => { - // Graphic units compatible with currently selected meters/groups. - const compatibleGraphicUnits = new Set(); - // Graphic units incompatible with currently selected meters/groups. - const incompatibleGraphicUnits = new Set(); - // First must get a set from the array of deep meter numbers which is all meters currently in this group. - const deepMetersSet = new Set(state.deepMeters); - // Get the units that are compatible with this set of meters. - const allowedDefaultGraphicUnit = unitsCompatibleWithMeters(deepMetersSet); - // No unit allowed so modify allowed ones. Should not be there but will be fine if is. - allowedDefaultGraphicUnit.add(-99); - graphicUnitsState.possibleGraphicUnits.forEach(unit => { - // If current graphic unit exists in the set of allowed graphic units then compatible and not otherwise. - if (allowedDefaultGraphicUnit.has(unit.id)) { - compatibleGraphicUnits.add(unit); - } - else { - incompatibleGraphicUnits.add(unit); - } - }); - // Update the state - setGraphicUnitsState(graphicUnitsState => ({ - ...graphicUnitsState, - compatibleGraphicUnits: compatibleGraphicUnits, - incompatibleGraphicUnits: incompatibleGraphicUnits - })); - // If any of these change then it needs to be updated. - // metersState normally does not change but can so include. - // pik is needed since the compatible units is not correct until pik is available. - }, [graphicUnitsState.possibleGraphicUnits, state.deepMeters]); - - const tooltipStyle = { - ...tooltipBaseStyle, - tooltipCreateGroupView: 'help.admin.groupcreate' - }; - - return ( - <> - {/* Show modal button */} - - - - - -
- -
-
- {/* when any of the group properties are changed call one of the functions. */} - - - {/* Name input */} - - - handleStringChange(e)} - required value={state.name} - invalid={state.name === ''} /> - - - - - {/* default graphic unit input */} - - - handleNumberChange(e)}> - {/* First list the selectable ones and then the rest as disabled. */} - {Array.from(graphicUnitsState.compatibleGraphicUnits).map(unit => { - return () - })} - {Array.from(graphicUnitsState.incompatibleGraphicUnits).map(unit => { - return () - })} - - - - {/* Displayable input */} - - - handleBooleanChange(e)}> - {Object.keys(TrueFalseType).map(key => { - return () - })} - - - {/* GPS input */} - - - handleStringChange(e)} - value={getGPSString(state.gps)} /> - - - {/* Area input */} - - - - handleNumberChange(e)} - invalid={state.area < 0} /> - {/* Calculate sum of meter areas */} - - - - - - - - {/* meter area unit input */} - - - handleStringChange(e)} - invalid={state.area > 0 && state.areaUnit === AreaUnitType.none}> - {Object.keys(AreaUnitType).map(key => { - return () - })} - - - - - - - {/* Note input */} - - - handleStringChange(e)} - value={state.note} /> - - {/* The child meters in this group */} - { - - : - { - // The meters changed so update the current list of deep meters - // Get the currently included/selected meters as an array of the ids. - const updatedChildMeters = newSelectedMeterOptions.map(meter => { return meter.value; }); - // The id is not really needed so set to -1 since same function for edit. - const newDeepMeters = metersInChangedGroup({ ...state, childMeters: updatedChildMeters, id: -1 }); - // The choice may have invalidated the default graphic unit so it needs - // to be reset to no unit. - // The selection encodes this information in the color but recalculate - // to see if this is the case. - // Get the units compatible with the new set of deep meters in group. - const newAllowedDGU = unitsCompatibleWithMeters(new Set(newDeepMeters)); - // Add no unit (-99) since that is okay so no change needed if current default graphic unit. - newAllowedDGU.add(-99); - let dgu = state.defaultGraphicUnit; - if (!newAllowedDGU.has(dgu)) { - // The current default graphic unit is not compatible so set to no unit and warn admin. - notifyUser(`${translate('group.create.nounit')} "${unitsDataById[dgu].identifier}"`); - dgu = -99; - } - // Update the deep meter, child meter & default graphic unit state based on the changes. - // Note could update child meters above to avoid updating state value for metersInChangedGroup but want - // to avoid too many state updates. - // It is possible the default graphic unit is unchanged but just do this. - setState({ ...state, deepMeters: newDeepMeters, childMeters: updatedChildMeters, defaultGraphicUnit: dgu }); - }} - /> - - } - {/* The child groups in this group */} - { - : - { - // The groups changed so update the current list of deep meters - // Get the currently included/selected meters as an array of the ids. - const updatedChildGroups = newSelectedGroupOptions.map(group => { return group.value; }); - // The id is not really needed so set to -1 since same function for edit. - const newDeepMeters = metersInChangedGroup({ ...state, childGroups: updatedChildGroups, id: -1 }); - // The choice may have invalidated the default graphic unit so it needs - // to be reset to no unit. - // The selection encodes this information in the color but recalculate - // to see if this is the case. - // Get the units compatible with the new set of deep meters in group. - const newAllowedDGU = unitsCompatibleWithMeters(new Set(newDeepMeters)); - // Add no unit (-99) since that is okay so no change needed if current default graphic unit. - newAllowedDGU.add(-99); - let dgu = state.defaultGraphicUnit; - if (!newAllowedDGU.has(dgu)) { - // The current default graphic unit is not compatible so set to no unit and warn admin. - notifyUser(`${translate('group.create.nounit')} "${unitsDataById[dgu].identifier}"`); - dgu = -99; - } - // Update the deep meter, child meter & default graphic unit state based on the changes. - // Note could update child groups above to avoid updating state value for metersInChangedGroup but want - // to avoid too many state updates. - // It is possible the default graphic unit is unchanged but just do this. - setState({ ...state, deepMeters: newDeepMeters, childGroups: updatedChildGroups, defaultGraphicUnit: dgu }); - }} - /> - - } - {/* All (deep) meters in this group */} - - : - - - - - {/* Hides the modal */} - - {/* On click calls the function handleSaveChanges in this component */} - - -
- - ); - - /** - * Converts the child meters of this group to options for menu sorted by identifier - * @returns SelectOptions sorted for child meters of group creating. - */ - function metersToSelectOptions(): SelectOption[] { - // In format for the display component for menu. - const selectedMetersUnsorted: SelectOption[] = []; - state.childMeters.forEach(groupId => { - selectedMetersUnsorted.push({ - value: groupId, - label: metersDataById[groupId].identifier - // isDisabled not needed since only used for selected and not display. - } as SelectOption - ); - }); - // Want chosen in sorted order. - return _.sortBy(selectedMetersUnsorted, item => item.label.toLowerCase(), 'asc'); - } - - /** - * Converts the child groups of this group to options for menu sorted by name - * @returns SelectOptions sorted for child groups of group editing. - */ - function groupsToSelectOptions(): SelectOption[] { - // In format for the display component for menu. - const selectedGroupsUnsorted: SelectOption[] = []; - state.childGroups.forEach(groupId => { - selectedGroupsUnsorted.push({ - value: groupId, - label: groupDataById[groupId].name - // isDisabled not needed since only used for selected and not display. - } as SelectOption - ); - }); - // Want chosen in sorted order. - return _.sortBy(selectedGroupsUnsorted, item => item.label.toLowerCase(), 'asc'); - } - - /** - * Converts the deep meters of this group to list options sorted by identifier. - * @returns names of all child meters in sorted order. - */ - function deepMetersToList() { - // Create list of meter identifiers. - const listedDeepMeters: string[] = []; - state.deepMeters.forEach(meterId => { - listedDeepMeters.push(metersDataById[meterId].identifier); - }); - // Sort for display. - return listedDeepMeters.sort(); - } -} diff --git a/src/client/app/components/groups/EditGroupModalComponent.tsx b/src/client/app/components/groups/EditGroupModalComponent.tsx index dbcbe091a..be459ab75 100644 --- a/src/client/app/components/groups/EditGroupModalComponent.tsx +++ b/src/client/app/components/groups/EditGroupModalComponent.tsx @@ -7,22 +7,23 @@ import * as React from 'react'; // Realize that * is already imported from react import { useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useSelector } from 'react-redux'; import { Button, Col, Container, FormFeedback, FormGroup, Input, InputGroup, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; -import { State } from 'types/redux/state'; -import TooltipHelpComponent from '../../components/TooltipHelpComponent'; +import TooltipHelpComponent from '../TooltipHelpComponent'; +import { groupsApi, selectGroupDataById } from '../../redux/api/groupsApi'; +import { useAppSelector } from '../../redux/hooks'; +import { selectPossibleGraphicUnits } from '../../redux/selectors/adminSelectors'; +import { selectIsAdmin } from '../../reducers/currentUser'; +import { store } from '../../store'; import '../../styles/card-page.css'; import '../../styles/modal.css'; import { tooltipBaseStyle } from '../../styles/modalStyle'; import { DataType } from '../../types/Datasources'; -import { ConversionArray } from '../../types/conversionArray'; import { SelectOption, TrueFalseType } from '../../types/items'; import { GroupData } from '../../types/redux/groups'; import { UnitData } from '../../types/redux/units'; -import { groupsApi } from '../../utils/api'; import { GPSPoint, isValidGPSInput } from '../../utils/calibration'; import { GroupCase, @@ -32,18 +33,17 @@ import { unitsCompatibleWithMeters } from '../../utils/determineCompatibleUnits'; import { AreaUnitType, getAreaUnitConversion } from '../../utils/getAreaUnitConversion'; -import { isRoleAdmin } from '../../utils/hasPermissions'; import { getGPSString, notifyUser, nullToEmptyString } from '../../utils/input'; import translate from '../../utils/translate'; import ConfirmActionModalComponent from '../ConfirmActionModalComponent'; import ListDisplayComponent from '../ListDisplayComponent'; import MultiSelectComponent from '../MultiSelectComponent'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; +import { selectMeterDataById } from '../../redux/api/metersApi'; interface EditGroupModalComponentProps { show: boolean; groupId: number; - possibleGraphicUnits: Set; // passed in to handle opening the modal handleShow: () => void; // passed in to handle closing the modal @@ -55,23 +55,24 @@ interface EditGroupModalComponentProps { * @param props state variables needed to define the component * @returns Group edit element */ -export default function EditGroupModalComponent(props: EditGroupModalComponentProps) { - // const dispatch: Dispatch = useDispatch(); - +export default function EditGroupModalComponentWIP(props: EditGroupModalComponentProps) { + const [submitGroupEdits] = groupsApi.useEditGroupMutation() + const [deleteGroup] = groupsApi.useDeleteGroupMutation() // Meter state - const metersState = useSelector((state: State) => state.meters.byMeterID); + const meterDataById = useAppSelector(selectMeterDataById); // Group state used on other pages - const globalGroupsState = useSelector((state: State) => state.groups.byGroupID); + const groupDataById = useAppSelector(selectGroupDataById); // Make a local copy of the group data so we can update during the edit process. // When the group is saved the values will be synced again with the global state. // This needs to be a deep clone so the changes are only local. - const [editGroupsState, setEditGroupsState] = useState(_.cloneDeep(globalGroupsState)); + const [editGroupsState, setEditGroupsState] = useState(_.cloneDeep(groupDataById)); + const possibleGraphicUnits = useAppSelector(selectPossibleGraphicUnits) + // The current groups state of group being edited of the local copy. It should always be valid. const groupState = editGroupsState[props.groupId]; // Check for admin status - const currentUser = useSelector((state: State) => state.currentUser.profile); - const loggedInAsAdmin = (currentUser !== null) && isRoleAdmin(currentUser.role); + const loggedInAsAdmin = useAppSelector(selectIsAdmin); // The information on the allowed children of this group that can be selected in the menus. const groupChildrenDefaults = { @@ -83,8 +84,8 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr // Information on the default graphic unit values. const graphicUnitsStateDefaults = { - possibleGraphicUnits: props.possibleGraphicUnits, - compatibleGraphicUnits: props.possibleGraphicUnits, + possibleGraphicUnits: possibleGraphicUnits, + compatibleGraphicUnits: possibleGraphicUnits, incompatibleGraphicUnits: new Set() } @@ -167,7 +168,7 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr // Do not call the handler function because we do not want to open the parent modal setShowDeleteConfirmationModal(false); // Delete the group using the state object where only really need id. - // dispatch(deleteGroup(groupState)); + deleteGroup(groupState.id) } /* End Confirm Delete Modal */ @@ -179,7 +180,7 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr let areaSum = 0; let notifyMsg = ''; groupState.deepMeters.forEach(meterID => { - const meter = metersState[meterID]; + const meter = meterDataById[meterID]; if (meter.area > 0) { if (meter.areaUnit != AreaUnitType.none) { areaSum += meter.area * getAreaUnitConversion(meter.areaUnit, groupState.areaUnit); @@ -221,7 +222,7 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr // Failure to edit groups will not trigger a re-render, as no state has changed. Therefore, we must manually reset the values const resetState = () => { // Set back to the global group values for this group. As before, need a deep copy. - setEditGroupsState(_.cloneDeep(globalGroupsState)); + setEditGroupsState(_.cloneDeep(groupDataById)); // Set back to the default values for the menus. setGroupChildrenState(groupChildrenDefaults); setGraphicUnitsState(graphicUnitsStateDefaults); @@ -252,7 +253,7 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr // Check for changes by comparing the original, global state to edited state. // This is the unedited state of the group being edited to compare to for changes. - const originalGroupState = globalGroupsState[groupState.id]; + const originalGroupState = groupDataById[groupState.id]; // Check children separately since lists. const childMeterChanges = !_.isEqual(originalGroupState.childMeters, groupState.childMeters); const childGroupChanges = !_.isEqual(originalGroupState.childGroups, groupState.childGroups); @@ -303,7 +304,7 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr // been made in the edit state. const groupsChanged: number[] = []; Object.values(editGroupsState).forEach(group => { - if (group.defaultGraphicUnit !== globalGroupsState[group.id].defaultGraphicUnit) { + if (group.defaultGraphicUnit !== groupDataById[group.id].defaultGraphicUnit) { groupsChanged.push(group.id); } }); @@ -326,7 +327,7 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr console.log(submitState, 'removeme') // This saves group to the DB and then refreshes the window if the last group being updated and // changes were made to the children. This avoid a reload on name change, etc. - // dispatch(submitGroupEdits(submitState, (i === groupsChanged.length ? true : false) && (childMeterChanges || childGroupChanges))); + submitGroupEdits(submitState) }); // The next line is unneeded since do refresh. // dispatch(removeUnsavedChanges()); @@ -345,16 +346,16 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr // Get groups okay for this group. Similar to meters. const possibleGroups = getGroupMenuOptionsForGroup(groupState.id, groupState.defaultGraphicUnit, groupState.deepMeters); // Update the state - setGroupChildrenState({ + setGroupChildrenState(groupChildrenState => ({ ...groupChildrenState, meterSelectOptions: possibleMeters, groupSelectOptions: possibleGroups - }); + })); } // pik is needed since the compatible units is not correct until pik is available. // metersState normally does not change but can so include. // globalGroupsState can change if another group is created/edited and this can change ones displayed in menus. - }, [ConversionArray.pikAvailable(), metersState, globalGroupsState, groupState.defaultGraphicUnit, groupState.deepMeters]); + }, [groupState.deepMeters, groupState.defaultGraphicUnit, groupState.id, loggedInAsAdmin]); // Update default graphic units set. useEffect(() => { @@ -379,11 +380,11 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr } }); // Update the state - setGraphicUnitsState({ + setGraphicUnitsState(graphicUnitsState => ({ ...graphicUnitsState, compatibleGraphicUnits: compatibleGraphicUnits, incompatibleGraphicUnits: incompatibleGraphicUnits - }); + })); } // If any of these change then it needs to be updated. // pik is needed since the compatible units is not correct until pik is available. @@ -391,7 +392,7 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr // If another group that is included in this group is changed then it must be redone // but we currently do a refresh so it is covered. It should still be okay if // the deep meters of this group are properly updated. - }, [ConversionArray.pikAvailable(), metersState, groupState.deepMeters]); + }, [graphicUnitsState.possibleGraphicUnits, groupState.deepMeters, loggedInAsAdmin]); const tooltipStyle = { ...tooltipBaseStyle, @@ -606,7 +607,7 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr // The new child meter removal was rejected so put it back. Should only be one item so no need to sort. newSelectedMeterOptions.push({ value: removedMeterId, - label: metersState[removedMeterId].identifier + label: meterDataById[removedMeterId].identifier // isDisabled not needed since only used for selected and not display. } as SelectOption ); @@ -747,7 +748,7 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr // Only do next step if update is still possible. if (shouldUpdate) { // Get all parent groups of this group. - const parentGroupIDs = await groupsApi.getParentIDs(groupState.id); + const { data: parentGroupIDs = [] } = await store.dispatch(groupsApi.endpoints.getParentIDs.initiate(groupState.id, { subscribe: false })) // Check for group changes and have admin agree or not. shouldUpdate = await validateGroupPostAddChild(groupState.id, parentGroupIDs, tempGroupsState); } @@ -892,7 +893,8 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr */ async function validateDelete() { // Get all parent groups of this group. - const parentGroupIDs = await groupsApi.getParentIDs(groupState.id); + const { data: parentGroupIDs = [] } = await store.dispatch(groupsApi.endpoints.getParentIDs.initiate(groupState.id, { subscribe: false })) + // If there are parents then you cannot delete this group. Notify admin. if (parentGroupIDs.length !== 0) { // This will hold the overall message for the admin alert. @@ -918,7 +920,7 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr groupState.childMeters.forEach(groupId => { selectedMetersUnsorted.push({ value: groupId, - label: metersState[groupId].identifier + label: meterDataById[groupId].identifier // isDisabled not needed since only used for selected and not display. } as SelectOption ); @@ -939,7 +941,7 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr value: groupId, // Use globalGroupsState so see edits in other groups. You would miss an update // in this group but it cannot be on the menu so that is okay. - label: globalGroupsState[groupId].name + label: groupDataById[groupId].name // isDisabled not needed since only used for selected and not display. } as SelectOption ); @@ -959,7 +961,7 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr // Tells if any meter is not visible to user. let hasHidden = false; groupState.childMeters.forEach(meterId => { - const meterIdentifier = metersState[meterId].identifier; + const meterIdentifier = meterDataById[meterId].identifier; // The identifier is null if the meter is not visible to this user. If hidden then do // not list and otherwise label. if (meterIdentifier === null) { @@ -1018,7 +1020,7 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr const listedDeepMeters: string[] = []; let hasHidden = false; groupState.deepMeters.forEach(meterId => { - const meterIdentifier = metersState[meterId].identifier; + const meterIdentifier = meterDataById[meterId].identifier; if (meterIdentifier === null) { // The identifier is null if the meter is not visible to this user. hasHidden = true; diff --git a/src/client/app/components/groups/EditGroupModalComponentWIP.tsx b/src/client/app/components/groups/EditGroupModalComponentWIP.tsx deleted file mode 100644 index c28ae4bc1..000000000 --- a/src/client/app/components/groups/EditGroupModalComponentWIP.tsx +++ /dev/null @@ -1,1087 +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 * as _ from 'lodash'; -import * as React from 'react'; -// Realize that * is already imported from react -import { useEffect, useState } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { - Button, Col, Container, FormFeedback, FormGroup, Input, InputGroup, - Label, Modal, ModalBody, ModalFooter, ModalHeader, Row -} from 'reactstrap'; -import TooltipHelpComponent from '../../components/TooltipHelpComponent'; -import { groupsApi, selectGroupDataById } from '../../redux/api/groupsApi'; -import { useAppSelector } from '../../redux/hooks'; -import { selectPossibleGraphicUnits } from '../../redux/selectors/adminSelectors'; -import { selectIsAdmin } from '../../reducers/currentUser'; -import { store } from '../../store'; -import '../../styles/card-page.css'; -import '../../styles/modal.css'; -import { tooltipBaseStyle } from '../../styles/modalStyle'; -import { DataType } from '../../types/Datasources'; -import { SelectOption, TrueFalseType } from '../../types/items'; -import { GroupData } from '../../types/redux/groups'; -import { UnitData } from '../../types/redux/units'; -import { GPSPoint, isValidGPSInput } from '../../utils/calibration'; -import { - GroupCase, - getCompatibilityChangeCase, - getGroupMenuOptionsForGroup, - getMeterMenuOptionsForGroup, - unitsCompatibleWithMeters -} from '../../utils/determineCompatibleUnits'; -import { AreaUnitType, getAreaUnitConversion } from '../../utils/getAreaUnitConversion'; -import { getGPSString, notifyUser, nullToEmptyString } from '../../utils/input'; -import translate from '../../utils/translate'; -import ConfirmActionModalComponent from '../ConfirmActionModalComponent'; -import ListDisplayComponent from '../ListDisplayComponent'; -import MultiSelectComponent from '../MultiSelectComponent'; -import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import { selectMeterDataById } from '../../redux/api/metersApi'; - -interface EditGroupModalComponentProps { - show: boolean; - groupId: number; - // passed in to handle opening the modal - handleShow: () => void; - // passed in to handle closing the modal - handleClose: () => void; -} - -/** - * Defines the edit group modal form - * @param props state variables needed to define the component - * @returns Group edit element - */ -export default function EditGroupModalComponentWIP(props: EditGroupModalComponentProps) { - const [submitGroupEdits] = groupsApi.useEditGroupMutation() - const [deleteGroup] = groupsApi.useDeleteGroupMutation() - // Meter state - const meterDataById = useAppSelector(selectMeterDataById); - // Group state used on other pages - const groupDataById = useAppSelector(selectGroupDataById); - // Make a local copy of the group data so we can update during the edit process. - // When the group is saved the values will be synced again with the global state. - // This needs to be a deep clone so the changes are only local. - const [editGroupsState, setEditGroupsState] = useState(_.cloneDeep(groupDataById)); - const possibleGraphicUnits = useAppSelector(selectPossibleGraphicUnits) - - // The current groups state of group being edited of the local copy. It should always be valid. - const groupState = editGroupsState[props.groupId]; - - // Check for admin status - const loggedInAsAdmin = useAppSelector(selectIsAdmin); - - // The information on the allowed children of this group that can be selected in the menus. - const groupChildrenDefaults = { - // The meter selections in format for selection dropdown. - meterSelectOptions: [] as SelectOption[], - // The group selections in format for selection dropdown. - groupSelectOptions: [] as SelectOption[] - } - - // Information on the default graphic unit values. - const graphicUnitsStateDefaults = { - possibleGraphicUnits: possibleGraphicUnits, - compatibleGraphicUnits: possibleGraphicUnits, - incompatibleGraphicUnits: new Set() - } - - /* State */ - // Handlers for each type of input change where update the local edit state. - - const handleStringChange = (e: React.ChangeEvent) => { - setEditGroupsState({ - ...editGroupsState, - [groupState.id]: { - ...editGroupsState[groupState.id], - [e.target.name]: e.target.value - } - }) - } - - const handleBooleanChange = (e: React.ChangeEvent) => { - setEditGroupsState({ - ...editGroupsState, - [groupState.id]: { - ...editGroupsState[groupState.id], - [e.target.name]: JSON.parse(e.target.value) - } - }) - } - - const handleNumberChange = (e: React.ChangeEvent) => { - setEditGroupsState({ - ...editGroupsState, - [groupState.id]: { - ...editGroupsState[groupState.id], - [e.target.name]: Number(e.target.value) - } - }) - } - - // Dropdowns state - const [groupChildrenState, setGroupChildrenState] = useState(groupChildrenDefaults) - const [graphicUnitsState, setGraphicUnitsState] = useState(graphicUnitsStateDefaults); - - /* Edit Group Validation: - Name cannot be blank - Area must be positive or zero - If area is nonzero, area unit must be set - Group must have at least one child (i.e has deep child meters) - */ - const [validGroup, setValidGroup] = useState(false); - useEffect(() => { - setValidGroup( - groupState.name !== '' && - (groupState.area === 0 || (groupState.area > 0 && groupState.areaUnit !== AreaUnitType.none)) && - (groupState.deepMeters.length > 0) - ); - }, [groupState.area, groupState.areaUnit, groupState.name, groupState.deepMeters]); - /* End State */ - - /* Confirm Delete Modal */ - // Separate from state comment to keep everything related to the warning confirmation modal together - const [showDeleteConfirmationModal, setShowDeleteConfirmationModal] = useState(false); - const deleteConfirmationMessage = translate('group.delete.group') + ' "' + groupState.name + '"?'; - const deleteConfirmText = translate('group.delete.group'); - const deleteRejectText = translate('cancel'); - // The first two handle functions below are required because only one Modal can be open at a time. - // The messages for delete are a modal so a separate one. Note other user messages are window popups. - // TODO We should probably go all to modal or popups for messages. - const handleDeleteConfirmationModalClose = () => { - // Hide the warning modal - setShowDeleteConfirmationModal(false); - // Show the edit modal - handleShow(); - } - const handleDeleteConfirmationModalOpen = () => { - // Hide the edit modal - handleClose(); - // Show the warning modal - setShowDeleteConfirmationModal(true); - } - const handleDeleteGroup = () => { - // Closes the warning modal - // Do not call the handler function because we do not want to open the parent modal - setShowDeleteConfirmationModal(false); - // Delete the group using the state object where only really need id. - deleteGroup(groupState.id) - } - /* End Confirm Delete Modal */ - - // Sums the area of the group's deep meters. It will tell the admin if any meters are omitted from the calculation, - // or if any other errors are encountered. - const handleAutoCalculateArea = () => { - if (groupState.deepMeters.length > 0) { - if (groupState.areaUnit != AreaUnitType.none) { - let areaSum = 0; - let notifyMsg = ''; - groupState.deepMeters.forEach(meterID => { - const meter = meterDataById[meterID]; - if (meter.area > 0) { - if (meter.areaUnit != AreaUnitType.none) { - areaSum += meter.area * getAreaUnitConversion(meter.areaUnit, groupState.areaUnit); - } else { - // This shouldn't happen because of the other checks in place when editing/creating a meter. - // However, there could still be edge cases (i.e meters from before area units were added) that could violate this. - notifyMsg += '\n"' + meter.identifier + '"' + translate('group.area.calculate.error.unit'); - } - } else { - notifyMsg += '\n"' + meter.identifier + '"' + translate('group.area.calculate.error.zero'); - } - }); - let msg = translate('group.area.calculate.header') + areaSum + ' ' + translate(`AreaUnitType.${groupState.areaUnit}`); - if (notifyMsg != '') { - msg += '\n' + translate('group.area.calculate.error.header') + notifyMsg; - } - if (window.confirm(msg)) { - setEditGroupsState({ - ...editGroupsState, - [groupState.id]: { - ...editGroupsState[groupState.id], - // the + here converts back into a number. this method also removes trailing zeroes. - ['area']: +areaSum.toPrecision(6) - } - }); - } - } else { - notifyUser(translate('group.area.calculate.error.group.unit')); - } - } else { - notifyUser(translate('group.area.calculate.error.no.meters')); - } - } - - // Reset the state to default values. - // To be used for the discard changes button - // Different use case from CreateGroupModalComponent's resetState - // This allows us to reset our state to match the store in the event of an edit failure - // Failure to edit groups will not trigger a re-render, as no state has changed. Therefore, we must manually reset the values - const resetState = () => { - // Set back to the global group values for this group. As before, need a deep copy. - setEditGroupsState(_.cloneDeep(groupDataById)); - // Set back to the default values for the menus. - setGroupChildrenState(groupChildrenDefaults); - setGraphicUnitsState(graphicUnitsStateDefaults); - } - - // Should show the modal for editing. - const handleShow = () => { - props.handleShow(); - } - - // Note this differs from the props.handleClose(). This is only called when the user - // clicks to discard or close the modal. - const handleClose = () => { - props.handleClose(); - if (loggedInAsAdmin) { - // State cannot change if you are not an admin. - resetState(); - } - } - - // Save changes - done when admin clicks the save button. - const handleSubmit = () => { - // Close the modal first to avoid repeat clicks - props.handleClose(); - - // true if inputted values are okay. Then can submit. - let inputOk = true; - - // Check for changes by comparing the original, global state to edited state. - // This is the unedited state of the group being edited to compare to for changes. - const originalGroupState = groupDataById[groupState.id]; - // Check children separately since lists. - const childMeterChanges = !_.isEqual(originalGroupState.childMeters, groupState.childMeters); - const childGroupChanges = !_.isEqual(originalGroupState.childGroups, groupState.childGroups); - const groupHasChanges = - ( - originalGroupState.name != groupState.name || - originalGroupState.displayable != groupState.displayable || - originalGroupState.gps != groupState.gps || - originalGroupState.note != groupState.note || - originalGroupState.area != groupState.area || - originalGroupState.defaultGraphicUnit != groupState.defaultGraphicUnit || - childMeterChanges || - childGroupChanges || - originalGroupState.areaUnit != groupState.areaUnit - ); - // Only validate and store if any changes. - if (groupHasChanges) { - //Check GPS is okay. - const gpsInput = groupState.gps; - let gps: GPSPoint | null = null; - const latitudeIndex = 0; - const longitudeIndex = 1; - // If the user input a value then gpsInput should be a string - // null came from DB and it is okay to just leave it - Not a String. - if (typeof gpsInput === 'string') { - if (isValidGPSInput(gpsInput)) { - // Clearly gpsInput is a string but TS complains about the split so cast. - const gpsValues = (gpsInput as string).split(',').map((value: string) => parseFloat(value)); - // It is valid and needs to be in this format for routing - gps = { - longitude: gpsValues[longitudeIndex], - latitude: gpsValues[latitudeIndex] - }; - } else if ((gpsInput as string).length !== 0) { - // GPS not okay and there since non-zero length value. - // TODO isValidGPSInput currently pops up an alert so not doing it here, may change - // so leaving code commented out. - // notifyUser(translate('input.gps.range') + groupState.gps + '.'); - inputOk = false; - } - } - - if (inputOk) { - // The input passed validation so okay to save. - - // A change in this group may have changed other group's default graphic unit. Thus, create a list of - // all groups needing to be saved. The change would have already - // been made in the edit state. - const groupsChanged: number[] = []; - Object.values(editGroupsState).forEach(group => { - if (group.defaultGraphicUnit !== groupDataById[group.id].defaultGraphicUnit) { - groupsChanged.push(group.id); - } - }); - // Make sure the group being edited is on the list. - if (!groupsChanged.includes(groupState.id)) { - // Add the edited one to the list. - groupsChanged.push(groupState.id); - } - - // For all changed groups, save the new group to the DB. - groupsChanged.forEach(groupId => { - const thisGroupState = editGroupsState[groupId]; - // There are extra properties in the state so only include the desired ones for edit submit. - // GPS is one above since may differ from the state. - const submitState = { - id: thisGroupState.id, name: thisGroupState.name, childMeters: thisGroupState.childMeters, - childGroups: thisGroupState.childGroups, gps: gps, displayable: thisGroupState.displayable, - note: thisGroupState.note, area: thisGroupState.area, defaultGraphicUnit: thisGroupState.defaultGraphicUnit, areaUnit: thisGroupState.areaUnit - } - console.log(submitState, 'removeme') - // This saves group to the DB and then refreshes the window if the last group being updated and - // changes were made to the children. This avoid a reload on name change, etc. - submitGroupEdits(submitState) - }); - // The next line is unneeded since do refresh. - // dispatch(removeUnsavedChanges()); - } else { - notifyUser(translate('group.input.error')); - } - } - }; - - // Determine allowed child meters/groups . - useEffect(() => { - // Can only vary if admin and only used then. - if (loggedInAsAdmin) { - // Get meters that okay for this group in a format the component can display. - const possibleMeters = getMeterMenuOptionsForGroup(groupState.defaultGraphicUnit, groupState.deepMeters); - // Get groups okay for this group. Similar to meters. - const possibleGroups = getGroupMenuOptionsForGroup(groupState.id, groupState.defaultGraphicUnit, groupState.deepMeters); - // Update the state - setGroupChildrenState(groupChildrenState => ({ - ...groupChildrenState, - meterSelectOptions: possibleMeters, - groupSelectOptions: possibleGroups - })); - } - // pik is needed since the compatible units is not correct until pik is available. - // metersState normally does not change but can so include. - // globalGroupsState can change if another group is created/edited and this can change ones displayed in menus. - }, [groupState.deepMeters, groupState.defaultGraphicUnit, groupState.id, loggedInAsAdmin]); - - // Update default graphic units set. - useEffect(() => { - // Only shown to an admin. - if (loggedInAsAdmin) { - // Graphic units compatible with currently selected meters/groups. - const compatibleGraphicUnits = new Set(); - // Graphic units incompatible with currently selected meters/groups. - const incompatibleGraphicUnits = new Set(); - // First must get a set from the array of deep meter numbers which is all meters currently in this group. - const deepMetersSet = new Set(groupState.deepMeters); - // Get the units that are compatible with this set of meters. - const allowedDefaultGraphicUnit = unitsCompatibleWithMeters(deepMetersSet); - // No unit allowed so modify allowed ones. Should not be there but will be fine if is since set. - allowedDefaultGraphicUnit.add(-99); - graphicUnitsState.possibleGraphicUnits.forEach(unit => { - // If current graphic unit exists in the set of allowed graphic units then compatible and not otherwise. - if (allowedDefaultGraphicUnit.has(unit.id)) { - compatibleGraphicUnits.add(unit); - } else { - incompatibleGraphicUnits.add(unit); - } - }); - // Update the state - setGraphicUnitsState(graphicUnitsState => ({ - ...graphicUnitsState, - compatibleGraphicUnits: compatibleGraphicUnits, - incompatibleGraphicUnits: incompatibleGraphicUnits - })); - } - // If any of these change then it needs to be updated. - // pik is needed since the compatible units is not correct until pik is available. - // metersState normally does not change but can so include. - // If another group that is included in this group is changed then it must be redone - // but we currently do a refresh so it is covered. It should still be okay if - // the deep meters of this group are properly updated. - }, [graphicUnitsState.possibleGraphicUnits, groupState.deepMeters, loggedInAsAdmin]); - - const tooltipStyle = { - ...tooltipBaseStyle, - // Switch help depending if admin or not. - tooltipEditGroupView: loggedInAsAdmin ? 'help.admin.groupedit' : 'help.groups.groupdetails' - }; - - return ( - <> - {/* This is for the modal for delete. */} - - - {/* In a number of the items that follow, what is shown varies on whether you are an admin. */} - - - -
- -
-
- - {loggedInAsAdmin ? - - {/* Name input for admin*/} - - - handleStringChange(e)} - required value={groupState.name} - invalid={groupState.name === ''} /> - - - - - {/* default graphic unit input for admin */} - - - handleNumberChange(e)}> - {/* First list the selectable ones and then the rest as disabled. */} - {Array.from(graphicUnitsState.compatibleGraphicUnits).map(unit => { - return () - })} - {Array.from(graphicUnitsState.incompatibleGraphicUnits).map(unit => { - return () - })} - - - - : <> - {/* Name display for non-admin */} - - - - - {/* default graphic unit display for non-admin */} - - - {/* TODO: This component still displays a dropdown arrow, even though a user cannot use the dropdown */} - - {Array.from(graphicUnitsState.compatibleGraphicUnits).map(unit => { - return () - })} - - - } - {loggedInAsAdmin && <> - - - {/* Displayable input, only for admin. */} - - - handleBooleanChange(e)}> - {Object.keys(TrueFalseType).map(key => { - return () - })} - - - - - {/* GPS input, only for admin. */} - - - handleStringChange(e)} - value={getGPSString(groupState.gps)} /> - - - - - - {/* Area input, only for admin. */} - - - - handleNumberChange(e)} - invalid={groupState.area < 0} /> - {/* Calculate sum of meter areas */} - - - - - - - - - - {/* meter area unit input */} - - - handleStringChange(e)} - invalid={groupState.area > 0 && groupState.areaUnit === AreaUnitType.none}> - {Object.keys(AreaUnitType).map(key => { - return () - })} - - - - - - - - {/* Note input, only for admin. */} - - - handleStringChange(e)} - value={nullToEmptyString(groupState.note)} /> - - } - {/* The child meters in this group */} - {loggedInAsAdmin ? - - : - { - // The meters changed so verify update is okay and deal with appropriately. - // The length of selected meters should only vary by 1 since each change is handled separately. - // Compare the new length to the original length that is the same as - // the number of child meters of group being edited. - if (newSelectedMeterOptions.length === groupState.childMeters.length + 1) { - // A meter was selected so it is considered for adding. - // The newly selected item is always the last one. - // Now attempt to add the child to see if okay. - const childAdded = await assignChildToGroup(newSelectedMeterOptions[newSelectedMeterOptions.length - 1].value, DataType.Meter); - if (!childAdded) { - // The new child meter was rejected so remove it. It is the last one. - newSelectedMeterOptions.pop(); - } - } else { - // Could have removed any item so figure out which one it is. Need to convert options to ids. - const removedMeter = _.difference(groupState.childMeters, newSelectedMeterOptions.map(item => { return item.value; })); - // There should only be one removed item. - const removedMeterId = removedMeter[0]; - const childRemoved = removeChildFromGroup(removedMeterId, DataType.Meter) - if (!childRemoved) { - // The new child meter removal was rejected so put it back. Should only be one item so no need to sort. - newSelectedMeterOptions.push({ - value: removedMeterId, - label: meterDataById[removedMeterId].identifier - // isDisabled not needed since only used for selected and not display. - } as SelectOption - ); - } - } - }} - /> - - : - - : - - - } - {/* The child groups in this group */} - {loggedInAsAdmin ? - - : - { - // The groups changed so verify update is okay and deal with appropriately. - // The length of of selected groups should only vary by 1 since each change is handled separately. - // Compare the new length to the original length that is the same as - // the number of child groups of group being edited. - if (newSelectedGroupOptions.length === groupState.childGroups.length + 1) { - // A group was selected so it is considered for adding. - // The newly selected item is always the last one. - // Now attempt to add the child to see if okay. - const childAdded = await assignChildToGroup(newSelectedGroupOptions[newSelectedGroupOptions.length - 1].value, DataType.Group); - if (!childAdded) { - // The new child meter was rejected so remove it. It is the last one. - newSelectedGroupOptions.pop(); - } - } else { - // Could have removed any item so figure out which one it is. Need to convert options to ids. - const removedGroup = _.difference(groupState.childGroups, newSelectedGroupOptions.map(item => { return item.value; })); - // There should only be one removed item. - const removedGroupId = removedGroup[0]; - const childRemoved = removeChildFromGroup(removedGroupId, DataType.Group) - if (!childRemoved) { - // The new child group removal was rejected so put it back. Should only be one item so no need to sort. - newSelectedGroupOptions.push({ - value: removedGroupId, - // The name should not have changed since cannot be group editing but use the edit state to be consistent. - label: editGroupsState[removedGroupId].name - // isDisabled not needed since only used for selected and not display. - } as SelectOption - ); - } - } - }} - /> - - : - - : - - - } - {/* All (deep) meters in this group */} - : - - - - {/* Delete, discard & save buttons if admin and close button if not. */} - {loggedInAsAdmin ? -
- {/* delete group */} - - {/* Hides the modal */} - - {/* On click calls the function handleSaveChanges in this component */} - -
- : - - } -
-
- - ); - - // The following functions are nested so can easily get and set the state that is local to the outer function. - - /** - * Validates and warns user when adding a child group/meter to a specific group. - * If the check pass, update the edited group and related groups. - * @param childId The group/meter's id to add to the parent group. - * @param childType Can be group or meter. - * @returns true if the child was assigned and false otherwise - */ - async function assignChildToGroup(childId: number, childType: DataType): Promise { - // Create a deep copy of the edit state before adding the child. We only need some of the state but this is easier. - // This copy is directly changed without using the Redux hooks since it is not used by React. - // This means that changes to the group do not happen unless the change is accepted and this copy is - // put back into the edit state. - const tempGroupsState = _.cloneDeep(editGroupsState); - - // Add the child to the group being edited in temp so can decide if want change. - // This assumes there are no duplicates which is not allowed by menus - if (childType === DataType.Meter) { - tempGroupsState[groupState.id].childMeters.push(childId); - } else { - tempGroupsState[groupState.id].childGroups.push(childId); - } - // The deep meters of any group can change for any group containing the group that just had a meter/group added. - // Since groups can be indirectly included in another group it is hard to know which ones where impacted so - // just redo them all for now. Also do this group since it likely changed. - // Returned value tells if update should happen. - let shouldUpdate = !Object.values(tempGroupsState).some(group => { - const deepMeters = calculateMetersInGroup(group.id, tempGroupsState); - if (deepMeters.length === 0) { - // There is a circular dependency so this change is not allowed. - // Cannot be case of no children since adding child. - // Let the user know. - notifyUser(`${translate('group.edit.circular')}\n\n${translate('group.edit.cancelled')}`); - // Stops processing and will return this result (negated). - return true; - } else { - // Group okay so update deep meters for it. - tempGroupsState[group.id].deepMeters = deepMeters; - // Go to next group/keep processing. - return false; - } - }); - - // Only do next step if update is still possible. - if (shouldUpdate) { - // Get all parent groups of this group. - const { data: parentGroupIDs = [] } = await store.dispatch(groupsApi.endpoints.getParentIDs.initiate(groupState.id, { subscribe: false })) - // Check for group changes and have admin agree or not. - shouldUpdate = await validateGroupPostAddChild(groupState.id, parentGroupIDs, tempGroupsState); - } - // If the admin wants to apply changes and allowed. - if (shouldUpdate) { - // Update the group. Now, the changes actually happen. - // Done by setting the edit state to the temp state so does not impact other groups - // and what is seen until the admin saves. - // Could limit to only ones changed but just do since local state and easy pull easy to see changed by Redux. - setEditGroupsState(tempGroupsState); - } - - // Tell if applied update. - return shouldUpdate; - } - - /** - * Determines if the change in compatible units of one group are okay with another group. - * Warns admin of changes and returns true if the changes should happen. - * @param gid The group that has a change in compatible units. - * @param parentGroupIds The parent groups' ids of that group. - * @param groupsState The local group state to use. - * @returns true if change fine or if admin agreed. false if admin does not or the change is an issue. - */ - function validateGroupPostAddChild(gid: number, parentGroupIds: number[], groupsState: any): boolean { - // This will hold the overall message for the admin alert. - let msg = ''; - // Tells if the change should be cancelled. - let cancel = false; - // We check the group being edited and all parent groups for changes in default graphic unit. - for (const groupId of [...parentGroupIds, gid]) { - // Use the edit group since want the current values for deepMeters for comparison. - const parentGroup = editGroupsState[groupId]; - // Get parent's compatible units - const parentCompatibleUnits = unitsCompatibleWithMeters(new Set(parentGroup.deepMeters)); - // Get compatibility change case when add this group to its parent. - const compatibilityChangeCase = getCompatibilityChangeCase(parentCompatibleUnits, gid, DataType.Group, - parentGroup.defaultGraphicUnit, groupsState[groupId].deepMeters); - switch (compatibilityChangeCase) { - case GroupCase.NoCompatibleUnits: - // The group has no compatible units so cannot do this. - msg += `${translate('group')} "${parentGroup.name}" ${translate('group.edit.nocompatible')}\n`; - cancel = true; - break; - - case GroupCase.LostDefaultGraphicUnit: - // The group has fewer compatible units and one of the lost ones is the default graphic unit. - msg += `${translate('group')} "${parentGroup.name}" ${translate('group.edit.nounit')}\n`; - // The current default graphic unit is no longer valid so make it no unit. - groupsState[groupId].defaultGraphicUnit = -99; - break; - - case GroupCase.LostCompatibleUnits: - // The group has fewer compatible units but the default graphic unit is still allowed. - msg += `${translate('group')} "${parentGroup.name}" ${translate('group.edit.changed')}\n`; - break; - - // Case NoChange requires no message. - } - } - if (msg !== '') { - // There is a message to display to the user. - if (cancel) { - // If cancel is true, doesn't allow the admin to apply changes. - msg += `\n${translate('group.edit.cancelled')}`; - notifyUser(msg); - } else { - // If msg is not empty, warns the admin and asks if they want to apply changes. - msg += `\n${translate('group.edit.verify')}`; - cancel = !window.confirm(msg); - } - } - return !cancel; - } - - /** - * Handles removing child from a group. - * @param childId The group/meter's id to add to the parent group. - * @param childType Can be group or meter. - * @returns true if change fine or if admin agreed. false if admin does not or the change is an issue. - */ - function removeChildFromGroup(childId: number, childType: DataType): boolean { - // Unlike adding, you do not change the default graphic unit by removing. Thus, you only need to recalculate the - // deep meters and remove this child from the group being edited. - - // Create a deep copy of the edit state before adding the child. We only need some of the state but this is easier. - // This copy is directly changed without using the Redux hooks since it is not used by React. - // This means that changes to the group do not happen put back into the edit state. - // For the record, it was tried to not create the copy and update the edit state for each change. This had - // two issues. First, the next step in this function does not see the change because Redux does not update - // until the next render. Second, and more importantly, the updated state was not showing during the render. - // Why that is the case was unclear because the set value were correct. Given all of this and to make the - // code more similar to add, it is done with a copy. - const tempGroupsState = _.cloneDeep(editGroupsState); - - // Add the child to the group being edited. - if (childType === DataType.Meter) { - // All the children without one being removed. - const newChildren = _.filter(tempGroupsState[groupState.id].childMeters, value => value != childId); - tempGroupsState[groupState.id].childMeters = newChildren; - } else { - // All the children without one being removed. - const newChildren = _.filter(tempGroupsState[groupState.id].childGroups, value => value != childId); - tempGroupsState[groupState.id].childGroups = newChildren; - } - - // The deep meters of any group can change for any group containing the group that just had a meter/group added. - // Since groups can be indirectly included in another group it is hard to know which ones where impacted so - // just redo them all for now. Also do this group since it likely changed. - const groupOk = !Object.values(tempGroupsState).some(group => { - const newDeepMeters = calculateMetersInGroup(group.id, tempGroupsState); - // If the array is empty then there are no child meters nor groups and this is not allowed. - // The change is rejected. - // This should only happen for the group being edited but check for all since easier. - if (newDeepMeters.length === 0) { - // Let the user know. - notifyUser(`${translate('group.edit.empty')}\n\n${translate('group.edit.cancelled')}`); - // Indicate issue and stop processing. - return true; - } else { - // Update the temp deep meters and continue. - tempGroupsState[group.id].deepMeters = newDeepMeters; - return false; - } - }); - - // Only update if the group is okay. - if (groupOk) { - // Update the group. Now, the changes actually happen. - // Done by setting the edit state to the temp state so does not impact other groups - // and what is seen until the admin saves. - // Could limit to only ones changed but just do since local state and easy. - setEditGroupsState(tempGroupsState); - } - // Tells if the edit was accepted. - return groupOk; - } - - /** - * Checks if this group is contained in another group. If so, no delete. - * If not, then continue delete process. - */ - async function validateDelete() { - // Get all parent groups of this group. - const { data: parentGroupIDs = [] } = await store.dispatch(groupsApi.endpoints.getParentIDs.initiate(groupState.id, { subscribe: false })) - - // If there are parents then you cannot delete this group. Notify admin. - if (parentGroupIDs.length !== 0) { - // This will hold the overall message for the admin alert. - let msg = `${translate('group')} "${groupState.name}" ${translate('group.delete.issue')}:\n`; - parentGroupIDs.forEach(groupId => { - msg += `${editGroupsState[groupId].name}\n`; - }) - msg += `\n${translate('group.edit.cancelled')}`; - notifyUser(msg); - } else { - // The group can be deleted. - handleDeleteConfirmationModalOpen(); - } - } - - /** - * Converts the child meters of this group to options for menu sorted by identifier - * @returns sorted SelectOption for child meters of group editing. - */ - function metersToSelectOptions(): SelectOption[] { - // In format for the display component for menu. - const selectedMetersUnsorted: SelectOption[] = []; - groupState.childMeters.forEach(groupId => { - selectedMetersUnsorted.push({ - value: groupId, - label: meterDataById[groupId].identifier - // isDisabled not needed since only used for selected and not display. - } as SelectOption - ); - }); - // Want chosen in sorted order. - return _.sortBy(selectedMetersUnsorted, item => item.label.toLowerCase(), 'asc'); - } - - /** - * Converts the child groups of this group to options for menu sorted by name - * @returns sorted SelectOption for child groups of group editing. - */ - function groupsToSelectOptions(): SelectOption[] { - // In format for the display component for menu. - const selectedGroupsUnsorted: SelectOption[] = []; - groupState.childGroups.forEach(groupId => { - selectedGroupsUnsorted.push({ - value: groupId, - // Use globalGroupsState so see edits in other groups. You would miss an update - // in this group but it cannot be on the menu so that is okay. - label: groupDataById[groupId].name - // isDisabled not needed since only used for selected and not display. - } as SelectOption - ); - }); - // Want chosen in sorted order. - return _.sortBy(selectedGroupsUnsorted, item => item.label.toLowerCase(), 'asc'); - } - - /** - * Converts the child meters of this group to list options sorted by name. - * This is needed for non-admins. Hidden items are not shown but noted in list. - * @returns names of all child meters in sorted order. - */ - function metersToList(): string[] { - // Hold the list for display. - const listedMeters: string[] = []; - // Tells if any meter is not visible to user. - let hasHidden = false; - groupState.childMeters.forEach(meterId => { - const meterIdentifier = meterDataById[meterId].identifier; - // The identifier is null if the meter is not visible to this user. If hidden then do - // not list and otherwise label. - if (meterIdentifier === null) { - hasHidden = true; - } else { - listedMeters.push(meterIdentifier); - } - }); - // Sort for display. Before were sorted by id so not okay here. - listedMeters.sort(); - if (hasHidden) { - // There are hidden meters so note at bottom of list. - listedMeters.push(translate('meter.hidden')); - } - return listedMeters; - } - - /** - * Converts the child meters of this group to list options sorted by name. - * This is needed for non-admins. Hidden items are not shown but noted in list. - * @returns names of all child meters in sorted order. - */ - function groupsToList(): string[] { - const listedGroups: string[] = []; - let hasHidden = false; - groupState.childGroups.forEach(groupId => { - // The name is null if the group is not visible to this user. - // TODO The following line should work but does not (it does for meters). - // The Redux state has the name of hidden groups but it should not. A quick - // attempt to fix did not work as login/out did not clear as expected when - // control what is returned. This needs to be addressed. - // if (groupName !== null) { - // For now, check if the group is displayable. - if (editGroupsState[groupId].displayable) { - listedGroups.push(editGroupsState[groupId].name); - } else { - hasHidden = true; - } - }); - // Sort for display. Before were sorted by id so not okay here. - listedGroups.sort(); - if (hasHidden) { - // There are hidden groups so note at bottom of list. - listedGroups.push(translate('group.hidden')); - } - return listedGroups; - } - - /** - * Converts the deep meters of this group to list options sorted by identifier. - * Hidden items are not shown but noted in list; admins should never see that. - * @returns names of all child meters in sorted order. - */ - function deepMetersToList() { - // Unlike child meter/group, these are lists for all users. - const listedDeepMeters: string[] = []; - let hasHidden = false; - groupState.deepMeters.forEach(meterId => { - const meterIdentifier = meterDataById[meterId].identifier; - if (meterIdentifier === null) { - // The identifier is null if the meter is not visible to this user. - hasHidden = true; - } else { - // If not null then either non-admin can see or you are an admin. - listedDeepMeters.push(meterIdentifier); - } - }); - // Sort for display. - listedDeepMeters.sort(); - if (hasHidden) { - // There are hidden meters so note at bottom of list. - // This should never happen to an admin. - listedDeepMeters.push(translate('meter.hidden')); - } - return listedDeepMeters; - } -} - -/** - * Returns the set of meters ids associated with the groupId. Does full calculation where - * only uses the direct meter and group children. It uses a store passed to it so it can - * be changed without changing the Redux group store. Thus, it directly and recursively gets - * the deep meters of a group. - * @param groupId The groupId. - * @param groupState The group state to use in the calculation. - * @param times The number of times the function has been recursively called. Not passed on first call and only used internally. - * @returns Array of deep children ids of this group or empty array if none/circular dependency. - */ -function calculateMetersInGroup(groupId: number, groupState: any, times: number = 0): number[] { - // The number of times should be set to zero on the first call. Each time add one and assume - // if depth of calls is greater than value then there is a circular dependency and stop to report issue. - // This assumes no site will ever have a group chain of this length which seems safe. - if (++times > 50) { - return []; - } - // Group to get the deep meters for. - const groupToCheck = groupState[groupId] as GroupData; - // Use a set to avoid duplicates. - // The deep meters are the direct child meters of this group plus the direct child meters - // of all included meters, recursively. - // This should reproduce some DB functionality but using local state. - const deepMeters = new Set(groupToCheck.childMeters); - // Loop over all included groups to get its meters. - groupToCheck.childGroups.some(group => { - // Get the deep meters of this group. - const meters = calculateMetersInGroup(group, groupState, times); - if (meters.length === 0) { - // Issue found so stop loop and return empty set. There must be meters if all is okay. - // Clear deep meters so calling function knows there is an issue. - deepMeters.clear(); - // Stops the processing. - return true; - } else { - // Add to set of deep meters for the group checking. - meters.forEach(meter => { deepMeters.add(meter); }); - // Continue loop to process more. - return false; - } - }); - // Create an array of the deep meters of this group and return it. - // It will be empty if there are none. - return Array.from(deepMeters); -} diff --git a/src/client/app/components/groups/GroupViewComponent.tsx b/src/client/app/components/groups/GroupViewComponent.tsx index 1346ac27a..669400040 100644 --- a/src/client/app/components/groups/GroupViewComponent.tsx +++ b/src/client/app/components/groups/GroupViewComponent.tsx @@ -6,22 +6,18 @@ import * as React from 'react'; // Realize that * is already imported from react import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useSelector } from 'react-redux'; import { Button } from 'reactstrap'; import { GroupData } from 'types/redux/groups'; -import { State } from 'types/redux/state'; +import { selectUnitDataById } from '../../redux/api/unitsApi'; +import { useAppSelector } from '../../redux/hooks'; +import { selectIsAdmin } from '../../reducers/currentUser'; import '../../styles/card-page.css'; -import { UnitData } from '../../types/redux/units'; -import { isRoleAdmin } from '../../utils/hasPermissions'; import { noUnitTranslated } from '../../utils/input'; import translate from '../../utils/translate'; -import EditGroupModalComponent from './EditGroupModalComponent'; +import EditGroupModalComponentWIP from './EditGroupModalComponent'; interface GroupViewComponentProps { group: GroupData; - // This isn't used in this component but are passed to the edit component - // This is done to avoid having to recalculate the possible units sets in each view component - possibleGraphicUnits: Set; } /** @@ -29,9 +25,10 @@ interface GroupViewComponentProps { * @param props variables passed in to define * @returns Group info card element */ -export default function GroupViewComponent(props: GroupViewComponentProps) { +export default function GroupViewComponentWIP(props: GroupViewComponentProps) { // Don't check if admin since only an admin is allowed to route to this page. + // Edit Modal Show const [showEditModal, setShowEditModal] = useState(false); @@ -43,14 +40,13 @@ export default function GroupViewComponent(props: GroupViewComponentProps) { setShowEditModal(false); } - // current user state - const currentUser = useSelector((state: State) => state.currentUser.profile); // Check for admin status - const loggedInAsAdmin = (currentUser !== null) && isRoleAdmin(currentUser.role); + const loggedInAsAdmin = useAppSelector(selectIsAdmin); // Set up to display the units associated with the group as the unit identifier. // unit state - const unitState = useSelector((state: State) => state.units.units); + const unitDataById = useAppSelector(selectUnitDataById); + return (
@@ -62,7 +58,7 @@ export default function GroupViewComponent(props: GroupViewComponentProps) { {/* Use meter translation id string since same one wanted. */} {/* This is the default graphic unit associated with the group or no unit if none. */} - {props.group.defaultGraphicUnit === -99 ? ' ' + noUnitTranslated().identifier : ' ' + unitState[props.group.defaultGraphicUnit].identifier} + {props.group.defaultGraphicUnit === -99 ? ' ' + noUnitTranslated().identifier : ' ' + unitDataById[props.group.defaultGraphicUnit].identifier}
{loggedInAsAdmin &&
@@ -81,10 +77,9 @@ export default function GroupViewComponent(props: GroupViewComponentProps) { {loggedInAsAdmin ? : } {/* Creates a child GroupModalEditComponent */} -
diff --git a/src/client/app/components/groups/GroupViewComponentWIP.tsx b/src/client/app/components/groups/GroupViewComponentWIP.tsx deleted file mode 100644 index 2bb55e7ed..000000000 --- a/src/client/app/components/groups/GroupViewComponentWIP.tsx +++ /dev/null @@ -1,88 +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 * as React from 'react'; -// Realize that * is already imported from react -import { useState } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Button } from 'reactstrap'; -import { GroupData } from 'types/redux/groups'; -import { selectUnitDataById } from '../../redux/api/unitsApi'; -import { useAppSelector } from '../../redux/hooks'; -import { selectIsAdmin } from '../../reducers/currentUser'; -import '../../styles/card-page.css'; -import { noUnitTranslated } from '../../utils/input'; -import translate from '../../utils/translate'; -import EditGroupModalComponentWIP from './EditGroupModalComponentWIP'; - -interface GroupViewComponentProps { - group: GroupData; -} - -/** - * Defines the group info card - * @param props variables passed in to define - * @returns Group info card element - */ -export default function GroupViewComponentWIP(props: GroupViewComponentProps) { - // Don't check if admin since only an admin is allowed to route to this page. - - - // Edit Modal Show - const [showEditModal, setShowEditModal] = useState(false); - - const handleShow = () => { - setShowEditModal(true); - } - - const handleClose = () => { - setShowEditModal(false); - } - - // Check for admin status - const loggedInAsAdmin = useAppSelector(selectIsAdmin); - - // Set up to display the units associated with the group as the unit identifier. - // unit state - const unitDataById = useAppSelector(selectUnitDataById); - - - return ( -
- {/* Use identifier-container since similar and groups only have name */} -
- {props.group.name} -
-
- {/* Use meter translation id string since same one wanted. */} - - {/* This is the default graphic unit associated with the group or no unit if none. */} - {props.group.defaultGraphicUnit === -99 ? ' ' + noUnitTranslated().identifier : ' ' + unitDataById[props.group.defaultGraphicUnit].identifier} -
- {loggedInAsAdmin && -
- {translate(`TrueFalseType.${props.group.displayable.toString()}`)} -
- } - {/* Only show first 30 characters so card does not get too big. Should limit to one line */} - {loggedInAsAdmin && -
- {props.group.note?.slice(0, 29)} -
- } -
- - {/* Creates a child GroupModalEditComponent */} - -
-
- ); -} diff --git a/src/client/app/components/groups/GroupsDetailComponent.tsx b/src/client/app/components/groups/GroupsDetailComponent.tsx index dcec4050d..a86c90453 100644 --- a/src/client/app/components/groups/GroupsDetailComponent.tsx +++ b/src/client/app/components/groups/GroupsDetailComponent.tsx @@ -4,17 +4,13 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import TooltipHelpComponent from '../../components/TooltipHelpComponent'; - +import TooltipHelpComponent from '../TooltipHelpComponent'; import { useAppSelector } from '../../redux/hooks'; +import { selectIsAdmin } from '../../reducers/currentUser'; import { selectVisibleMeterAndGroupData } from '../../redux/selectors/adminSelectors'; -import { potentialGraphicUnits } from '../../utils/input'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import CreateGroupModalComponent from './CreateGroupModalComponent'; -import GroupViewComponent from './GroupViewComponent'; -import { GroupData } from 'types/redux/groups'; -import { selectUnitDataById } from '../../redux/api/unitsApi'; -import { selectIsAdmin } from '../../reducers/currentUser'; +import CreateGroupModalComponentWIP from './CreateGroupModalComponent'; +import GroupViewComponentWIP from './GroupViewComponent'; /** * Defines the groups page card view @@ -28,12 +24,7 @@ export default function GroupsDetailComponent() { // We only want displayable groups if non-admins because they still have non-displayable in state. const { visibleGroups } = useAppSelector(state => selectVisibleMeterAndGroupData(state)); - // Units state - const unitDataById = useAppSelector(selectUnitDataById); - - // Possible graphic units to use - const possibleGraphicUnits = potentialGraphicUnits(unitDataById); const titleStyle: React.CSSProperties = { textAlign: 'center' @@ -61,22 +52,16 @@ export default function GroupsDetailComponent() { {isAdmin &&
{/* The actual button for create is inside this component. */} - < CreateGroupModalComponent - possibleGraphicUnits={possibleGraphicUnits} + < CreateGroupModalComponentWIP />
} {
- {/* Create a GroupViewComponent for each groupData in Groups State after sorting by name */} {Object.values(visibleGroups) - .sort((groupA: GroupData, groupB: GroupData) => (groupA.name.toLowerCase() > groupB.name.toLowerCase()) ? 1 : - ((groupB.name.toLowerCase() > groupA.name.toLowerCase()) ? -1 : 0)) - .map(groupData => ( ())}
} diff --git a/src/client/app/components/groups/GroupsDetailComponentWIP.tsx b/src/client/app/components/groups/GroupsDetailComponentWIP.tsx deleted file mode 100644 index ef5c9e546..000000000 --- a/src/client/app/components/groups/GroupsDetailComponentWIP.tsx +++ /dev/null @@ -1,72 +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 * as React from 'react'; -import { FormattedMessage } from 'react-intl'; -import TooltipHelpComponent from '../../components/TooltipHelpComponent'; -import { useAppSelector } from '../../redux/hooks'; -import { selectIsAdmin } from '../../reducers/currentUser'; -import { selectVisibleMeterAndGroupData } from '../../redux/selectors/adminSelectors'; -import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import CreateGroupModalComponentWIP from './CreateGroupModalComponentWIP'; -import GroupViewComponentWIP from './GroupViewComponentWIP'; - -/** - * Defines the groups page card view - * @returns Groups page element - */ -export default function GroupsDetailComponentWIP() { - - // Check for admin status - const isAdmin = useAppSelector(state => selectIsAdmin(state)); - - // We only want displayable groups if non-admins because they still have non-displayable in state. - const { visibleGroups } = useAppSelector(state => selectVisibleMeterAndGroupData(state)); - - - - const titleStyle: React.CSSProperties = { - textAlign: 'center' - }; - - const tooltipStyle = { - display: 'inline-block', - fontSize: '50%', - // Switch help depending if admin or not. - tooltipGroupView: isAdmin ? 'help.admin.groupview' : 'help.groups.groupview' - }; - - return ( -
-
- - -
-

- -
- -
-

- {isAdmin && -
- {/* The actual button for create is inside this component. */} - < CreateGroupModalComponentWIP - /> -
- } - { -
- {Object.values(visibleGroups) - .map(groupData => ())} -
- } -
-
-
- ); -} diff --git a/src/client/app/components/meters/CreateMeterModalComponent.tsx b/src/client/app/components/meters/CreateMeterModalComponent.tsx index 0b418ef27..ccbef9ef3 100644 --- a/src/client/app/components/meters/CreateMeterModalComponent.tsx +++ b/src/client/app/components/meters/CreateMeterModalComponent.tsx @@ -6,193 +6,96 @@ import * as moment from 'moment'; import * as React from 'react'; import { useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useDispatch, useSelector } from 'react-redux'; import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; -import { Dispatch } from 'types/redux/actions'; -import { State } from 'types/redux/state'; -import { addMeter } from '../../actions/meters'; -import TooltipHelpComponent from '../../components/TooltipHelpComponent'; +import TooltipHelpComponent from '../TooltipHelpComponent'; +import { metersApi } from '../../redux/api/metersApi'; +import { useAppSelector } from '../../redux/hooks'; +import { makeSelectGraphicUnitCompatibility, selectDefaultCreateMeterValues } from '../../redux/selectors/adminSelectors'; import '../../styles/modal.css'; import { tooltipBaseStyle } from '../../styles/modalStyle'; -import { ConversionArray } from '../../types/conversionArray'; import { TrueFalseType } from '../../types/items'; -import { MeterTimeSortType, MeterType } from '../../types/redux/meters'; +import { MeterData, MeterTimeSortType, MeterType } from '../../types/redux/meters'; import { UnitData } from '../../types/redux/units'; import { GPSPoint, isValidGPSInput } from '../../utils/calibration'; -import { unitsCompatibleWithUnit } from '../../utils/determineCompatibleUnits'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; import { notifyUser } from '../../utils/input'; import translate from '../../utils/translate'; import TimeZoneSelect from '../TimeZoneSelect'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; +import { showSuccessNotification } from '../../utils/notifications'; // TODO Moved the possible meters/graphic units calculations up to the details component // This was to prevent the calculations from being done on every load, but now requires them to be passed as props -interface CreateMeterModalComponentProps { +export interface CreateMeterModalComponentProps { possibleMeterUnits: Set; possibleGraphicUnits: Set; } /** * Defines the create meter modal form - * @param props Component props * @returns Meter create element */ -export default function CreateMeterModalComponent(props: CreateMeterModalComponentProps) { - - const dispatch: Dispatch = useDispatch(); +export default function CreateMeterModalComponent() { + const [addMeter] = metersApi.endpoints.addMeter.useMutation() // Admin state so can get the default reading frequency. - const adminState = useSelector((state: State) => state.admin) - - const defaultValues = { - id: -99, - identifier: '', - name: '', - area: 0, - enabled: false, - displayable: false, - meterType: '', - url: '', - timeZone: '', - gps: '', - // Defaults of -999 (not to be confused with -99 which is no unit) - // Purely for allowing the default select to be "select a ..." - unitId: -999, - defaultGraphicUnit: -999, - note: '', - cumulative: false, - cumulativeReset: false, - cumulativeResetStart: '', - cumulativeResetEnd: '', - endOnlyTime: false, - readingGap: adminState.defaultMeterReadingGap, - readingVariation: 0, - readingDuplication: 1, - timeSort: MeterTimeSortType.increasing, - reading: 0.0, - startTimestamp: '', - endTimestamp: '', - previousEnd: '', - areaUnit: AreaUnitType.none, - readingFrequency: adminState.defaultMeterReadingFrequency, - minVal: adminState.defaultMeterMinimumValue, - maxVal: adminState.defaultMeterMaximumValue, - minDate: adminState.defaultMeterMinimumDate, - maxDate: adminState.defaultMeterMaximumDate, - maxError: adminState.defaultMeterMaximumErrors, - disableChecks: adminState.defaultMeterDisableChecks - } - - const dropdownsStateDefaults = { - possibleMeterUnits: props.possibleMeterUnits, - possibleGraphicUnits: props.possibleGraphicUnits, - compatibleUnits: props.possibleMeterUnits, - incompatibleUnits: new Set(), - compatibleGraphicUnits: props.possibleGraphicUnits, - incompatibleGraphicUnits: new Set() - } + // Memo'd memoized selector + const selectGraphicUnitCompatibility = React.useMemo(makeSelectGraphicUnitCompatibility, []) + const defaultValues = useAppSelector(selectDefaultCreateMeterValues) /* State */ // To make this consistent with EditUnitModalComponent, we don't pass show and close via props // even this one does have other props. // Modal show const [showModal, setShowModal] = useState(false); - const handleShow = () => setShowModal(true); + // Handlers for each type of input change - const [state, setState] = useState(defaultValues); + const [meterDetails, setMeterDetails] = useState(defaultValues); + const { + incompatibleGraphicUnits, + compatibleGraphicUnits, + compatibleUnits, + incompatibleUnits + // Type assertion due to conflicting GPS Property + } = useAppSelector(state => selectGraphicUnitCompatibility(state, meterDetails as unknown as MeterData)) + const handleShow = () => setShowModal(true); const handleStringChange = (e: React.ChangeEvent) => { - setState({ ...state, [e.target.name]: e.target.value }); + setMeterDetails({ ...meterDetails, [e.target.name]: e.target.value }); } const handleBooleanChange = (e: React.ChangeEvent) => { - setState({ ...state, [e.target.name]: JSON.parse(e.target.value) }); + setMeterDetails({ ...meterDetails, [e.target.name]: JSON.parse(e.target.value) }); } const handleNumberChange = (e: React.ChangeEvent) => { - setState({ ...state, [e.target.name]: Number(e.target.value) }); + setMeterDetails({ ...meterDetails, [e.target.name]: Number(e.target.value) }); } const handleTimeZoneChange = (timeZone: string) => { - setState({ ...state, ['timeZone']: timeZone }); + setMeterDetails({ ...meterDetails, ['timeZone']: timeZone }); } // Dropdowns - const [dropdownsState, setDropdownsState] = useState(dropdownsStateDefaults); - - /* Create Meter Validation: - Name cannot be blank - Area must be positive or zero - If area is nonzero, area unit must be set - Reading Gap must be greater than zero - Reading Variation must be greater than zero - Reading Duplication must be between 1 and 9 - Reading frequency cannot be blank - Unit and Default Graphic Unit must be set (can be to no unit) - Meter type must be set - If displayable is true and unitId is set to -99, warn admin - Mininum Value cannot bigger than Maximum Value - Minimum Value and Maximum Value must be between valid input - Minimum Date and Maximum cannot be blank - Minimum Date cannot be after Maximum Date - Minimum Date and Maximum Value must be between valid input - Maximum No of Error must be between 0 and valid input - */ + const [selectedUnitId, setSelectedUnitId] = useState(false); + const [selectedGraphicId, setSelectedGraphicId] = useState(false); + const [validMeter, setValidMeter] = useState(false); - const MIN_VAL = Number.MIN_SAFE_INTEGER; - const MAX_VAL = Number.MAX_SAFE_INTEGER; - const MIN_DATE_MOMENT = moment(0).utc(); - const MAX_DATE_MOMENT = moment(0).utc().add(5000, 'years'); - const MIN_DATE = MIN_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); - const MAX_DATE = MAX_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); - const MAX_ERRORS = 75; + useEffect(() => { - setValidMeter( - state.name !== '' && - (state.area === 0 || (state.area > 0 && state.areaUnit !== AreaUnitType.none)) && - state.readingGap >= 0 && - state.readingVariation >= 0 && - (state.readingDuplication >= 1 && state.readingDuplication <= 9) && - state.readingFrequency !== '' && - state.unitId !== -999 && - state.defaultGraphicUnit !== -999 && - state.meterType !== '' && - state.minVal >= MIN_VAL && - state.minVal <= state.maxVal && - state.maxVal <= MAX_VAL && - moment(state.minDate).isValid() && - moment(state.maxDate).isValid() && - moment(state.minDate).isSameOrAfter(MIN_DATE_MOMENT) && - moment(state.minDate).isSameOrBefore(moment(state.maxDate)) && - moment(state.maxDate).isSameOrBefore(MAX_DATE_MOMENT) && - (state.maxError >= 0 && state.maxError <= MAX_ERRORS) - ); - }, [ - state.area, - state.name, - state.readingGap, - state.readingVariation, - state.readingDuplication, - state.areaUnit, - state.readingFrequency, - state.unitId, - state.defaultGraphicUnit, - state.meterType, - state.minVal, - state.maxVal, - state.minDate, - state.maxDate, - state.maxError - ]); + // Conflicting GPS point type so type assertions + setValidMeter(isValidCreateMeter(meterDetails as unknown as MeterData)); + }, [meterDetails]); /* End State */ // Reset the state to default values // This would also benefit from a single state changing function for all state const resetState = () => { - setState(defaultValues); + setMeterDetails(defaultValues); + setSelectedGraphicId(false) + setSelectedUnitId(false) } const handleClose = () => { @@ -204,7 +107,7 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone // that create starts from an empty template. // Submit - const handleSubmit = () => { + const handleSubmit = async () => { // Close modal first to avoid repeat clicks setShowModal(false); @@ -213,12 +116,9 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone // TODO Maybe should do as a single popup? - // Set default identifier as name if left blank - state.identifier = (!state.identifier || state.identifier.length === 0) ? state.name : state.identifier; - // Check GPS entered. // Validate GPS is okay and take from string to GPSPoint to submit. - const gpsInput = state.gps; + const gpsInput = meterDetails.gps; let gps: GPSPoint | null = null; const latitudeIndex = 0; const longitudeIndex = 1; @@ -227,14 +127,13 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone if (typeof gpsInput === 'string') { if (isValidGPSInput(gpsInput)) { // Clearly gpsInput is a string but TS complains about the split so cast. - const gpsValues = (gpsInput as string).split(',').map((value: string) => parseFloat(value)); + const gpsValues = gpsInput.split(',').map(value => parseFloat(value)); // It is valid and needs to be in this format for routing. gps = { longitude: gpsValues[longitudeIndex], latitude: gpsValues[latitudeIndex] }; - // gpsInput must be of type string but TS does not think so so cast. - } else if ((gpsInput as string).length !== 0) { + } else if (gpsInput.length !== 0) { // GPS not okay. Only true if some input. // TODO isValidGPSInput currently pops up an alert so not doing it here, may change // so leaving code commented out. @@ -247,94 +146,34 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone // The input passed validation. // The default value for timeZone is an empty string but that should be null for DB. // See below for usage of timeZoneValue. - const timeZoneValue = (state.timeZone == '' ? null : state.timeZone); // GPS may have been updated so create updated state to submit. - const submitState = { ...state, gps: gps, timeZone: timeZoneValue }; + const submitState = { + ...meterDetails, + gps: gps, + timeZone: (meterDetails.timeZone == '' ? null : meterDetails.timeZone), + // Set default identifier as name if left blank + identifier: !meterDetails.identifier || meterDetails.identifier.length === 0 ? meterDetails.name : meterDetails.identifier + }; // Submit new meter if checks where ok. - dispatch(addMeter(submitState)); - resetState(); + // Attempt to add meter to database + addMeter(submitState) + .unwrap() + .then(() => { + // if successful, the mutation will invalidate existing cache causing all meter details to be retrieved + showSuccessNotification(translate('meter.successfully.create.meter')); + resetState(); + }) + .catch(err => { + // TODO Better way than popup with React but want to stay so user can read/copy. + console.log(err) + window.alert(translate('meter.failed.to.create.meter') + '"' + err.data + '"'); + }) } else { // Tell user that not going to update due to input issues. notifyUser(translate('meter.input.error')); } }; - // Update compatible units and graphic units set. - // This works the same as Edit with a single useEffect. See Edit for an explanation but note - // those issues were never seen with create. - useEffect(() => { - // Graphic units compatible with currently selected unit - const compatibleGraphicUnits = new Set(); - // Graphic units incompatible with currently selected unit - const incompatibleGraphicUnits = new Set(); - // If a unit has been selected that is not 'no unit' - if (state.unitId != -999 && state.unitId != -99) { - // Find all units compatible with the selected unit - const unitsCompatibleWithSelectedUnit = unitsCompatibleWithUnit(state.unitId); - dropdownsState.possibleGraphicUnits.forEach(unit => { - // If current graphic unit exists in the set of compatible graphic units OR if the current graphic unit is 'no unit' - if (unitsCompatibleWithSelectedUnit.has(unit.id) || unit.id == -99) { - compatibleGraphicUnits.add(unit); - } - else { - incompatibleGraphicUnits.add(unit); - } - }); - } - // No unit is selected - else { - // OED does not allow a default graphic unit if there is no unit so it must be -99. - // We don't reset if it is currently -999 since want user to select something. - state.defaultGraphicUnit = state.defaultGraphicUnit === -999 ? -999 : -99; - dropdownsState.possibleGraphicUnits.forEach(unit => { - // Only -99 is allowed. - if (unit.id == -99) { - compatibleGraphicUnits.add(unit); - } - else { - incompatibleGraphicUnits.add(unit); - } - }); - } - - // Units compatible with currently selected graphic unit - let compatibleUnits = new Set(); - // Units incompatible with currently selected graphic unit - const incompatibleUnits = new Set(); - // If a default graphic unit has been selected that is not 'no unit' - if (state.defaultGraphicUnit != -999 && state.defaultGraphicUnit != -99) { - // Find all units compatible with the selected graphic unit - dropdownsState.possibleMeterUnits.forEach(unit => { - // Graphic units compatible with the current meter unit - const compatibleGraphicUnitsForUnit = unitsCompatibleWithUnit(unit.id); - // If the currently selected default graphic unit exists in the set of graphic units compatible with the current meter unit - // Also add the 'no unit' unit - if (compatibleGraphicUnitsForUnit.has(state.defaultGraphicUnit) || unit.id == -99) { - // add the current meter unit to the list of compatible units - compatibleUnits.add(unit); - } - else { - // add the current meter unit to the list of incompatible units - incompatibleUnits.add(unit); - } - }); - } - // No default graphic unit is selected - else { - // All units are compatible - compatibleUnits = new Set(dropdownsState.possibleMeterUnits); - } - setDropdownsState({ - ...dropdownsState, - // The new set helps avoid repaints. - compatibleGraphicUnits: new Set(compatibleGraphicUnits), - incompatibleGraphicUnits: new Set(incompatibleGraphicUnits), - compatibleUnits: new Set(compatibleUnits), - incompatibleUnits: new Set(incompatibleUnits) - }); - // If either unit or the status of pik changes then this needs to be done. - // pik is needed since the compatible units is not correct until pik is available. - }, [state.unitId, state.defaultGraphicUnit, ConversionArray.pikAvailable()]); const tooltipStyle = { ...tooltipBaseStyle, @@ -347,7 +186,7 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone // The DB stores null for no choice and TimeZoneSelect expects null for no choice. // To get around this, a new variable is used for the menu options so it can have // both values where the empty string is converted to null. - const timeZoneValue: string | null = (state.timeZone === '' ? null : state.timeZone); + const timeZoneValue: string | null = (meterDetails.timeZone === '' ? null : meterDetails.timeZone); return ( <> @@ -375,7 +214,7 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone type='text' autoComplete='on' onChange={e => handleStringChange(e)} - value={state.identifier} /> + value={meterDetails.identifier} /> {/* Name input */} @@ -386,8 +225,8 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone type='text' autoComplete='on' onChange={e => handleStringChange(e)} - required value={state.name} - invalid={state.name === ''} /> + required value={meterDetails.name} + invalid={meterDetails.name === ''} /> @@ -401,20 +240,23 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone id='unitId' name='unitId' type='select' - value={state.unitId} - onChange={e => handleNumberChange(e)} - invalid={state.unitId === -999}> + value={selectedUnitId ? meterDetails.unitId : -999} + onChange={e => { + handleNumberChange(e) + setSelectedUnitId(true) + }} + invalid={!selectedUnitId}> {} - {Array.from(dropdownsState.compatibleUnits).map(unit => { + {Array.from(compatibleUnits).map(unit => { return () })} - {Array.from(dropdownsState.incompatibleUnits).map(unit => { + {Array.from(incompatibleUnits).map(unit => { return () })} @@ -427,20 +269,24 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone id='defaultGraphicUnit' name='defaultGraphicUnit' type='select' - value={state.defaultGraphicUnit} - onChange={e => handleNumberChange(e)} - invalid={state.defaultGraphicUnit === -999}> + value={selectedGraphicId ? meterDetails.defaultGraphicUnit : -999} + invalid={!selectedGraphicId} + onChange={e => { + handleNumberChange(e) + setSelectedGraphicId(true) + }} + > {} - {Array.from(dropdownsState.compatibleGraphicUnits).map(unit => { + {Array.from(compatibleGraphicUnits).map(unit => { return () })} - {Array.from(dropdownsState.incompatibleGraphicUnits).map(unit => { + {Array.from(incompatibleGraphicUnits).map(unit => { return () })} @@ -455,7 +301,7 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone id='enabled' name='enabled' type='select' - value={state.enabled.toString()} + value={meterDetails.enabled.toString()} onChange={e => handleBooleanChange(e)}> {Object.keys(TrueFalseType).map(key => { return () @@ -469,9 +315,9 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone id='displayable' name='displayable' type='select' - value={state.displayable.toString()} + value={meterDetails.displayable.toString()} onChange={e => handleBooleanChange(e)} - invalid={state.displayable && state.unitId === -99}> + invalid={meterDetails.displayable && meterDetails.unitId === -99}> {Object.keys(TrueFalseType).map(key => { return () })} @@ -489,14 +335,14 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone id='meterType' name='meterType' type='select' - value={state.meterType} + value={meterDetails.meterType} onChange={e => handleStringChange(e)} - invalid={state.meterType === ''}> + invalid={meterDetails.meterType === ''}> {/* The default value is a blank string so then tell user to select one. */} {} @@ -516,8 +362,8 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone type='text' autoComplete='on' onChange={e => handleStringChange(e)} - value={state.readingFrequency} - invalid={state.readingFrequency === ''} /> + value={meterDetails.readingFrequency} + invalid={meterDetails.readingFrequency === ''} /> @@ -533,7 +379,7 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone type='text' autoComplete='off' onChange={e => handleStringChange(e)} - value={state.url} /> + value={meterDetails.url} /> {/* GPS input */} @@ -543,7 +389,7 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone name='gps' type='text' onChange={e => handleStringChange(e)} - value={state.gps} /> + value={meterDetails.gps} /> @@ -555,9 +401,9 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone name='area' type='number' min='0' - defaultValue={state.area} + defaultValue={meterDetails.area} onChange={e => handleNumberChange(e)} - invalid={state.area < 0} /> + invalid={meterDetails.area < 0} /> @@ -569,9 +415,9 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone id='areaUnit' name='areaUnit' type='select' - value={state.areaUnit} + value={meterDetails.areaUnit} onChange={e => handleStringChange(e)} - invalid={state.area > 0 && state.areaUnit === AreaUnitType.none}> + invalid={meterDetails.area > 0 && meterDetails.areaUnit === AreaUnitType.none}> {Object.keys(AreaUnitType).map(key => { return () })} @@ -589,7 +435,7 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone name='note' type='textarea' onChange={e => handleStringChange(e)} - value={state.note} + value={meterDetails.note} placeholder='Note' /> @@ -600,7 +446,7 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone id='cumulative' name='cumulative' type='select' - value={state.cumulative.toString()} + value={meterDetails.cumulative.toString()} onChange={e => handleBooleanChange(e)}> {Object.keys(TrueFalseType).map(key => { return () @@ -614,7 +460,7 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone id='cumulativeReset' name='cumulativeReset' type='select' - value={state.cumulativeReset.toString()} + value={meterDetails.cumulativeReset.toString()} onChange={e => handleBooleanChange(e)}> {Object.keys(TrueFalseType).map(key => { return () @@ -632,7 +478,7 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone type='text' autoComplete='off' onChange={e => handleStringChange(e)} - value={state.cumulativeResetStart} + value={meterDetails.cumulativeResetStart} placeholder='HH:MM:SS' /> {/* cumulativeResetEnd input */} @@ -644,7 +490,7 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone type='text' autoComplete='off' onChange={e => handleStringChange(e)} - value={state.cumulativeResetEnd} + value={meterDetails.cumulativeResetEnd} placeholder='HH:MM:SS' /> @@ -656,7 +502,7 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone id='endOnlyTime' name='endOnlyTime' type='select' - value={state.endOnlyTime.toString()} + value={meterDetails.endOnlyTime.toString()} onChange={e => handleBooleanChange(e)}> {Object.keys(TrueFalseType).map(key => { return () @@ -672,8 +518,8 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone type='number' onChange={e => handleNumberChange(e)} min='0' - defaultValue={state.readingGap} - invalid={state?.readingGap < 0} /> + defaultValue={meterDetails.readingGap} + invalid={meterDetails?.readingGap < 0} /> @@ -689,8 +535,8 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone type='number' onChange={e => handleNumberChange(e)} min='0' - defaultValue={state.readingVariation} - invalid={state?.readingVariation < 0} /> + defaultValue={meterDetails.readingVariation} + invalid={meterDetails?.readingVariation < 0} /> @@ -706,8 +552,8 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone step='1' min='1' max='9' - defaultValue={state.readingDuplication} - invalid={state?.readingDuplication < 1 || state?.readingDuplication > 9} /> + defaultValue={meterDetails.readingDuplication} + invalid={meterDetails?.readingDuplication < 1 || meterDetails?.readingDuplication > 9} /> @@ -721,7 +567,7 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone id='timeSort' name='timeSort' type='select' - value={state.timeSort} + value={meterDetails.timeSort} onChange={e => handleStringChange(e)}> {Object.keys(MeterTimeSortType).map(key => { // This is a bit of a hack but it should work fine. The TypeSortTypes and MeterTimeSortType should be in sync. @@ -746,11 +592,11 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone type='number' onChange={e => handleNumberChange(e)} min={MIN_VAL} - max={state.maxVal} - defaultValue={state.minVal} - invalid={state?.minVal < MIN_VAL || state?.minVal > state?.maxVal} /> + max={meterDetails.maxVal} + defaultValue={meterDetails.minVal} + invalid={meterDetails?.minVal < MIN_VAL || meterDetails?.minVal > meterDetails?.maxVal} /> - + {/* maxVal input */} @@ -761,12 +607,12 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone name='maxVal' type='number' onChange={e => handleNumberChange(e)} - min={state.minVal} + min={meterDetails.minVal} max={MAX_VAL} - defaultValue={state.maxVal} - invalid={state?.maxVal > MAX_VAL || state?.minVal > state?.maxVal} /> + defaultValue={meterDetails.maxVal} + invalid={meterDetails?.maxVal > MAX_VAL || meterDetails?.minVal > meterDetails?.maxVal} /> - + @@ -781,12 +627,12 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone autoComplete='on' onChange={e => handleStringChange(e)} placeholder='YYYY-MM-DD HH:MM:SS' - required value={state.minDate} - invalid={!moment(state.minDate).isValid() - || !moment(state.minDate).isSameOrAfter(MIN_DATE_MOMENT) - || !moment(state.minDate).isSameOrBefore(moment(state.maxDate))} /> + required value={meterDetails.minDate} + invalid={!moment(meterDetails.minDate).isValid() + || !moment(meterDetails.minDate).isSameOrAfter(MIN_DATE_MOMENT) + || !moment(meterDetails.minDate).isSameOrBefore(moment(meterDetails.maxDate))} /> - + {/* maxDate input */} @@ -799,12 +645,12 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone autoComplete='on' placeholder='YYYY-MM-DD HH:MM:SS' onChange={e => handleStringChange(e)} - required value={state.maxDate} - invalid={!moment(state.maxDate).isValid() - || !moment(state.maxDate).isSameOrBefore(MAX_DATE_MOMENT) - || !moment(state.maxDate).isSameOrAfter(moment(state.minDate))} /> + required value={meterDetails.maxDate} + invalid={!moment(meterDetails.maxDate).isValid() + || !moment(meterDetails.maxDate).isSameOrBefore(MAX_DATE_MOMENT) + || !moment(meterDetails.maxDate).isSameOrAfter(moment(meterDetails.minDate))} /> - + @@ -820,8 +666,8 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone onChange={e => handleNumberChange(e)} min='0' max={MAX_ERRORS} - defaultValue={state.maxError} - invalid={state?.maxError > MAX_ERRORS || state?.maxError < 0} /> + defaultValue={meterDetails.maxError} + invalid={meterDetails?.maxError > MAX_ERRORS || meterDetails?.maxError < 0} /> @@ -832,7 +678,7 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone id='disableChecks' name='disableChecks' type='select' - defaultValue={state.disableChecks?.toString()} + defaultValue={meterDetails.disableChecks?.toString()} onChange={e => handleBooleanChange(e)}> {Object.keys(TrueFalseType).map(key => { return () @@ -849,7 +695,7 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone name='reading' type='number' onChange={e => handleNumberChange(e)} - defaultValue={state.reading} /> + defaultValue={meterDetails.reading} /> {/* startTimestamp input */} @@ -861,7 +707,7 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone autoComplete='on' onChange={e => handleStringChange(e)} placeholder='YYYY-MM-DD HH:MM:SS' - value={state.startTimestamp} /> + value={meterDetails.startTimestamp} /> @@ -875,7 +721,7 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone autoComplete='on' onChange={e => handleStringChange(e)} placeholder='YYYY-MM-DD HH:MM:SS' - value={state.endTimestamp} /> + value={meterDetails.endTimestamp} /> {/* previousEnd input */} @@ -887,7 +733,7 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone autoComplete='on' onChange={e => handleStringChange(e)} placeholder='YYYY-MM-DD HH:MM:SS' - value={state.previousEnd} /> + value={meterDetails.previousEnd} /> @@ -905,3 +751,54 @@ export default function CreateMeterModalComponent(props: CreateMeterModalCompone ); } + + +/* Create Meter Validation: + Name cannot be blank + Area must be positive or zero + If area is nonzero, area unit must be set + Reading Gap must be greater than zero + Reading Variation must be greater than zero + Reading Duplication must be between 1 and 9 + Reading frequency cannot be blank + Unit and Default Graphic Unit must be set (can be to no unit) + Meter type must be set + If displayable is true and unitId is set to -99, warn admin + Minimum Value cannot bigger than Maximum Value + Minimum Value and Maximum Value must be between valid input + Minimum Date and Maximum cannot be blank + Minimum Date cannot be after Maximum Date + Minimum Date and Maximum Value must be between valid input + Maximum No of Error must be between 0 and valid input +*/ +const isValidCreateMeter = (meterDetails: MeterData) => { + return meterDetails.name !== '' && + (meterDetails.area === 0 || (meterDetails.area > 0 && meterDetails.areaUnit !== AreaUnitType.none)) && + meterDetails.readingGap >= 0 && + meterDetails.readingVariation >= 0 && + (meterDetails.readingDuplication >= 1 && meterDetails.readingDuplication <= 9) && + meterDetails.readingFrequency !== '' && + meterDetails.unitId !== -99 && + meterDetails.defaultGraphicUnit !== -99 && + meterDetails.meterType !== '' && + meterDetails.minVal >= MIN_VAL && + meterDetails.minVal <= meterDetails.maxVal && + meterDetails.maxVal <= MAX_VAL && + moment(meterDetails.minDate).isValid() && + moment(meterDetails.maxDate).isValid() && + moment(meterDetails.minDate).isSameOrAfter(MIN_DATE_MOMENT) && + moment(meterDetails.minDate).isSameOrBefore(moment(meterDetails.maxDate)) && + moment(meterDetails.maxDate).isSameOrBefore(MAX_DATE_MOMENT) && + (meterDetails.maxError >= 0 && meterDetails.maxError <= MAX_ERRORS) +} + + + + +const MIN_VAL = Number.MIN_SAFE_INTEGER; +const MAX_VAL = Number.MAX_SAFE_INTEGER; +const MIN_DATE_MOMENT = moment(0).utc(); +const MAX_DATE_MOMENT = moment(0).utc().add(5000, 'years'); +const MIN_DATE = MIN_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); +const MAX_DATE = MAX_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); +const MAX_ERRORS = 75; diff --git a/src/client/app/components/meters/CreateMeterModalComponentWIP.tsx b/src/client/app/components/meters/CreateMeterModalComponentWIP.tsx deleted file mode 100644 index 2a1544c93..000000000 --- a/src/client/app/components/meters/CreateMeterModalComponentWIP.tsx +++ /dev/null @@ -1,804 +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 * as moment from 'moment'; -import * as React from 'react'; -import { useEffect, useState } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; -import TooltipHelpComponent from '../../components/TooltipHelpComponent'; -import { metersApi } from '../../redux/api/metersApi'; -import { useAppSelector } from '../../redux/hooks'; -import { makeSelectGraphicUnitCompatibility, selectDefaultCreateMeterValues } from '../../redux/selectors/adminSelectors'; -import '../../styles/modal.css'; -import { tooltipBaseStyle } from '../../styles/modalStyle'; -import { TrueFalseType } from '../../types/items'; -import { MeterData, MeterTimeSortType, MeterType } from '../../types/redux/meters'; -import { UnitData } from '../../types/redux/units'; -import { GPSPoint, isValidGPSInput } from '../../utils/calibration'; -import { AreaUnitType } from '../../utils/getAreaUnitConversion'; -import { notifyUser } from '../../utils/input'; -import translate from '../../utils/translate'; -import TimeZoneSelect from '../TimeZoneSelect'; -import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import { showSuccessNotification } from '../../utils/notifications'; - - -// TODO Moved the possible meters/graphic units calculations up to the details component -// This was to prevent the calculations from being done on every load, but now requires them to be passed as props -export interface CreateMeterModalComponentProps { - possibleMeterUnits: Set; - possibleGraphicUnits: Set; -} - -/** - * Defines the create meter modal form - * @returns Meter create element - */ -export default function CreateMeterModalComponent() { - - const [addMeter] = metersApi.endpoints.addMeter.useMutation() - // Admin state so can get the default reading frequency. - // Memo'd memoized selector - const selectGraphicUnitCompatibility = React.useMemo(makeSelectGraphicUnitCompatibility, []) - const defaultValues = useAppSelector(selectDefaultCreateMeterValues) - - /* State */ - // To make this consistent with EditUnitModalComponent, we don't pass show and close via props - // even this one does have other props. - // Modal show - const [showModal, setShowModal] = useState(false); - - - // Handlers for each type of input change - const [meterDetails, setMeterDetails] = useState(defaultValues); - const { - incompatibleGraphicUnits, - compatibleGraphicUnits, - compatibleUnits, - incompatibleUnits - // Type assertion due to conflicting GPS Property - } = useAppSelector(state => selectGraphicUnitCompatibility(state, meterDetails as unknown as MeterData)) - const handleShow = () => setShowModal(true); - - const handleStringChange = (e: React.ChangeEvent) => { - setMeterDetails({ ...meterDetails, [e.target.name]: e.target.value }); - } - - const handleBooleanChange = (e: React.ChangeEvent) => { - setMeterDetails({ ...meterDetails, [e.target.name]: JSON.parse(e.target.value) }); - } - - const handleNumberChange = (e: React.ChangeEvent) => { - setMeterDetails({ ...meterDetails, [e.target.name]: Number(e.target.value) }); - } - - const handleTimeZoneChange = (timeZone: string) => { - setMeterDetails({ ...meterDetails, ['timeZone']: timeZone }); - } - - // Dropdowns - const [selectedUnitId, setSelectedUnitId] = useState(false); - const [selectedGraphicId, setSelectedGraphicId] = useState(false); - - const [validMeter, setValidMeter] = useState(false); - - useEffect(() => { - // Conflicting GPS point type so type assertions - setValidMeter(isValidCreateMeter(meterDetails as unknown as MeterData)); - }, [meterDetails]); - /* End State */ - - // Reset the state to default values - // This would also benefit from a single state changing function for all state - const resetState = () => { - setMeterDetails(defaultValues); - setSelectedGraphicId(false) - setSelectedUnitId(false) - } - - const handleClose = () => { - setShowModal(false); - resetState(); - }; - - // Unlike edit, we decided to discard and inputs when you choose to leave the page. The reasoning is - // that create starts from an empty template. - - // Submit - const handleSubmit = async () => { - // Close modal first to avoid repeat clicks - setShowModal(false); - - // true if inputted values are okay. Then can submit. - let inputOk = true; - - // TODO Maybe should do as a single popup? - - // Check GPS entered. - // Validate GPS is okay and take from string to GPSPoint to submit. - const gpsInput = meterDetails.gps; - let gps: GPSPoint | null = null; - const latitudeIndex = 0; - const longitudeIndex = 1; - // If the user input a value then gpsInput should be a string. - // null came from the DB and it is okay to just leave it - Not a string. - if (typeof gpsInput === 'string') { - if (isValidGPSInput(gpsInput)) { - // Clearly gpsInput is a string but TS complains about the split so cast. - const gpsValues = gpsInput.split(',').map(value => parseFloat(value)); - // It is valid and needs to be in this format for routing. - gps = { - longitude: gpsValues[longitudeIndex], - latitude: gpsValues[latitudeIndex] - }; - } else if (gpsInput.length !== 0) { - // GPS not okay. Only true if some input. - // TODO isValidGPSInput currently pops up an alert so not doing it here, may change - // so leaving code commented out. - // notifyUser(translate('input.gps.range') + state.gps + '.'); - inputOk = false; - } - } - - if (inputOk) { - // The input passed validation. - // The default value for timeZone is an empty string but that should be null for DB. - // See below for usage of timeZoneValue. - // GPS may have been updated so create updated state to submit. - const submitState = { - ...meterDetails, - gps: gps, - timeZone: (meterDetails.timeZone == '' ? null : meterDetails.timeZone), - // Set default identifier as name if left blank - identifier: !meterDetails.identifier || meterDetails.identifier.length === 0 ? meterDetails.name : meterDetails.identifier - }; - // Submit new meter if checks where ok. - // Attempt to add meter to database - addMeter(submitState) - .unwrap() - .then(() => { - // if successful, the mutation will invalidate existing cache causing all meter details to be retrieved - showSuccessNotification(translate('meter.successfully.create.meter')); - resetState(); - }) - .catch(err => { - // TODO Better way than popup with React but want to stay so user can read/copy. - console.log(err) - window.alert(translate('meter.failed.to.create.meter') + '"' + err.data + '"'); - }) - } else { - // Tell user that not going to update due to input issues. - notifyUser(translate('meter.input.error')); - } - }; - - - const tooltipStyle = { - ...tooltipBaseStyle, - // Only an admin can create a meter. - tooltipCreateMeterView: 'help.admin.metercreate' - }; - - // This is a bit of a hack. The defaultValues set the time zone to the empty string. - // This makes the type a string and no easy way was found to allow null too. - // The DB stores null for no choice and TimeZoneSelect expects null for no choice. - // To get around this, a new variable is used for the menu options so it can have - // both values where the empty string is converted to null. - const timeZoneValue: string | null = (meterDetails.timeZone === '' ? null : meterDetails.timeZone); - - return ( - <> - {/* Show modal button */} - - - - - -
- -
-
- {/* when any of the Meter values are changed call one of the functions. */} - - - {/* Identifier input */} - - - handleStringChange(e)} - value={meterDetails.identifier} /> - - {/* Name input */} - - - handleStringChange(e)} - required value={meterDetails.name} - invalid={meterDetails.name === ''} /> - - - - - - - {/* meter unit input */} - - - { - handleNumberChange(e) - setSelectedUnitId(true) - }} - invalid={!selectedUnitId}> - {} - {Array.from(compatibleUnits).map(unit => { - return () - })} - {Array.from(incompatibleUnits).map(unit => { - return () - })} - - - - {/* default graphic unit input */} - - - { - handleNumberChange(e) - setSelectedGraphicId(true) - }} - > - {} - {Array.from(compatibleGraphicUnits).map(unit => { - return () - })} - {Array.from(incompatibleGraphicUnits).map(unit => { - return () - })} - - - - - - {/* Enabled input */} - - - handleBooleanChange(e)}> - {Object.keys(TrueFalseType).map(key => { - return () - })} - - - {/* Displayable input */} - - - handleBooleanChange(e)} - invalid={meterDetails.displayable && meterDetails.unitId === -99}> - {Object.keys(TrueFalseType).map(key => { - return () - })} - - - - - - - - {/* Meter type input */} - - - handleStringChange(e)} - invalid={meterDetails.meterType === ''}> - {/* The default value is a blank string so then tell user to select one. */} - {} - {/* The dB expects lowercase. */} - {Object.keys(MeterType).map(key => { - return () - })} - - - - {/* Meter reading frequency */} - - - handleStringChange(e)} - value={meterDetails.readingFrequency} - invalid={meterDetails.readingFrequency === ''} /> - - - - - - - {/* URL input */} - - - handleStringChange(e)} - value={meterDetails.url} /> - - {/* GPS input */} - - - handleStringChange(e)} - value={meterDetails.gps} /> - - - - {/* Area input */} - - - handleNumberChange(e)} - invalid={meterDetails.area < 0} /> - - - - - {/* meter area unit input */} - - - handleStringChange(e)} - invalid={meterDetails.area > 0 && meterDetails.areaUnit === AreaUnitType.none}> - {Object.keys(AreaUnitType).map(key => { - return () - })} - - - - - - - {/* note input */} - - - handleStringChange(e)} - value={meterDetails.note} - placeholder='Note' /> - - - {/* cumulative input */} - - - handleBooleanChange(e)}> - {Object.keys(TrueFalseType).map(key => { - return () - })} - - - {/* cumulativeReset input */} - - - handleBooleanChange(e)}> - {Object.keys(TrueFalseType).map(key => { - return () - })} - - - - - {/* cumulativeResetStart input */} - - - handleStringChange(e)} - value={meterDetails.cumulativeResetStart} - placeholder='HH:MM:SS' /> - - {/* cumulativeResetEnd input */} - - - handleStringChange(e)} - value={meterDetails.cumulativeResetEnd} - placeholder='HH:MM:SS' /> - - - - {/* endOnlyTime input */} - - - handleBooleanChange(e)}> - {Object.keys(TrueFalseType).map(key => { - return () - })} - - - {/* readingGap input */} - - - handleNumberChange(e)} - min='0' - defaultValue={meterDetails.readingGap} - invalid={meterDetails?.readingGap < 0} /> - - - - - - - {/* readingVariation input */} - - - handleNumberChange(e)} - min='0' - defaultValue={meterDetails.readingVariation} - invalid={meterDetails?.readingVariation < 0} /> - - - - - {/* readingDuplication input */} - - - handleNumberChange(e)} - step='1' - min='1' - max='9' - defaultValue={meterDetails.readingDuplication} - invalid={meterDetails?.readingDuplication < 1 || meterDetails?.readingDuplication > 9} /> - - - - - - - {/* timeSort input */} - - - handleStringChange(e)}> - {Object.keys(MeterTimeSortType).map(key => { - // This is a bit of a hack but it should work fine. The TypeSortTypes and MeterTimeSortType should be in sync. - // The translation is on the former so we use that enum name there but loop on the other to get the value desired. - return () - })} - - - {/* Timezone input */} - - - handleTimeZoneChange(timeZone)} /> - - - - {/* minVal input */} - - - handleNumberChange(e)} - min={MIN_VAL} - max={meterDetails.maxVal} - defaultValue={meterDetails.minVal} - invalid={meterDetails?.minVal < MIN_VAL || meterDetails?.minVal > meterDetails?.maxVal} /> - - - - - {/* maxVal input */} - - - handleNumberChange(e)} - min={meterDetails.minVal} - max={MAX_VAL} - defaultValue={meterDetails.maxVal} - invalid={meterDetails?.maxVal > MAX_VAL || meterDetails?.minVal > meterDetails?.maxVal} /> - - - - - - - {/* minDate input */} - - - handleStringChange(e)} - placeholder='YYYY-MM-DD HH:MM:SS' - required value={meterDetails.minDate} - invalid={!moment(meterDetails.minDate).isValid() - || !moment(meterDetails.minDate).isSameOrAfter(MIN_DATE_MOMENT) - || !moment(meterDetails.minDate).isSameOrBefore(moment(meterDetails.maxDate))} /> - - - - - {/* maxDate input */} - - - handleStringChange(e)} - required value={meterDetails.maxDate} - invalid={!moment(meterDetails.maxDate).isValid() - || !moment(meterDetails.maxDate).isSameOrBefore(MAX_DATE_MOMENT) - || !moment(meterDetails.maxDate).isSameOrAfter(moment(meterDetails.minDate))} /> - - - - - - - {/* DisableChecks input */} - {/* maxError input */} - - - handleNumberChange(e)} - min='0' - max={MAX_ERRORS} - defaultValue={meterDetails.maxError} - invalid={meterDetails?.maxError > MAX_ERRORS || meterDetails?.maxError < 0} /> - - - - - - - handleBooleanChange(e)}> - {Object.keys(TrueFalseType).map(key => { - return () - })} - - - - - {/* reading input */} - - - handleNumberChange(e)} - defaultValue={meterDetails.reading} /> - - {/* startTimestamp input */} - - - handleStringChange(e)} - placeholder='YYYY-MM-DD HH:MM:SS' - value={meterDetails.startTimestamp} /> - - - - {/* endTimestamp input */} - - - handleStringChange(e)} - placeholder='YYYY-MM-DD HH:MM:SS' - value={meterDetails.endTimestamp} /> - - {/* previousEnd input */} - - - handleStringChange(e)} - placeholder='YYYY-MM-DD HH:MM:SS' - value={meterDetails.previousEnd} /> - - - - - {/* Hides the modal */} - - {/* On click calls the function handleSaveChanges in this component */} - - -
- - ); -} - - -/* Create Meter Validation: - Name cannot be blank - Area must be positive or zero - If area is nonzero, area unit must be set - Reading Gap must be greater than zero - Reading Variation must be greater than zero - Reading Duplication must be between 1 and 9 - Reading frequency cannot be blank - Unit and Default Graphic Unit must be set (can be to no unit) - Meter type must be set - If displayable is true and unitId is set to -99, warn admin - Minimum Value cannot bigger than Maximum Value - Minimum Value and Maximum Value must be between valid input - Minimum Date and Maximum cannot be blank - Minimum Date cannot be after Maximum Date - Minimum Date and Maximum Value must be between valid input - Maximum No of Error must be between 0 and valid input -*/ -const isValidCreateMeter = (meterDetails: MeterData) => { - return meterDetails.name !== '' && - (meterDetails.area === 0 || (meterDetails.area > 0 && meterDetails.areaUnit !== AreaUnitType.none)) && - meterDetails.readingGap >= 0 && - meterDetails.readingVariation >= 0 && - (meterDetails.readingDuplication >= 1 && meterDetails.readingDuplication <= 9) && - meterDetails.readingFrequency !== '' && - meterDetails.unitId !== -99 && - meterDetails.defaultGraphicUnit !== -99 && - meterDetails.meterType !== '' && - meterDetails.minVal >= MIN_VAL && - meterDetails.minVal <= meterDetails.maxVal && - meterDetails.maxVal <= MAX_VAL && - moment(meterDetails.minDate).isValid() && - moment(meterDetails.maxDate).isValid() && - moment(meterDetails.minDate).isSameOrAfter(MIN_DATE_MOMENT) && - moment(meterDetails.minDate).isSameOrBefore(moment(meterDetails.maxDate)) && - moment(meterDetails.maxDate).isSameOrBefore(MAX_DATE_MOMENT) && - (meterDetails.maxError >= 0 && meterDetails.maxError <= MAX_ERRORS) -} - - - - -const MIN_VAL = Number.MIN_SAFE_INTEGER; -const MAX_VAL = Number.MAX_SAFE_INTEGER; -const MIN_DATE_MOMENT = moment(0).utc(); -const MAX_DATE_MOMENT = moment(0).utc().add(5000, 'years'); -const MIN_DATE = MIN_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); -const MAX_DATE = MAX_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); -const MAX_ERRORS = 75; diff --git a/src/client/app/components/meters/EditMeterModalComponent.tsx b/src/client/app/components/meters/EditMeterModalComponent.tsx index 41baa1e86..c60ea2f57 100644 --- a/src/client/app/components/meters/EditMeterModalComponent.tsx +++ b/src/client/app/components/meters/EditMeterModalComponent.tsx @@ -2,37 +2,33 @@ * 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 _ from 'lodash'; +import * as moment from 'moment'; import * as React from 'react'; -import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; +import { useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import translate from '../../utils/translate'; -import { useDispatch, useSelector as useAppSelector } from 'react-redux'; -import { useState, useEffect } from 'react'; -import { State } from 'types/redux/state'; +import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; +import TooltipHelpComponent from '../TooltipHelpComponent'; +import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; +import { metersApi, selectMeterById } from '../../redux/api/metersApi'; +import { selectUnitDataById } from '../../redux/api/unitsApi'; +import { useAppDispatch, useAppSelector } from '../../redux/hooks'; +import { makeSelectGraphicUnitCompatibility } from '../../redux/selectors/adminSelectors'; import '../../styles/modal.css'; -import { MeterData, MeterTimeSortType, MeterType } from '../../types/redux/meters'; -import { submitEditedMeter } from '../../actions/meters'; -import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import TooltipHelpComponent from '../../components/TooltipHelpComponent'; +import { tooltipBaseStyle } from '../../styles/modalStyle'; import { TrueFalseType } from '../../types/items'; -import TimeZoneSelect from '../TimeZoneSelect'; +import { MeterData, MeterTimeSortType, MeterType } from '../../types/redux/meters'; +import { UnitRepresentType } from '../../types/redux/units'; import { GPSPoint, isValidGPSInput } from '../../utils/calibration'; -import { UnitData } from '../../types/redux/units'; -import { unitsCompatibleWithUnit } from '../../utils/determineCompatibleUnits'; -import { ConversionArray } from '../../types/conversionArray'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; -import { notifyUser, getGPSString, nullToEmptyString, noUnitTranslated } from '../../utils/input'; -import { tooltipBaseStyle } from '../../styles/modalStyle'; -import { UnitRepresentType } from '../../types/redux/units'; -import { Dispatch } from 'types/redux/actions'; -import * as moment from 'moment'; -import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; +import { getGPSString, notifyUser, nullToEmptyString } from '../../utils/input'; +import translate from '../../utils/translate'; +import TimeZoneSelect from '../TimeZoneSelect'; +import TooltipMarkerComponent from '../TooltipMarkerComponent'; interface EditMeterModalComponentProps { show: boolean; meter: MeterData; - possibleMeterUnits: Set; - possibleGraphicUnits: Set; // passed in to handle closing the modal handleClose: () => void; } @@ -42,155 +38,32 @@ interface EditMeterModalComponentProps { * @returns Meter edit element */ export default function EditMeterModalComponent(props: EditMeterModalComponentProps) { - const dispatch: Dispatch = useDispatch(); - + const dispatch = useAppDispatch(); + const [editMeter] = metersApi.useEditMeterMutation() + // since this selector is shared amongst many other modals, we must use a selector factory in order + // to have a single selector per modal instance. Memo ensures that this is a stable reference + const selectGraphicUnitCompatibility = React.useMemo(makeSelectGraphicUnitCompatibility, []) // The current meter's state of meter being edited. It should always be valid. - const meterState = useAppSelector((state: State) => state.meters.byMeterID[props.meter.id]); - - // Set existing meter values - const values = { - id: props.meter.id, - name: props.meter.name, - url: props.meter.url, - enabled: props.meter.enabled, - displayable: props.meter.displayable, - meterType: props.meter.meterType, - timeZone: props.meter.timeZone, - gps: props.meter.gps, - identifier: props.meter.identifier, - note: props.meter.note, - area: props.meter.area, - cumulative: props.meter.cumulative, - cumulativeReset: props.meter.cumulativeReset, - cumulativeResetStart: props.meter.cumulativeResetStart, - cumulativeResetEnd: props.meter.cumulativeResetEnd, - readingGap: props.meter.readingGap, - readingVariation: props.meter.readingVariation, - readingDuplication: props.meter.readingDuplication, - timeSort: props.meter.timeSort, - endOnlyTime: props.meter.endOnlyTime, - reading: props.meter.reading, - startTimestamp: props.meter.startTimestamp, - endTimestamp: props.meter.endTimestamp, - previousEnd: props.meter.previousEnd, - unitId: props.meter.unitId, - defaultGraphicUnit: props.meter.defaultGraphicUnit, - areaUnit: props.meter.areaUnit, - readingFrequency: props.meter.readingFrequency, - minVal: props.meter.minVal, - maxVal: props.meter.maxVal, - minDate: props.meter.minDate, - maxDate: props.meter.maxDate, - maxError: props.meter.maxError, - disableChecks: props.meter.disableChecks - } - const dropdownsStateDefaults = { - possibleMeterUnits: props.possibleMeterUnits, - possibleGraphicUnits: props.possibleGraphicUnits, - compatibleUnits: props.possibleMeterUnits, - incompatibleUnits: new Set(), - compatibleGraphicUnits: props.possibleGraphicUnits, - incompatibleGraphicUnits: new Set() - } + const meterState = useAppSelector(state => selectMeterById(state, props.meter.id)); + const [localMeterEdits, setLocalMeterEdits] = useState(_.cloneDeep(meterState)); + const { + compatibleGraphicUnits, + incompatibleGraphicUnits, + compatibleUnits, + incompatibleUnits + } = useAppSelector(state => selectGraphicUnitCompatibility(state, localMeterEdits)) + useEffect(() => { setLocalMeterEdits(_.cloneDeep(meterState)) }, [meterState]) /* State */ - // Handlers for each type of input change - const [state, setState] = useState(values); - - const handleStringChange = (e: React.ChangeEvent) => { - setState({ ...state, [e.target.name]: e.target.value }); - } - - const handleBooleanChange = (e: React.ChangeEvent) => { - setState({ ...state, [e.target.name]: JSON.parse(e.target.value) }); - } - - const handleNumberChange = (e: React.ChangeEvent) => { - setState({ ...state, [e.target.name]: Number(e.target.value) }); - } - - const handleTimeZoneChange = (timeZone: string) => { - setState({ ...state, ['timeZone']: timeZone }); - } - - // Dropdowns - const [dropdownsState, setDropdownsState] = useState(dropdownsStateDefaults); - // unit state - const unitState = useAppSelector((state: State) => state.units.units); + const unitDataById = useAppSelector(selectUnitDataById); - /* Edit Meter Validation: - Name cannot be blank - Area must be positive or zero - If area is nonzero, area unit must be set - Reading Gap must be greater than zero - Reading Variation must be greater than zero - Reading Duplication must be between 1 and 9 - Reading frequency cannot be blank - If displayable is true and unitId is set to -99, warn admin - Mininum Value cannot bigger than Maximum Value - Minimum Value and Maximum Value must be between valid input - Minimum Date and Maximum cannot be blank - Minimum Date cannot be after Maximum Date - Minimum Date and Maximum Value must be between valid input - Maximum No of Error must be between 0 and valid input - */ - const [validMeter, setValidMeter] = useState(false); - const MIN_VAL = Number.MIN_SAFE_INTEGER; - const MAX_VAL = Number.MAX_SAFE_INTEGER; - const MIN_DATE_MOMENT = moment(0).utc(); - const MAX_DATE_MOMENT = moment(0).utc().add(5000, 'years'); - const MIN_DATE = MIN_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); - const MAX_DATE = MAX_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); - const MAX_ERRORS = 75; - useEffect(() => { - setValidMeter( - state.name !== '' && - (state.area === 0 || (state.area > 0 && state.areaUnit !== AreaUnitType.none)) && - state.readingGap >= 0 && - state.readingVariation >= 0 && - (state.readingDuplication >= 1 && state.readingDuplication <= 9) && - state.readingFrequency !== '' && - state.minVal >= MIN_VAL && - state.minVal <= state.maxVal && - state.maxVal <= MAX_VAL && - moment(state.minDate).isValid() && - moment(state.maxDate).isValid() && - moment(state.minDate).isSameOrAfter(MIN_DATE_MOMENT) && - moment(state.minDate).isSameOrBefore(moment(state.maxDate)) && - moment(state.maxDate).isSameOrBefore(MAX_DATE_MOMENT) && - (state.maxError >= 0 && state.maxError <= MAX_ERRORS) - ); - }, [ - state.area, - state.name, - state.readingGap, - state.readingVariation, - state.readingDuplication, - state.areaUnit, - state.readingFrequency, - state.minVal, - state.maxVal, - state.minDate, - state.maxDate, - state.maxError - ]); - /* End State */ + const [validMeter, setValidMeter] = useState(isValidMeter(localMeterEdits)); - // Reset the state to default values - // To be used for the discard changes button - // Different use case from CreateMeterModalComponent's resetState - // This allows us to reset our state to match the store in the event of an edit failure - // Failure to edit meters will not trigger a re-render, as no state has changed. Therefore, we must manually reset the values - const resetState = () => { - setState(values); - } + useEffect(() => { setValidMeter(isValidMeter(localMeterEdits)) }, [localMeterEdits]); + /* End State */ - const handleClose = () => { - props.handleClose(); - resetState(); - } // Save changes // Currently using the old functionality which is to compare inherited prop values to state values @@ -204,51 +77,17 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr let inputOk = true; // Check for changes by comparing state to props - const meterHasChanges = - ( - props.meter.identifier != state.identifier || - props.meter.name != state.name || - props.meter.area != state.area || - props.meter.enabled != state.enabled || - props.meter.displayable != state.displayable || - props.meter.meterType != state.meterType || - props.meter.url != state.url || - props.meter.timeZone != state.timeZone || - props.meter.gps != state.gps || - props.meter.unitId != state.unitId || - props.meter.defaultGraphicUnit != state.defaultGraphicUnit || - props.meter.note != state.note || - props.meter.cumulative != state.cumulative || - props.meter.cumulativeReset != state.cumulativeReset || - props.meter.cumulativeResetStart != state.cumulativeResetStart || - props.meter.cumulativeResetEnd != state.cumulativeResetEnd || - props.meter.endOnlyTime != state.endOnlyTime || - props.meter.reading != state.reading || - props.meter.readingGap != state.readingGap || - props.meter.readingVariation != state.readingVariation || - props.meter.readingDuplication != state.readingDuplication || - props.meter.timeSort != state.timeSort || - props.meter.startTimestamp != state.startTimestamp || - props.meter.endTimestamp != state.endTimestamp || - props.meter.previousEnd != state.previousEnd || - props.meter.areaUnit != state.areaUnit || - props.meter.readingFrequency != state.readingFrequency || - props.meter.minVal != state.minVal || - props.meter.maxVal != state.maxVal || - props.meter.minDate != state.minDate || - props.meter.maxDate != state.maxDate || - props.meter.maxError != state.maxError || - props.meter.disableChecks != state.disableChecks - ); + const meterHasChanges = !_.isEqual(meterState, localMeterEdits) // Only validate and store if any changes. if (meterHasChanges) { // Set default identifier as name if left blank - state.identifier = (!state.identifier || state.identifier.length === 0) ? state.name : state.identifier; + localMeterEdits.identifier = (!localMeterEdits.identifier || localMeterEdits.identifier.length === 0) ? + localMeterEdits.name : localMeterEdits.identifier; // Check GPS entered. // Validate GPS is okay and take from string to GPSPoint to submit. - const gpsInput = state.gps; + const gpsInput = localMeterEdits.gps; let gps: GPSPoint | null = null; const latitudeIndex = 0; const longitudeIndex = 1; @@ -276,20 +115,21 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr if (inputOk) { // The input passed validation. // GPS may have been updated so create updated state to submit. - const submitState = { ...state, gps: gps }; + const submitState = { ...localMeterEdits, gps }; // The reading views need to be refreshed if going to/from no unit or // to/from type quantity. // The check does it by first seeing if the unit changed and, if so, it // sees if either were non unit meaning it crossed since both cannot be no unit // or the unit change to/from quantity. - const shouldRefreshReadingViews = (props.meter.unitId != state.unitId) && - ((props.meter.unitId == -99 || state.unitId == -99) || - (unitState[props.meter.unitId].unitRepresent == UnitRepresentType.quantity - && unitState[state.unitId].unitRepresent != UnitRepresentType.quantity) || - (unitState[props.meter.unitId].unitRepresent != UnitRepresentType.quantity - && unitState[state.unitId].unitRepresent == UnitRepresentType.quantity)); + const shouldRefreshReadingViews = (props.meter.unitId != localMeterEdits.unitId) && + ((props.meter.unitId == -99 || localMeterEdits.unitId == -99) || + (unitDataById[props.meter.unitId].unitRepresent == UnitRepresentType.quantity + && unitDataById[localMeterEdits.unitId].unitRepresent != UnitRepresentType.quantity) || + (unitDataById[props.meter.unitId].unitRepresent != UnitRepresentType.quantity + && unitDataById[localMeterEdits.unitId].unitRepresent == UnitRepresentType.quantity)); // Submit new meter if checks where ok. - dispatch(submitEditedMeter(submitState, shouldRefreshReadingViews)); + // dispatch(submitEditedMeter(submitState, shouldRefreshReadingViews) as ThunkAction); + editMeter({ meterData: submitState, shouldRefreshViews: shouldRefreshReadingViews }) dispatch(unsavedWarningSlice.actions.removeUnsavedChanges()); } else { // Tell user that not going to update due to input issues. @@ -298,101 +138,34 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr } }; - // TODO This useEffect can probably be extracted into a single function in the future, as create meter also uses them. - // Note there are now differences, e.g., -999 check. - - // Update compatible units and graphic units set. - // Note an earlier version had two useEffect calls: one for each menu. This lead to an issue because it did separate - // setState calls that were asynchronous. As a result, the second one could use state state when doing ...dropdownsState - // and lose the first changes. Fusing them fixes this. - useEffect(() => { - // Graphic units compatible with currently selected unit - const compatibleGraphicUnits = new Set(); - // Graphic units incompatible with currently selected unit - const incompatibleGraphicUnits = new Set(); - // If unit is not 'no unit' - if (state.unitId != -99) { - // Find all units compatible with the selected unit - const unitsCompatibleWithSelectedUnit = unitsCompatibleWithUnit(state.unitId); - dropdownsState.possibleGraphicUnits.forEach(unit => { - // If current graphic unit exists in the set of compatible graphic units OR if the current graphic unit is 'no unit' - if (unitsCompatibleWithSelectedUnit.has(unit.id) || unit.id === -99) { - compatibleGraphicUnits.add(unit); - } else { - incompatibleGraphicUnits.add(unit); - } - }); - } else { - // No unit is selected - // OED does not allow a default graphic unit if there is no unit so it must be -99. - state.defaultGraphicUnit = -99; - dropdownsState.possibleGraphicUnits.forEach(unit => { - // Only -99 is allowed. - if (unit.id === -99) { - compatibleGraphicUnits.add(unit); - } else { - incompatibleGraphicUnits.add(unit); - } - }); - } + const handleStringChange = (e: React.ChangeEvent) => { + setLocalMeterEdits({ ...localMeterEdits, [e.target.name]: e.target.value.trim() }); + } - // Units compatible with currently selected graphic unit - let compatibleUnits = new Set(); - // Units incompatible with currently selected graphic unit - const incompatibleUnits = new Set(); - // If a default graphic unit is not 'no unit' - if (state.defaultGraphicUnit !== -99) { - // Find all units compatible with the selected graphic unit - dropdownsState.possibleMeterUnits.forEach(unit => { - // Graphic units compatible with the current meter unit - const compatibleGraphicUnits = unitsCompatibleWithUnit(unit.id); - // If the currently selected default graphic unit exists in the set of graphic units compatible with the current meter unit - // Also add the 'no unit' unit - if (compatibleGraphicUnits.has(state.defaultGraphicUnit) || unit.id === -99) { - // add the current meter unit to the list of compatible units - compatibleUnits.add(unit.id === -99 ? noUnitTranslated() : unit); - } else { - // add the current meter unit to the list of incompatible units - incompatibleUnits.add(unit); - } - }); - } else { - // No default graphic unit is selected - // All units are compatible - compatibleUnits = new Set(dropdownsState.possibleMeterUnits); - } - // Update the state - setDropdownsState({ - ...dropdownsState, - // The new set helps avoid repaints. - compatibleGraphicUnits: new Set(compatibleGraphicUnits), - incompatibleGraphicUnits: new Set(incompatibleGraphicUnits), - compatibleUnits: new Set(compatibleUnits), - incompatibleUnits: new Set(incompatibleUnits) - }); - // If either unit or the status of pik changes then this needs to be done. - // pik is needed since the compatible units is not correct until pik is available. - }, [state.unitId, state.defaultGraphicUnit, ConversionArray.pikAvailable()]); + const handleBooleanChange = (e: React.ChangeEvent) => { + setLocalMeterEdits({ ...localMeterEdits, [e.target.name]: JSON.parse(e.target.value) }); + } - // If you edit and return to this page then want to see the DB result formatted for users - // for the readingFrequency. Since the update on save is to the global state, need to - // change the state used for display here. Note if you change readingFrequency but it - // is only a change in format and not value then this will not update because Redux is - // smart and sees they are the same. This is not really an issue to worry about but - // noted for others. - useEffect(() => { - setState({ - ...state, - readingFrequency: meterState.readingFrequency - }) - }, [meterState.readingFrequency]); + const handleNumberChange = (e: React.ChangeEvent) => { + setLocalMeterEdits({ ...localMeterEdits, [e.target.name]: Number(e.target.value) }); + } - const tooltipStyle = { - ...tooltipBaseStyle, - // Only and admin can edit a meter. - tooltipEditMeterView: 'help.admin.meteredit' - }; + const handleTimeZoneChange = (timeZone: string) => { + setLocalMeterEdits({ ...localMeterEdits, ['timeZone']: timeZone }); + } + // Reset the state to default values + // To be used for the discard changes button + // Different use case from CreateMeterModalComponent's resetState + // This allows us to reset our state to match the store in the event of an edit failure + // Failure to edit meters will not trigger a re-render, as no state has changed. Therefore, we must manually reset the values + const resetState = () => { + setLocalMeterEdits(meterState); + } + const handleClose = () => { + props.handleClose(); + resetState(); + } return ( <> @@ -415,7 +188,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr type='text' autoComplete='on' onChange={e => handleStringChange(e)} - value={state.identifier} /> + value={localMeterEdits.identifier} /> {/* Name input */} @@ -426,8 +199,8 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr type='text' autoComplete='on' onChange={e => handleStringChange(e)} - value={state.name} - invalid={state.name === ''} /> + value={localMeterEdits.name} + invalid={localMeterEdits.name === ''} /> @@ -441,12 +214,12 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr id='unitId' name='unitId' type='select' - value={state.unitId} + value={localMeterEdits.unitId} onChange={e => handleNumberChange(e)}> - {Array.from(dropdownsState.compatibleUnits).map(unit => { + {Array.from(compatibleUnits).map(unit => { return () })} - {Array.from(dropdownsState.incompatibleUnits).map(unit => { + {Array.from(incompatibleUnits).map(unit => { return () })} @@ -458,12 +231,12 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr id='defaultGraphicUnit' name='defaultGraphicUnit' type='select' - value={state.defaultGraphicUnit} + value={localMeterEdits.defaultGraphicUnit} onChange={e => handleNumberChange(e)}> - {Array.from(dropdownsState.compatibleGraphicUnits).map(unit => { + {Array.from(compatibleGraphicUnits).map(unit => { return () })} - {Array.from(dropdownsState.incompatibleGraphicUnits).map(unit => { + {Array.from(incompatibleGraphicUnits).map(unit => { return () })} @@ -483,7 +256,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr // happens when your reload one of these pages but to avoid issues it uses // the ? to avoid access. This only applies to items where you dereference // the state value such as .toString() here. - value={state.enabled?.toString()} + value={localMeterEdits.enabled?.toString()} onChange={e => handleBooleanChange(e)}> {Object.keys(TrueFalseType).map(key => { return () @@ -497,9 +270,9 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr id='displayable' name='displayable' type='select' - value={state.displayable?.toString()} + value={localMeterEdits.displayable?.toString()} onChange={e => handleBooleanChange(e)} - invalid={state.displayable && state.unitId === -99}> + invalid={localMeterEdits.displayable && localMeterEdits.unitId === -99}> {Object.keys(TrueFalseType).map(key => { return () })} @@ -517,7 +290,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr id='meterType' name='meterType' type='select' - value={state.meterType} + value={localMeterEdits.meterType} onChange={e => handleStringChange(e)}> {/* The dB expects lowercase. */} {Object.keys(MeterType).map(key => { @@ -534,8 +307,8 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr type='text' autoComplete='on' onChange={e => handleStringChange(e)} - value={state.readingFrequency} - invalid={state.readingFrequency === ''} /> + value={localMeterEdits.readingFrequency} + invalid={localMeterEdits.readingFrequency === ''} /> @@ -551,7 +324,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr type='text' autoComplete='off' onChange={e => handleStringChange(e)} - value={nullToEmptyString(state.url)} /> + value={nullToEmptyString(localMeterEdits.url)} /> {/* GPS input */} @@ -562,7 +335,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr type='text' autoComplete='on' onChange={e => handleStringChange(e)} - value={getGPSString(state.gps)} /> + value={getGPSString(localMeterEdits.gps)} /> @@ -574,9 +347,9 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr name='area' type='number' min='0' - defaultValue={state.area} + defaultValue={localMeterEdits.area} onChange={e => handleNumberChange(e)} - invalid={state.area < 0} /> + invalid={localMeterEdits.area < 0} /> @@ -588,9 +361,9 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr id='areaUnit' name='areaUnit' type='select' - value={state.areaUnit} + value={localMeterEdits.areaUnit} onChange={e => handleStringChange(e)} - invalid={state.area > 0 && state.areaUnit === AreaUnitType.none}> + invalid={localMeterEdits.area > 0 && localMeterEdits.areaUnit === AreaUnitType.none}> {Object.keys(AreaUnitType).map(key => { return () })} @@ -608,7 +381,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr name='note' type='textarea' onChange={e => handleStringChange(e)} - value={nullToEmptyString(state.note)} + value={nullToEmptyString(localMeterEdits.note)} placeholder='Note' /> @@ -619,7 +392,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr id='cumulative' name='cumulative' type='select' - value={state.cumulative?.toString()} + value={localMeterEdits.cumulative?.toString()} onChange={e => handleBooleanChange(e)}> {Object.keys(TrueFalseType).map(key => { return () @@ -633,7 +406,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr id='cumulativeReset' name='cumulativeReset' type='select' - value={state.cumulativeReset?.toString()} + value={localMeterEdits.cumulativeReset?.toString()} onChange={e => handleBooleanChange(e)}> {Object.keys(TrueFalseType).map(key => { return () @@ -651,7 +424,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr type='text' autoComplete='off' onChange={e => handleStringChange(e)} - value={state.cumulativeResetStart} + value={localMeterEdits.cumulativeResetStart} placeholder='HH:MM:SS' /> {/* cumulativeResetEnd input */} @@ -663,7 +436,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr type='text' autoComplete='off' onChange={e => handleStringChange(e)} - value={state?.cumulativeResetEnd} + value={localMeterEdits?.cumulativeResetEnd} placeholder='HH:MM:SS' /> @@ -675,7 +448,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr id='endOnlyTime' name='endOnlyTime' type='select' - value={state.endOnlyTime?.toString()} + value={localMeterEdits.endOnlyTime?.toString()} onChange={e => handleBooleanChange(e)}> {Object.keys(TrueFalseType).map(key => { return () @@ -691,8 +464,8 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr type='number' onChange={e => handleNumberChange(e)} min='0' - defaultValue={state?.readingGap} - invalid={state?.readingGap < 0} /> + defaultValue={localMeterEdits?.readingGap} + invalid={localMeterEdits?.readingGap < 0} /> @@ -708,8 +481,8 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr type='number' onChange={e => handleNumberChange(e)} min='0' - defaultValue={state?.readingVariation} - invalid={state?.readingVariation < 0} /> + defaultValue={localMeterEdits?.readingVariation} + invalid={localMeterEdits?.readingVariation < 0} /> @@ -725,8 +498,8 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr step='1' min='1' max='9' - defaultValue={state?.readingDuplication} - invalid={state?.readingDuplication < 1 || state?.readingDuplication > 9} /> + defaultValue={localMeterEdits?.readingDuplication} + invalid={localMeterEdits?.readingDuplication < 1 || localMeterEdits?.readingDuplication > 9} /> @@ -740,7 +513,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr id='timeSort' name='timeSort' type='select' - value={state?.timeSort} + value={localMeterEdits?.timeSort} onChange={e => handleStringChange(e)}> {Object.keys(MeterTimeSortType).map(key => { // This is a bit of a hack but it should work fine. The TypeSortTypes and MeterTimeSortType should be in sync. @@ -752,7 +525,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr {/* Timezone input */} - handleTimeZoneChange(timeZone)} /> + handleTimeZoneChange(timeZone)} /> @@ -765,11 +538,11 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr type='number' onChange={e => handleNumberChange(e)} min={MIN_VAL} - max={state.maxVal} - required value={state.minVal} - invalid={state?.minVal < MIN_VAL || state?.minVal > state?.maxVal} /> + max={localMeterEdits.maxVal} + required value={localMeterEdits.minVal} + invalid={localMeterEdits?.minVal < MIN_VAL || localMeterEdits?.minVal > localMeterEdits?.maxVal} /> - + {/* maxVal input */} @@ -780,12 +553,12 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr name='maxVal' type='number' onChange={e => handleNumberChange(e)} - min={state.minVal} + min={localMeterEdits.minVal} max={MAX_VAL} - required value={state.maxVal} - invalid={state?.maxVal > MAX_VAL || state?.minVal > state?.maxVal} /> + required value={localMeterEdits.maxVal} + invalid={localMeterEdits?.maxVal > MAX_VAL || localMeterEdits?.minVal > localMeterEdits?.maxVal} /> - + @@ -800,12 +573,12 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr autoComplete='on' onChange={e => handleStringChange(e)} placeholder='YYYY-MM-DD HH:MM:SS' - required value={state.minDate} - invalid={!moment(state.minDate).isValid() - || !moment(state.minDate).isSameOrAfter(MIN_DATE_MOMENT) - || !moment(state.minDate).isSameOrBefore(moment(state.maxDate))} /> + required value={localMeterEdits.minDate} + invalid={!moment(localMeterEdits.minDate).isValid() + || !moment(localMeterEdits.minDate).isSameOrAfter(MIN_DATE_MOMENT) + || !moment(localMeterEdits.minDate).isSameOrBefore(moment(localMeterEdits.maxDate))} /> - + {/* maxDate input */} @@ -818,12 +591,12 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr autoComplete='on' onChange={e => handleStringChange(e)} placeholder='YYYY-MM-DD HH:MM:SS' - required value={state.maxDate} - invalid={!moment(state.maxDate).isValid() - || !moment(state.maxDate).isSameOrBefore(MAX_DATE_MOMENT) - || !moment(state.maxDate).isSameOrAfter(moment(state.minDate))} /> + required value={localMeterEdits.maxDate} + invalid={!moment(localMeterEdits.maxDate).isValid() + || !moment(localMeterEdits.maxDate).isSameOrBefore(MAX_DATE_MOMENT) + || !moment(localMeterEdits.maxDate).isSameOrAfter(moment(localMeterEdits.minDate))} /> - + @@ -838,8 +611,8 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr onChange={e => handleNumberChange(e)} min='0' max={MAX_ERRORS} - required value={state.maxError} - invalid={state?.maxError > MAX_ERRORS || state?.maxError < 0} /> + required value={localMeterEdits.maxError} + invalid={localMeterEdits?.maxError > MAX_ERRORS || localMeterEdits?.maxError < 0} /> @@ -851,9 +624,9 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr id='disableChecks' name='disableChecks' type='select' - value={state?.disableChecks?.toString()} + value={localMeterEdits?.disableChecks?.toString()} onChange={e => handleBooleanChange(e)} - invalid={state?.disableChecks && state.unitId === -99}> + invalid={localMeterEdits?.disableChecks && localMeterEdits.unitId === -99}> {Object.keys(TrueFalseType).map(key => { return () })} @@ -869,7 +642,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr name='reading' type='number' onChange={e => handleNumberChange(e)} - defaultValue={state?.reading} /> + defaultValue={localMeterEdits?.reading} /> {/* startTimestamp input */} @@ -881,7 +654,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr autoComplete='on' onChange={e => handleStringChange(e)} placeholder='YYYY-MM-DD HH:MM:SS' - value={state?.startTimestamp} /> + value={localMeterEdits?.startTimestamp} /> @@ -895,7 +668,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr autoComplete='on' onChange={e => handleStringChange(e)} placeholder='YYYY-MM-DD HH:MM:SS' - value={state?.endTimestamp} /> + value={localMeterEdits?.endTimestamp} /> {/* previousEnd input */} @@ -907,7 +680,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr autoComplete='on' onChange={e => handleStringChange(e)} placeholder='YYYY-MM-DD HH:MM:SS' - value={state?.previousEnd} /> + value={localMeterEdits?.previousEnd} /> @@ -925,3 +698,51 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr ); } + + +const MIN_VAL = Number.MIN_SAFE_INTEGER; +const MAX_VAL = Number.MAX_SAFE_INTEGER; +const MIN_DATE_MOMENT = moment(0).utc(); +const MAX_DATE_MOMENT = moment(0).utc().add(5000, 'years'); +const MIN_DATE = MIN_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); +const MAX_DATE = MAX_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); +const MAX_ERRORS = 75; +const tooltipStyle = { + ...tooltipBaseStyle, + // Only and admin can edit a meter. + tooltipEditMeterView: 'help.admin.meteredit' +}; + +const isValidMeter = (localMeterEdits: MeterData) => { + /* Edit Meter Validation: + Name cannot be blank + Area must be positive or zero + If area is nonzero, area unit must be set + Reading Gap must be greater than zero + Reading Variation must be greater than zero + Reading Duplication must be between 1 and 9 + Reading frequency cannot be blank + If displayable is true and unitId is set to -99, warn admin + Minimum Value cannot bigger than Maximum Value + Minimum Value and Maximum Value must be between valid input + Minimum Date and Maximum cannot be blank + Minimum Date cannot be after Maximum Date + Minimum Date and Maximum Value must be between valid input + Maximum No of Error must be between 0 and valid input + */ + return localMeterEdits.name !== '' && + (localMeterEdits.area === 0 || (localMeterEdits.area > 0 && localMeterEdits.areaUnit !== AreaUnitType.none)) && + localMeterEdits.readingGap >= 0 && + localMeterEdits.readingVariation >= 0 && + (localMeterEdits.readingDuplication >= 1 && localMeterEdits.readingDuplication <= 9) && + localMeterEdits.readingFrequency !== '' && + localMeterEdits.minVal >= MIN_VAL && + localMeterEdits.minVal <= localMeterEdits.maxVal && + localMeterEdits.maxVal <= MAX_VAL && + moment(localMeterEdits.minDate).isValid() && + moment(localMeterEdits.maxDate).isValid() && + moment(localMeterEdits.minDate).isSameOrAfter(MIN_DATE_MOMENT) && + moment(localMeterEdits.minDate).isSameOrBefore(moment(localMeterEdits.maxDate)) && + moment(localMeterEdits.maxDate).isSameOrBefore(MAX_DATE_MOMENT) && + (localMeterEdits.maxError >= 0 && localMeterEdits.maxError <= MAX_ERRORS) +} \ No newline at end of file diff --git a/src/client/app/components/meters/EditMeterModalComponentWIP.tsx b/src/client/app/components/meters/EditMeterModalComponentWIP.tsx deleted file mode 100644 index 2280d1d0a..000000000 --- a/src/client/app/components/meters/EditMeterModalComponentWIP.tsx +++ /dev/null @@ -1,748 +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 * as _ from 'lodash'; -import * as moment from 'moment'; -import * as React from 'react'; -import { useEffect, useState } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; -import TooltipHelpComponent from '../../components/TooltipHelpComponent'; -import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; -import { metersApi, selectMeterById } from '../../redux/api/metersApi'; -import { selectUnitDataById } from '../../redux/api/unitsApi'; -import { useAppDispatch, useAppSelector } from '../../redux/hooks'; -import { makeSelectGraphicUnitCompatibility } from '../../redux/selectors/adminSelectors'; -import '../../styles/modal.css'; -import { tooltipBaseStyle } from '../../styles/modalStyle'; -import { TrueFalseType } from '../../types/items'; -import { MeterData, MeterTimeSortType, MeterType } from '../../types/redux/meters'; -import { UnitRepresentType } from '../../types/redux/units'; -import { GPSPoint, isValidGPSInput } from '../../utils/calibration'; -import { AreaUnitType } from '../../utils/getAreaUnitConversion'; -import { getGPSString, notifyUser, nullToEmptyString } from '../../utils/input'; -import translate from '../../utils/translate'; -import TimeZoneSelect from '../TimeZoneSelect'; -import TooltipMarkerComponent from '../TooltipMarkerComponent'; - -interface EditMeterModalComponentProps { - show: boolean; - meter: MeterData; - // passed in to handle closing the modal - handleClose: () => void; -} -/** - * Defines the edit meter modal form - * @param props for the edit component - * @returns Meter edit element - */ -export default function EditMeterModalComponent(props: EditMeterModalComponentProps) { - const dispatch = useAppDispatch(); - const [editMeter] = metersApi.useEditMeterMutation() - // since this selector is shared amongst many other modals, we must use a selector factory in order - // to have a single selector per modal instance. Memo ensures that this is a stable reference - const selectGraphicUnitCompatibility = React.useMemo(makeSelectGraphicUnitCompatibility, []) - // The current meter's state of meter being edited. It should always be valid. - const meterState = useAppSelector(state => selectMeterById(state, props.meter.id)); - const [localMeterEdits, setLocalMeterEdits] = useState(_.cloneDeep(meterState)); - const { - compatibleGraphicUnits, - incompatibleGraphicUnits, - compatibleUnits, - incompatibleUnits - } = useAppSelector(state => selectGraphicUnitCompatibility(state, localMeterEdits)) - - useEffect(() => { setLocalMeterEdits(_.cloneDeep(meterState)) }, [meterState]) - /* State */ - // unit state - const unitDataById = useAppSelector(selectUnitDataById); - - - const [validMeter, setValidMeter] = useState(isValidMeter(localMeterEdits)); - - useEffect(() => { setValidMeter(isValidMeter(localMeterEdits)) }, [localMeterEdits]); - /* End State */ - - - // Save changes - // Currently using the old functionality which is to compare inherited prop values to state values - // If there is a difference between props and state, then a change was made - // Side note, we could probably just set a boolean when any input but this would not detect if edited but no change made. - const handleSaveChanges = () => { - // Close the modal first to avoid repeat clicks - props.handleClose(); - - // true if inputted values are okay. Then can submit. - let inputOk = true; - - // Check for changes by comparing state to props - const meterHasChanges = !_.isEqual(meterState, localMeterEdits) - - // Only validate and store if any changes. - if (meterHasChanges) { - // Set default identifier as name if left blank - localMeterEdits.identifier = (!localMeterEdits.identifier || localMeterEdits.identifier.length === 0) ? - localMeterEdits.name : localMeterEdits.identifier; - - // Check GPS entered. - // Validate GPS is okay and take from string to GPSPoint to submit. - const gpsInput = localMeterEdits.gps; - let gps: GPSPoint | null = null; - const latitudeIndex = 0; - const longitudeIndex = 1; - // If the user input a value then gpsInput should be a string. - // null came from the DB and it is okay to just leave it - Not a string. - if (typeof gpsInput === 'string') { - if (isValidGPSInput(gpsInput)) { - // Clearly gpsInput is a string but TS complains about the split so cast. - const gpsValues = (gpsInput as string).split(',').map((value: string) => parseFloat(value)); - // It is valid and needs to be in this format for routing. - gps = { - longitude: gpsValues[longitudeIndex], - latitude: gpsValues[latitudeIndex] - }; - // gpsInput must be of type string but TS does not think so so cast. - } else if ((gpsInput as string).length !== 0) { - // GPS not okay. - // TODO isValidGPSInput currently tops up an alert so not doing it here, may change - // so leaving code commented out. - // notifyUser(translate('input.gps.range') + state.gps + '.'); - inputOk = false; - } - } - - if (inputOk) { - // The input passed validation. - // GPS may have been updated so create updated state to submit. - const submitState = { ...localMeterEdits, gps }; - // The reading views need to be refreshed if going to/from no unit or - // to/from type quantity. - // The check does it by first seeing if the unit changed and, if so, it - // sees if either were non unit meaning it crossed since both cannot be no unit - // or the unit change to/from quantity. - const shouldRefreshReadingViews = (props.meter.unitId != localMeterEdits.unitId) && - ((props.meter.unitId == -99 || localMeterEdits.unitId == -99) || - (unitDataById[props.meter.unitId].unitRepresent == UnitRepresentType.quantity - && unitDataById[localMeterEdits.unitId].unitRepresent != UnitRepresentType.quantity) || - (unitDataById[props.meter.unitId].unitRepresent != UnitRepresentType.quantity - && unitDataById[localMeterEdits.unitId].unitRepresent == UnitRepresentType.quantity)); - // Submit new meter if checks where ok. - // dispatch(submitEditedMeter(submitState, shouldRefreshReadingViews) as ThunkAction); - editMeter({ meterData: submitState, shouldRefreshViews: shouldRefreshReadingViews }) - dispatch(unsavedWarningSlice.actions.removeUnsavedChanges()); - } else { - // Tell user that not going to update due to input issues. - notifyUser(translate('meter.input.error')); - } - } - }; - - const handleStringChange = (e: React.ChangeEvent) => { - setLocalMeterEdits({ ...localMeterEdits, [e.target.name]: e.target.value.trim() }); - } - - const handleBooleanChange = (e: React.ChangeEvent) => { - setLocalMeterEdits({ ...localMeterEdits, [e.target.name]: JSON.parse(e.target.value) }); - } - - const handleNumberChange = (e: React.ChangeEvent) => { - setLocalMeterEdits({ ...localMeterEdits, [e.target.name]: Number(e.target.value) }); - } - - const handleTimeZoneChange = (timeZone: string) => { - setLocalMeterEdits({ ...localMeterEdits, ['timeZone']: timeZone }); - } - // Reset the state to default values - // To be used for the discard changes button - // Different use case from CreateMeterModalComponent's resetState - // This allows us to reset our state to match the store in the event of an edit failure - // Failure to edit meters will not trigger a re-render, as no state has changed. Therefore, we must manually reset the values - const resetState = () => { - setLocalMeterEdits(meterState); - } - - const handleClose = () => { - props.handleClose(); - resetState(); - } - return ( - <> - - - - -
- -
-
- {/* when any of the Meter values are changed call one of the functions. */} - - - {/* Identifier input */} - - - handleStringChange(e)} - value={localMeterEdits.identifier} /> - - {/* Name input */} - - - handleStringChange(e)} - value={localMeterEdits.name} - invalid={localMeterEdits.name === ''} /> - - - - - - - {/* meter unit input */} - - - handleNumberChange(e)}> - {Array.from(compatibleUnits).map(unit => { - return () - })} - {Array.from(incompatibleUnits).map(unit => { - return () - })} - - - {/* default graphic unit input */} - - - handleNumberChange(e)}> - {Array.from(compatibleGraphicUnits).map(unit => { - return () - })} - {Array.from(incompatibleGraphicUnits).map(unit => { - return () - })} - - - - - {/* Enabled input */} - - - handleBooleanChange(e)}> - {Object.keys(TrueFalseType).map(key => { - return () - })} - - - {/* Displayable input */} - - - handleBooleanChange(e)} - invalid={localMeterEdits.displayable && localMeterEdits.unitId === -99}> - {Object.keys(TrueFalseType).map(key => { - return () - })} - - - - - - - - {/* Meter type input */} - - - handleStringChange(e)}> - {/* The dB expects lowercase. */} - {Object.keys(MeterType).map(key => { - return () - })} - - - {/* Meter reading frequency */} - - - handleStringChange(e)} - value={localMeterEdits.readingFrequency} - invalid={localMeterEdits.readingFrequency === ''} /> - - - - - - - {/* URL input */} - - - handleStringChange(e)} - value={nullToEmptyString(localMeterEdits.url)} /> - - {/* GPS input */} - - - handleStringChange(e)} - value={getGPSString(localMeterEdits.gps)} /> - - - - {/* Area input */} - - - handleNumberChange(e)} - invalid={localMeterEdits.area < 0} /> - - - - - {/* meter area unit input */} - - - handleStringChange(e)} - invalid={localMeterEdits.area > 0 && localMeterEdits.areaUnit === AreaUnitType.none}> - {Object.keys(AreaUnitType).map(key => { - return () - })} - - - - - - - {/* note input */} - - - handleStringChange(e)} - value={nullToEmptyString(localMeterEdits.note)} - placeholder='Note' /> - - - {/* cumulative input */} - - - handleBooleanChange(e)}> - {Object.keys(TrueFalseType).map(key => { - return () - })} - - - {/* cumulativeReset input */} - - - handleBooleanChange(e)}> - {Object.keys(TrueFalseType).map(key => { - return () - })} - - - - - {/* cumulativeResetStart input */} - - - handleStringChange(e)} - value={localMeterEdits.cumulativeResetStart} - placeholder='HH:MM:SS' /> - - {/* cumulativeResetEnd input */} - - - handleStringChange(e)} - value={localMeterEdits?.cumulativeResetEnd} - placeholder='HH:MM:SS' /> - - - - {/* endOnlyTime input */} - - - handleBooleanChange(e)}> - {Object.keys(TrueFalseType).map(key => { - return () - })} - - - {/* readingGap input */} - - - handleNumberChange(e)} - min='0' - defaultValue={localMeterEdits?.readingGap} - invalid={localMeterEdits?.readingGap < 0} /> - - - - - - - {/* readingVariation input */} - - - handleNumberChange(e)} - min='0' - defaultValue={localMeterEdits?.readingVariation} - invalid={localMeterEdits?.readingVariation < 0} /> - - - - - {/* readingDuplication input */} - - - handleNumberChange(e)} - step='1' - min='1' - max='9' - defaultValue={localMeterEdits?.readingDuplication} - invalid={localMeterEdits?.readingDuplication < 1 || localMeterEdits?.readingDuplication > 9} /> - - - - - - - {/* timeSort input */} - - - handleStringChange(e)}> - {Object.keys(MeterTimeSortType).map(key => { - // This is a bit of a hack but it should work fine. The TypeSortTypes and MeterTimeSortType should be in sync. - // The translation is on the former so we use that enum name there but loop on the other to get the value desired. - return () - })} - - - {/* Timezone input */} - - - handleTimeZoneChange(timeZone)} /> - - - - {/* minVal input */} - - - handleNumberChange(e)} - min={MIN_VAL} - max={localMeterEdits.maxVal} - required value={localMeterEdits.minVal} - invalid={localMeterEdits?.minVal < MIN_VAL || localMeterEdits?.minVal > localMeterEdits?.maxVal} /> - - - - - {/* maxVal input */} - - - handleNumberChange(e)} - min={localMeterEdits.minVal} - max={MAX_VAL} - required value={localMeterEdits.maxVal} - invalid={localMeterEdits?.maxVal > MAX_VAL || localMeterEdits?.minVal > localMeterEdits?.maxVal} /> - - - - - - - {/* minDate input */} - - - handleStringChange(e)} - placeholder='YYYY-MM-DD HH:MM:SS' - required value={localMeterEdits.minDate} - invalid={!moment(localMeterEdits.minDate).isValid() - || !moment(localMeterEdits.minDate).isSameOrAfter(MIN_DATE_MOMENT) - || !moment(localMeterEdits.minDate).isSameOrBefore(moment(localMeterEdits.maxDate))} /> - - - - - {/* maxDate input */} - - - handleStringChange(e)} - placeholder='YYYY-MM-DD HH:MM:SS' - required value={localMeterEdits.maxDate} - invalid={!moment(localMeterEdits.maxDate).isValid() - || !moment(localMeterEdits.maxDate).isSameOrBefore(MAX_DATE_MOMENT) - || !moment(localMeterEdits.maxDate).isSameOrAfter(moment(localMeterEdits.minDate))} /> - - - - - - - {/* maxError input */} - - - handleNumberChange(e)} - min='0' - max={MAX_ERRORS} - required value={localMeterEdits.maxError} - invalid={localMeterEdits?.maxError > MAX_ERRORS || localMeterEdits?.maxError < 0} /> - - - - - {/* DisableChecks input */} - - - handleBooleanChange(e)} - invalid={localMeterEdits?.disableChecks && localMeterEdits.unitId === -99}> - {Object.keys(TrueFalseType).map(key => { - return () - })} - - - - - {/* reading input */} - - - handleNumberChange(e)} - defaultValue={localMeterEdits?.reading} /> - - {/* startTimestamp input */} - - - handleStringChange(e)} - placeholder='YYYY-MM-DD HH:MM:SS' - value={localMeterEdits?.startTimestamp} /> - - - - {/* endTimestamp input */} - - - handleStringChange(e)} - placeholder='YYYY-MM-DD HH:MM:SS' - value={localMeterEdits?.endTimestamp} /> - - {/* previousEnd input */} - - - handleStringChange(e)} - placeholder='YYYY-MM-DD HH:MM:SS' - value={localMeterEdits?.previousEnd} /> - - - - - {/* Hides the modal */} - - {/* On click calls the function handleSaveChanges in this component */} - - -
- - ); -} - - -const MIN_VAL = Number.MIN_SAFE_INTEGER; -const MAX_VAL = Number.MAX_SAFE_INTEGER; -const MIN_DATE_MOMENT = moment(0).utc(); -const MAX_DATE_MOMENT = moment(0).utc().add(5000, 'years'); -const MIN_DATE = MIN_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); -const MAX_DATE = MAX_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); -const MAX_ERRORS = 75; -const tooltipStyle = { - ...tooltipBaseStyle, - // Only and admin can edit a meter. - tooltipEditMeterView: 'help.admin.meteredit' -}; - -const isValidMeter = (localMeterEdits: MeterData) => { - /* Edit Meter Validation: - Name cannot be blank - Area must be positive or zero - If area is nonzero, area unit must be set - Reading Gap must be greater than zero - Reading Variation must be greater than zero - Reading Duplication must be between 1 and 9 - Reading frequency cannot be blank - If displayable is true and unitId is set to -99, warn admin - Minimum Value cannot bigger than Maximum Value - Minimum Value and Maximum Value must be between valid input - Minimum Date and Maximum cannot be blank - Minimum Date cannot be after Maximum Date - Minimum Date and Maximum Value must be between valid input - Maximum No of Error must be between 0 and valid input - */ - return localMeterEdits.name !== '' && - (localMeterEdits.area === 0 || (localMeterEdits.area > 0 && localMeterEdits.areaUnit !== AreaUnitType.none)) && - localMeterEdits.readingGap >= 0 && - localMeterEdits.readingVariation >= 0 && - (localMeterEdits.readingDuplication >= 1 && localMeterEdits.readingDuplication <= 9) && - localMeterEdits.readingFrequency !== '' && - localMeterEdits.minVal >= MIN_VAL && - localMeterEdits.minVal <= localMeterEdits.maxVal && - localMeterEdits.maxVal <= MAX_VAL && - moment(localMeterEdits.minDate).isValid() && - moment(localMeterEdits.maxDate).isValid() && - moment(localMeterEdits.minDate).isSameOrAfter(MIN_DATE_MOMENT) && - moment(localMeterEdits.minDate).isSameOrBefore(moment(localMeterEdits.maxDate)) && - moment(localMeterEdits.maxDate).isSameOrBefore(MAX_DATE_MOMENT) && - (localMeterEdits.maxError >= 0 && localMeterEdits.maxError <= MAX_ERRORS) -} \ No newline at end of file diff --git a/src/client/app/components/meters/MeterViewComponent.tsx b/src/client/app/components/meters/MeterViewComponent.tsx index a2a08d0f0..8007060a3 100644 --- a/src/client/app/components/meters/MeterViewComponent.tsx +++ b/src/client/app/components/meters/MeterViewComponent.tsx @@ -1,29 +1,21 @@ /* 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/. */ + * 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 { Button } from 'reactstrap'; -import { State } from 'types/redux/state'; import { useState } from 'react'; -import { useSelector } from 'react-redux'; -import EditMeterModalComponent from './EditMeterModalComponent'; -import { MeterData } from 'types/redux/meters'; -import translate from '../../utils/translate'; import { FormattedMessage } from 'react-intl'; -import { isRoleAdmin } from '../../utils/hasPermissions'; -import { CurrentUserState } from 'types/redux/currentUser'; +import { Button } from 'reactstrap'; +import { MeterData } from 'types/redux/meters'; +import { useAppSelector } from '../../redux/hooks'; +import { selectGraphicName, selectUnitName } from '../../redux/selectors/adminSelectors'; import '../../styles/card-page.css'; -import { UnitData } from '../../types/redux/units'; -import { noUnitTranslated } from '../../utils/input'; +import translate from '../../utils/translate'; +import EditMeterModalComponentWIP from './EditMeterModalComponent'; +import { selectIsAdmin } from '../../reducers/currentUser'; interface MeterViewComponentProps { meter: MeterData; - currentUser: CurrentUserState; - // These two aren't used in this component but are passed to the edit component - // This is done to avoid having to recalculate the possible units sets in each view component - possibleMeterUnits: Set; - possibleGraphicUnits: Set; } /** @@ -34,31 +26,21 @@ interface MeterViewComponentProps { export default function MeterViewComponent(props: MeterViewComponentProps) { // Edit Modal Show const [showEditModal, setShowEditModal] = useState(false); + // Check for admin status + const loggedInAsAdmin = useAppSelector(selectIsAdmin); + + + // Set up to display the units associated with the meter as the unit identifier. + // This is the unit associated with the meter. + const unitName = useAppSelector(state => selectUnitName(state, props.meter.id)) + // This is the default graphic unit associated with the meter. See above for how code works. + const graphicName = useAppSelector(state => selectGraphicName(state, props.meter.id)) const handleShow = () => { setShowEditModal(true); } const handleClose = () => { setShowEditModal(false); } - - // current user state - const currentUser = useSelector((state: State) => state.currentUser.profile); - // Check for admin status - const loggedInAsAdmin = (currentUser !== null) && isRoleAdmin(currentUser.role); - - // Set up to display the units associated with the meter as the unit identifier. - // current unit state - const currentUnitState = useSelector((state: State) => state.units.units); - // This is the unit associated with the meter. - // The first test of length is because the state may not yet be set when loading. This should not be seen - // since the state should be set and the page redrawn so just use 'no unit'. - // The second test of -99 is for meters without units. - const unitName = (Object.keys(currentUnitState).length === 0 || props.meter.unitId === -99) ? - noUnitTranslated().identifier : currentUnitState[props.meter.unitId].identifier; - // This is the default graphic unit associated with the meter. See above for how code works. - const graphicName = (Object.keys(currentUnitState).length === 0 || props.meter.defaultGraphicUnit === -99) ? - noUnitTranslated().identifier : currentUnitState[props.meter.defaultGraphicUnit].identifier; - // Only display limited data if not an admin. return (
@@ -98,12 +80,11 @@ export default function MeterViewComponent(props: MeterViewComponentProps) { {/* Creates a child MeterModalEditComponent */} - + />
}
diff --git a/src/client/app/components/meters/MeterViewComponentWIP.tsx b/src/client/app/components/meters/MeterViewComponentWIP.tsx deleted file mode 100644 index c0154e6e0..000000000 --- a/src/client/app/components/meters/MeterViewComponentWIP.tsx +++ /dev/null @@ -1,92 +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 * as React from 'react'; -import { useState } from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Button } from 'reactstrap'; -import { MeterData } from 'types/redux/meters'; -import { useAppSelector } from '../../redux/hooks'; -import { selectGraphicName, selectUnitName } from '../../redux/selectors/adminSelectors'; -import '../../styles/card-page.css'; -import translate from '../../utils/translate'; -import EditMeterModalComponentWIP from './EditMeterModalComponentWIP'; -import { selectIsAdmin } from '../../reducers/currentUser'; - -interface MeterViewComponentProps { - meter: MeterData; -} - -/** - * Defines the meter info card - * @param props component props - * @returns Meter info card element - */ -export default function MeterViewComponent(props: MeterViewComponentProps) { - // Edit Modal Show - const [showEditModal, setShowEditModal] = useState(false); - // Check for admin status - const loggedInAsAdmin = useAppSelector(selectIsAdmin); - - - // Set up to display the units associated with the meter as the unit identifier. - // This is the unit associated with the meter. - const unitName = useAppSelector(state => selectUnitName(state, props.meter.id)) - // This is the default graphic unit associated with the meter. See above for how code works. - const graphicName = useAppSelector(state => selectGraphicName(state, props.meter.id)) - const handleShow = () => { - setShowEditModal(true); - } - const handleClose = () => { - setShowEditModal(false); - } - // Only display limited data if not an admin. - return ( -
-
- {props.meter.identifier} -
- {loggedInAsAdmin && -
- {props.meter.name} -
- } -
- {unitName} -
-
- {graphicName} -
- {loggedInAsAdmin && -
- {translate(`TrueFalseType.${props.meter.enabled.toString()}`)} -
- } - {loggedInAsAdmin && -
- {translate(`TrueFalseType.${props.meter.displayable.toString()}`)} -
- } - {loggedInAsAdmin && -
- {/* Only show first 30 characters so card does not get too big. Should limit to one line. Check in case null. */} - {props.meter.note?.slice(0, 29)} -
- } - {loggedInAsAdmin && -
- - {/* Creates a child MeterModalEditComponent */} - -
- } -
- ); -} diff --git a/src/client/app/components/meters/MetersDetailComponent.tsx b/src/client/app/components/meters/MetersDetailComponent.tsx index a6d635f02..21cf16284 100644 --- a/src/client/app/components/meters/MetersDetailComponent.tsx +++ b/src/client/app/components/meters/MetersDetailComponent.tsx @@ -2,69 +2,29 @@ * 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 _ from 'lodash'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -import TooltipHelpComponent from '../../components/TooltipHelpComponent'; +import TooltipHelpComponent from '../TooltipHelpComponent'; import { useAppSelector } from '../../redux/hooks'; +import { selectIsAdmin } from '../../reducers/currentUser'; import { selectVisibleMeterAndGroupData } from '../../redux/selectors/adminSelectors'; import '../../styles/card-page.css'; -import { MeterData } from '../../types/redux/meters'; -import { UnitData, UnitType } from '../../types/redux/units'; -import { noUnitTranslated, potentialGraphicUnits } from '../../utils/input'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import CreateMeterModalComponent from './CreateMeterModalComponent'; -import MeterViewComponent from './MeterViewComponent'; - -import { selectCurrentUser, selectIsAdmin } from '../../reducers/currentUser'; -import { selectUnitDataById } from '../../redux/api/unitsApi'; +import CreateMeterModalComponentWIP from './CreateMeterModalComponent'; +import MeterViewComponentWIP from './MeterViewComponent'; /** * Defines the meters page card view * @returns Meters page element */ export default function MetersDetailComponent() { - // current user state - const currentUserState = useAppSelector(selectCurrentUser); // Check for admin status const isAdmin = useAppSelector(selectIsAdmin); - // We only want displayable meters if non-admins because they still have // non-displayable in state. const { visibleMeters } = useAppSelector(selectVisibleMeterAndGroupData); - // Units state - const unitDataById = useAppSelector(selectUnitDataById); - - // TODO? Convert into Selector? - // Possible Meter Units to use - let possibleMeterUnits = new Set(); - // The meter unit can be any unit of type meter. - Object.values(unitDataById).forEach(unit => { - if (unit.typeOfUnit == UnitType.meter) { - possibleMeterUnits.add(unit); - } - }); - // Put in alphabetical order. - possibleMeterUnits = new Set(_.sortBy(Array.from(possibleMeterUnits), unit => unit.identifier.toLowerCase(), 'asc')); - // The default graphic unit can also be no unit/-99 but that is not desired so put last in list. - possibleMeterUnits.add(noUnitTranslated()); - - // Possible graphic units to use - const possibleGraphicUnits = potentialGraphicUnits(unitDataById); - - const titleStyle: React.CSSProperties = { - textAlign: 'center' - }; - - const tooltipStyle = { - display: 'inline-block', - fontSize: '50%', - // Switch help depending if admin or not. - tooltipMeterView: isAdmin ? 'help.admin.meterview' : 'help.meters.meterview' - }; - return (
@@ -73,33 +33,42 @@ export default function MetersDetailComponent() {

- +

{isAdmin &&
- {/* The actual button for create is inside this component. */} - +
} {
- {visibleMeters.map(MeterData => ( - - ))} + {/* Create a MeterViewComponent for each MeterData in Meters State after sorting by identifier */} + {/* Optional Chaining to prevent from crashing upon startup race conditions*/} + {Object.values(visibleMeters) + .map(MeterData => ( + + ))}
}
); } + +const titleStyle: React.CSSProperties = { + textAlign: 'center' +}; + + + +const tooltipStyle = { + display: 'inline-block', + fontSize: '50%' +}; + +// Switch help depending if admin or not. +const getToolTipMessage = (isAdmin: boolean) => isAdmin ? 'help.admin.meterview' : 'help.meters.meterview' \ No newline at end of file diff --git a/src/client/app/components/meters/MetersDetailComponentWIP.tsx b/src/client/app/components/meters/MetersDetailComponentWIP.tsx deleted file mode 100644 index 25839cca1..000000000 --- a/src/client/app/components/meters/MetersDetailComponentWIP.tsx +++ /dev/null @@ -1,74 +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 * as React from 'react'; -import { FormattedMessage } from 'react-intl'; -import TooltipHelpComponent from '../../components/TooltipHelpComponent'; -import { useAppSelector } from '../../redux/hooks'; -import { selectIsAdmin } from '../../reducers/currentUser'; -import { selectVisibleMeterAndGroupData } from '../../redux/selectors/adminSelectors'; -import '../../styles/card-page.css'; -import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import CreateMeterModalComponentWIP from './CreateMeterModalComponentWIP'; -import MeterViewComponentWIP from './MeterViewComponentWIP'; - -/** - * Defines the meters page card view - * @returns Meters page element - */ -export default function MetersDetailComponent() { - - // Check for admin status - const isAdmin = useAppSelector(selectIsAdmin); - // We only want displayable meters if non-admins because they still have - // non-displayable in state. - const { visibleMeters } = useAppSelector(selectVisibleMeterAndGroupData); - - return ( -
- - -
-

- -
- -
-

- {isAdmin && -
- -
- } - { -
- {/* Create a MeterViewComponent for each MeterData in Meters State after sorting by identifier */} - {/* Optional Chaining to prevent from crashing upon startup race conditions*/} - {Object.values(visibleMeters) - .map(MeterData => ( - - ))} -
- } -
-
- ); -} - -const titleStyle: React.CSSProperties = { - textAlign: 'center' -}; - - - -const tooltipStyle = { - display: 'inline-block', - fontSize: '50%' -}; - -// Switch help depending if admin or not. -const getToolTipMessage = (isAdmin: boolean) => isAdmin ? 'help.admin.meterview' : 'help.meters.meterview' \ No newline at end of file diff --git a/src/client/app/components/router/ErrorComponent.tsx b/src/client/app/components/router/ErrorComponent.tsx index fb4c52705..b9b9d6ea6 100644 --- a/src/client/app/components/router/ErrorComponent.tsx +++ b/src/client/app/components/router/ErrorComponent.tsx @@ -5,6 +5,7 @@ import * as React from 'react'; import { useNavigate } from 'react-router-dom'; import { Button } from 'reactstrap'; +import AppLayout from '../../components/AppLayout'; /** * @returns A simple loading spinner used to indicate that the startup init sequence is in progress @@ -12,20 +13,22 @@ import { Button } from 'reactstrap'; export default function ErrorComponent() { const nav = useNavigate(); return ( -
- {/* TODO make a good looking error page. This is a placeholder for now. */} -

- Oops! An error has occurred. -

- -
+ +
+ {/* TODO make a good looking error page. This is a placeholder for now. */} +

+ Oops! An error has occurred. +

+ +
+
) } diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index b002088f0..804ae9fb4 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -1419,11 +1419,9 @@ const LocaleTranslationData = { } } -// Infer +// Infer for completions on translate() export default LocaleTranslationData as typeof LocaleTranslationData; export type TranslationKey = keyof typeof LocaleTranslationData -// All locales should share the same keys, but intersection over all to be safe? -// Will probably error when forgetting to add same key to all locales when using translate() export type LocaleDataKey = keyof typeof LocaleTranslationData['en'] | keyof typeof LocaleTranslationData['es'] | From 72f111bb271848f9df94f6fb33f3bb2b63ce50a6 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Sun, 14 Jan 2024 23:21:40 +0000 Subject: [PATCH 052/131] Cleanup Legacy code - Rename slices - Mark files for deletion --- src/client/app/actions/mapReadings.ts | 84 -- src/client/app/actions/options.ts | 12 - src/client/app/actions/radarReadings.ts | 160 ---- src/client/app/actions/unsavedWarning.ts | 27 - .../components/AreaUnitSelectComponent.tsx | 4 +- .../app/components/BarChartComponent.tsx | 4 +- .../app/components/BarControlsComponent.tsx | 4 +- .../app/components/ChartSelectComponent.tsx | 4 +- .../components/CompareControlsComponent.tsx | 4 +- .../app/components/DashboardComponent.tsx | 6 +- .../app/components/DateRangeComponent.tsx | 9 +- .../app/components/ErrorBarComponent.tsx | 2 +- src/client/app/components/ExportComponent.tsx | 4 +- src/client/app/components/FooterComponent.tsx | 2 +- .../components/GraphicRateMenuComponent.tsx | 4 +- .../app/components/HeaderButtonsComponent.tsx | 16 +- src/client/app/components/HeaderComponent.tsx | 4 +- .../app/components/HistoryComponent.tsx | 4 +- .../components/LanguageSelectorComponent.tsx | 7 +- .../app/components/LineChartComponent.tsx | 4 +- .../app/components/MapChartComponent.tsx | 4 +- .../app/components/MapControlsComponent.tsx | 4 +- .../MeterAndGroupSelectComponent.tsx | 4 +- .../app/components/MeterDropDownComponent.tsx | 2 +- .../components/MultiCompareChartComponent.tsx | 4 +- .../app/components/RadarChartComponent.tsx | 4 +- .../ReadingsPerDaySelectComponent.tsx | 4 +- src/client/app/components/RouteComponent.tsx | 21 +- src/client/app/components/ThreeDComponent.tsx | 4 +- .../app/components/ThreeDPillComponent.tsx | 4 +- .../app/components/TooltipHelpComponent.tsx | 4 +- .../app/components/UIOptionsComponent.tsx | 4 +- .../app/components/UnitSelectComponent.tsx | 4 +- .../components/UnsavedWarningComponent.tsx | 218 ++--- .../components/UnsavedWarningComponentWIP.tsx | 76 -- .../app/components/admin/AdminComponent.tsx | 5 +- .../components/admin/CreateUserComponent.tsx | 132 +-- .../admin/CreateUserComponentWIP.tsx | 86 -- .../components/admin/PreferencesComponent.tsx | 817 +++++++----------- .../admin/PreferencesComponentWIP.tsx | 353 -------- .../components/admin/UsersDetailComponent.tsx | 135 +-- .../admin/UsersDetailComponentWIP.tsx | 140 --- .../conversion/ConversionViewComponent.tsx | 6 +- .../conversion/ConversionsDetailComponent.tsx | 8 +- .../CreateConversionModalComponent.tsx | 2 +- .../EditConversionModalComponent.tsx | 2 +- .../groups/CreateGroupModalComponent.tsx | 4 +- .../groups/EditGroupModalComponent.tsx | 6 +- .../components/groups/GroupViewComponent.tsx | 10 +- .../groups/GroupsDetailComponent.tsx | 12 +- .../maps/MapCalibrationInitiateComponent.tsx | 2 +- .../app/components/maps/MapViewComponent.tsx | 35 +- .../components/maps/MapsDetailComponent.tsx | 4 +- .../meters/CreateMeterModalComponent.tsx | 2 +- .../meters/EditMeterModalComponent.tsx | 5 +- .../components/meters/MeterViewComponent.tsx | 8 +- .../meters/MetersDetailComponent.tsx | 12 +- .../components/router/GraphLinkComponent.tsx | 8 +- .../unit/CreateUnitModalComponent.tsx | 19 +- .../unit/EditUnitModalComponent.tsx | 30 +- .../components/unit/UnitsDetailComponent.tsx | 2 +- .../app/containers/BarChartContainer.ts | 12 +- .../app/containers/ChartLinkContainer.ts | 2 +- .../app/containers/CompareChartContainer.ts | 9 +- .../app/containers/LineChartContainer.ts | 9 +- .../app/containers/MapChartContainer.ts | 9 +- .../app/containers/MeterDropdownContainer.ts | 24 - .../app/containers/RadarChartComponent.tsx | 335 ------- .../app/containers/RadarChartContainer.ts | 6 + .../app/containers/UnsavedWarningContainer.ts | 27 - .../containers/admin/CreateUserContainer.tsx | 78 -- .../containers/admin/PreferencesContainer.ts | 90 -- .../containers/admin/UsersDetailContainer.tsx | 93 -- .../MapCalibrationChartDisplayContainer.ts | 2 +- .../MapCalibrationInfoDisplayContainer.ts | 4 +- .../maps/MapCalibrationInitiateContainer.ts | 2 +- .../app/containers/maps/MapViewContainer.tsx | 2 +- .../containers/maps/MapsDetailContainer.tsx | 2 +- src/client/app/index.tsx | 2 +- src/client/app/initScript.ts | 54 -- src/client/app/reducers/barReadings.ts | 137 --- src/client/app/reducers/compareReadings.ts | 137 --- src/client/app/reducers/conversions.ts | 82 -- src/client/app/reducers/groups.ts | 80 -- src/client/app/reducers/lineReadings.ts | 123 --- src/client/app/reducers/meters.ts | 62 -- src/client/app/reducers/options.ts | 35 - src/client/app/reducers/radarReadings.ts | 127 --- src/client/app/reducers/units.ts | 56 -- src/client/app/reducers/unsavedWarning.ts | 75 -- src/client/app/{ => redux}/actions/admin.ts | 25 +- .../app/{ => redux}/actions/conversions.ts | 16 +- .../app/{ => redux}/actions/currentUser.ts | 17 +- src/client/app/{ => redux}/actions/graph.ts | 6 +- src/client/app/{ => redux}/actions/groups.ts | 8 + src/client/app/{ => redux}/actions/logs.ts | 4 +- src/client/app/{ => redux}/actions/map.ts | 18 +- src/client/app/{ => redux}/actions/meters.ts | 21 +- src/client/app/{ => redux}/actions/units.ts | 21 +- src/client/app/redux/api/authApi.ts | 2 +- src/client/app/redux/api/baseApi.ts | 3 +- src/client/app/redux/api/conversionsApi.ts | 13 +- src/client/app/redux/api/groupsApi.ts | 32 +- src/client/app/redux/api/readingsApi.ts | 10 +- src/client/app/redux/api/unitsApi.ts | 47 +- src/client/app/redux/api/userApi.ts | 2 +- src/client/app/redux/componentHooks.ts | 33 +- .../app/redux/middleware/graphHistory.ts | 2 +- src/client/app/{ => redux}/reducers/maps.ts | 9 +- .../app/redux/{hooks.ts => reduxHooks.ts} | 0 .../index.ts => redux/rootReducer.ts} | 16 +- .../app/redux/selectors/adminSelectors.ts | 2 +- .../selectors/authVisibilitySelectors.ts | 2 +- .../redux/selectors/chartQuerySelectors.ts | 2 +- .../app/redux/selectors/threeDSelectors.ts | 2 +- src/client/app/redux/selectors/uiSelectors.ts | 4 +- .../thunkSlice.ts => sliceCreators.ts} | 0 .../admin.ts => redux/slices/adminSlice.ts} | 14 +- .../slices}/appStateSlice.ts | 51 +- .../slices/currentUserSlice.ts} | 11 +- .../graph.ts => redux/slices/graphSlice.ts} | 12 +- src/client/app/store.ts | 2 +- .../app/utils/api/ConversionArrayApi.ts | 6 + src/client/app/utils/api/ConversionsApi.ts | 13 +- src/client/app/utils/api/GroupsApi.ts | 6 + src/client/app/utils/api/MapsApi.ts | 12 +- src/client/app/utils/api/MetersApi.ts | 6 +- src/client/app/utils/api/PreferencesApi.ts | 5 + src/client/app/utils/api/ReadingsApi.ts | 6 + src/client/app/utils/api/UsersApi.ts | 6 +- src/client/app/utils/api/VerificationApi.ts | 10 +- src/client/app/utils/api/VersionApi.ts | 5 + src/client/app/utils/api/unitsApi.ts | 14 +- src/client/app/utils/calibration.ts | 2 +- .../app/utils/determineCompatibleUnits.ts | 4 +- src/client/app/utils/translate.ts | 6 +- 136 files changed, 1041 insertions(+), 3714 deletions(-) delete mode 100644 src/client/app/actions/mapReadings.ts delete mode 100644 src/client/app/actions/options.ts delete mode 100644 src/client/app/actions/radarReadings.ts delete mode 100644 src/client/app/actions/unsavedWarning.ts delete mode 100644 src/client/app/components/UnsavedWarningComponentWIP.tsx delete mode 100644 src/client/app/components/admin/CreateUserComponentWIP.tsx delete mode 100644 src/client/app/components/admin/PreferencesComponentWIP.tsx delete mode 100644 src/client/app/components/admin/UsersDetailComponentWIP.tsx delete mode 100644 src/client/app/containers/MeterDropdownContainer.ts delete mode 100644 src/client/app/containers/RadarChartComponent.tsx delete mode 100644 src/client/app/containers/UnsavedWarningContainer.ts delete mode 100644 src/client/app/containers/admin/CreateUserContainer.tsx delete mode 100644 src/client/app/containers/admin/PreferencesContainer.ts delete mode 100644 src/client/app/containers/admin/UsersDetailContainer.tsx delete mode 100644 src/client/app/initScript.ts delete mode 100644 src/client/app/reducers/barReadings.ts delete mode 100644 src/client/app/reducers/compareReadings.ts delete mode 100644 src/client/app/reducers/conversions.ts delete mode 100644 src/client/app/reducers/groups.ts delete mode 100644 src/client/app/reducers/lineReadings.ts delete mode 100644 src/client/app/reducers/meters.ts delete mode 100644 src/client/app/reducers/options.ts delete mode 100644 src/client/app/reducers/radarReadings.ts delete mode 100644 src/client/app/reducers/units.ts delete mode 100644 src/client/app/reducers/unsavedWarning.ts rename src/client/app/{ => redux}/actions/admin.ts (90%) rename src/client/app/{ => redux}/actions/conversions.ts (92%) rename src/client/app/{ => redux}/actions/currentUser.ts (74%) rename src/client/app/{ => redux}/actions/graph.ts (81%) rename src/client/app/{ => redux}/actions/groups.ts (96%) rename src/client/app/{ => redux}/actions/logs.ts (92%) rename src/client/app/{ => redux}/actions/map.ts (96%) rename src/client/app/{ => redux}/actions/meters.ts (87%) rename src/client/app/{ => redux}/actions/units.ts (82%) rename src/client/app/{ => redux}/reducers/maps.ts (95%) rename src/client/app/redux/{hooks.ts => reduxHooks.ts} (100%) rename src/client/app/{reducers/index.ts => redux/rootReducer.ts} (54%) rename src/client/app/redux/{slices/thunkSlice.ts => sliceCreators.ts} (100%) rename src/client/app/{reducers/admin.ts => redux/slices/adminSlice.ts} (93%) rename src/client/app/{reducers => redux/slices}/appStateSlice.ts (68%) rename src/client/app/{reducers/currentUser.ts => redux/slices/currentUserSlice.ts} (87%) rename src/client/app/{reducers/graph.ts => redux/slices/graphSlice.ts} (97%) diff --git a/src/client/app/actions/mapReadings.ts b/src/client/app/actions/mapReadings.ts deleted file mode 100644 index 8a60f00ea..000000000 --- a/src/client/app/actions/mapReadings.ts +++ /dev/null @@ -1,84 +0,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 */ - -fetchGroupMapReadings(); - -/* 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 { Dispatch, GetState, Thunk } from '../types/redux/actions'; -import { TimeInterval } from '../../../common/TimeInterval'; -import * as moment from 'moment'; - -import { readingsApi } from '../utils/api'; - -/** - * Fetch the data for the given meters over the given interval. Fully manages the Redux lifecycle. - * Reads bar duration from the state. - * @param meterIDs The IDs of the meters whose data should be fetched - * @param timeInterval The time interval over which data should be fetched - * @param duration The length of time covered in this timeInterval - * @param unitID the ID of the unit for which to check - */ -function fetchMeterMapReadings(meterIDs: number[], timeInterval: TimeInterval, duration: moment.Duration, unitID: number): Thunk { - return async (dispatch: Dispatch) => { - dispatch(requestMeterBarReadings(meterIDs, timeInterval, duration, unitID)); - const meterMapReadings = await readingsApi.meterBarReadings(meterIDs, timeInterval, Math.round(duration.asDays()), unitID); - dispatch(receiveMeterBarReadings(meterIDs, timeInterval, duration, unitID, meterMapReadings)); - }; -} - -/** - * Fetch the data for the given groups over the given interval. Fully manages the Redux lifecycle. - * Reads bar duration from the state. - * @param groupIDs The IDs of the groups whose data should be fetched - * @param timeInterval The time interval over which data should be fetched - * @param duration The length of time covered in this timeInterval - * @param unitID the ID of the unit for which to check - */ - -function fetchGroupMapReadings(groupIDs: number[], timeInterval: TimeInterval, duration: moment.Duration, unitID: number): Thunk { - return async (dispatch: Dispatch) => { - dispatch(requestGroupBarReadings(groupIDs, timeInterval, duration, unitID)); - const groupMapReadings = await readingsApi.groupBarReadings(groupIDs, timeInterval, Math.round(duration.asDays()), unitID); - dispatch(receiveGroupBarReadings(groupIDs, timeInterval, duration, unitID, groupMapReadings)); - return Promise.resolve() - - }; -} - -/** - * Fetches readings for the map chart of all selected meters and groups, if needed. - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - */ -export function fetchNeededMapReadings(timeInterval: TimeInterval, unitID: number): Thunk { - return (dispatch: Dispatch, getState: GetState) => { - const state = getState(); - const promises: Promise[] = []; - const mapDuration = (timeInterval.equals(TimeInterval.unbounded())) ? moment.duration(4, 'weeks') - : moment.duration(timeInterval.duration('days'), 'days'); - // Determine which meters are missing data for this time interval - const meterIDsToFetchForMap = state.graph.selectedMeters.filter( - id => shouldFetchMeterBarReadings(state, id, timeInterval, mapDuration, unitID) - ); - // Fetch data for any missing meters - if (meterIDsToFetchForMap.length > 0) { - promises.push(dispatch(fetchMeterMapReadings(meterIDsToFetchForMap, timeInterval, mapDuration, unitID))); - } - - // Determine which groups are missing data for this time interval - const groupIDsToFetchForMap = state.graph.selectedGroups.filter( - id => shouldFetchGroupBarReadings(state, id, timeInterval, mapDuration, unitID) - ); - // Fetch data for any missing groups - if (groupIDsToFetchForMap.length > 0) { - promises.push(dispatch(fetchGroupMapReadings(groupIDsToFetchForMap, timeInterval, mapDuration, unitID))); - } - return Promise.all(promises); - }; -} diff --git a/src/client/app/actions/options.ts b/src/client/app/actions/options.ts deleted file mode 100644 index 1495b232a..000000000 --- a/src/client/app/actions/options.ts +++ /dev/null @@ -1,12 +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 { LanguageTypes } from '../types/redux/i18n'; -import * as moment from 'moment'; -import { optionsSlice } from '../reducers/options'; - -export function updateSelectedLanguage(selectedLanguage: LanguageTypes) { - moment.locale(selectedLanguage); - return optionsSlice.actions.updateSelectedLanguage(selectedLanguage); -} diff --git a/src/client/app/actions/radarReadings.ts b/src/client/app/actions/radarReadings.ts deleted file mode 100644 index b846cdce6..000000000 --- a/src/client/app/actions/radarReadings.ts +++ /dev/null @@ -1,160 +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 { TimeInterval } from '../../../common/TimeInterval'; -import { ActionType, Thunk, Dispatch, GetState } from '../types/redux/actions'; -import { State } from '../types/redux/state'; -import * as t from '../types/redux/radarReadings'; -import { readingsApi } from '../utils/api'; -import { LineReadings } from '../types/readings'; - -/** - * @param state the Redux state - * @param meterID the ID of the meter to check - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - * @returns True if the readings for the given meter, time duration and unit are missing; false otherwise. - */ -function shouldFetchMeterRadarReadings(state: State, meterID: number, timeInterval: TimeInterval, unitID: number): boolean { - const timeIntervalIndex = timeInterval.toString(); - - const readingsForID = state.readings.line.byMeterID[meterID]; - if (readingsForID === undefined) { - return true; - } - - const readingsForTimeInterval = readingsForID[timeIntervalIndex]; - if (readingsForTimeInterval === undefined) { - return true; - } - - const readingsForUnit = readingsForTimeInterval[unitID]; - if (readingsForUnit === undefined) { - return true; - } - - return !readingsForUnit.isFetching; -} - -/** - * @param state the Redux state - * @param groupID the ID of the group to check - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - * @returns True if the readings for the given group, time duration and unit are missing; false otherwise. - */ -function shouldFetchGroupRadarReadings(state: State, groupID: number, timeInterval: TimeInterval, unitID: number): boolean { - const timeIntervalIndex = timeInterval.toString(); - - const readingsForID = state.readings.line.byGroupID[groupID]; - if (readingsForID === undefined) { - return true; - } - - const readingsForTimeInterval = readingsForID[timeIntervalIndex]; - if (readingsForTimeInterval === undefined) { - return true; - } - - const readingsForUnit = readingsForTimeInterval[unitID]; - if (readingsForUnit === undefined) { - return true; - } - - return !readingsForUnit.isFetching; -} - -/** - * @param meterIDs the IDs of the meters to get readings - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - */ -function requestMeterRadarReadings(meterIDs: number[], timeInterval: TimeInterval, unitID: number): t.RequestMeterRadarReadingAction { - return { type: ActionType.RequestMeterLineReadings, meterIDs, timeInterval, unitID }; -} - -/** - * @param groupIDs the IDs of the groups to get readings - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - */ -function requestGroupRadarReadings(groupIDs: number[], timeInterval: TimeInterval, unitID: number): t.RequestGroupRadarReadingAction { - return { type: ActionType.RequestGroupLineReadings, groupIDs, timeInterval, unitID }; -} -/** - * @param meterIDs the IDs of the meters to get readings - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - * @param readings the readings for the given meters - */ -function receiveMeterRadarReadings( - meterIDs: number[], timeInterval: TimeInterval, unitID: number, readings: LineReadings): t.ReceiveMeterRadarReadingAction { - return { type: ActionType.ReceiveMeterLineReadings, meterIDs, timeInterval, unitID, readings }; -} - -/** - * @param groupIDs the IDs of the groups to get readings - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - * @param readings the readings for the given groups - */ -function receiveGroupLineReadings( - groupIDs: number[], timeInterval: TimeInterval, unitID: number, readings: LineReadings): t.ReceiveGroupRadarReadingAction { - return { type: ActionType.ReceiveGroupLineReadings, groupIDs, timeInterval, unitID, readings }; -} - -/** - * @param meterIDs the IDs of the meters to get readings - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - */ -function fetchMeterRadarReadings(meterIDs: number[], timeInterval: TimeInterval, unitID: number): Thunk { - return async (dispatch: Dispatch) => { - dispatch(requestMeterRadarReadings(meterIDs, timeInterval, unitID)); - const meterLineReadings = await readingsApi.meterRadarReadings(meterIDs, timeInterval, unitID); - dispatch(receiveMeterRadarReadings(meterIDs, timeInterval, unitID, meterLineReadings)); - }; -} - -/** - * @param groupIDs the IDs of the groups to get readings - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - */ -function fetchGroupRadarReadings(groupIDs: number[], timeInterval: TimeInterval, unitID: number): Thunk { - return async (dispatch: Dispatch) => { - dispatch(requestGroupRadarReadings(groupIDs, timeInterval, unitID)); - const groupLineReadings = await readingsApi.groupRadarReadings(groupIDs, timeInterval, unitID); - dispatch(receiveGroupLineReadings(groupIDs, timeInterval, unitID, groupLineReadings)); - }; -} -/** - * Fetches readings for the radar chart of all selected meters and groups, if needed. - * @param timeInterval the interval over which to check - * @param unitID the ID of the unit for which to check - */ -export function fetchNeededRadarReadings(timeInterval: TimeInterval, unitID: number): Thunk { - return (dispatch: Dispatch, getState: GetState) => { - const state = getState(); - const promises: Array> = []; - - // Determine which meters are missing data for this time interval - const meterIDsToFetchForLine = state.graph.selectedMeters.filter( - id => shouldFetchMeterRadarReadings(state, id, timeInterval, unitID) - ); - if (meterIDsToFetchForLine.length > 0) { - promises.push(dispatch(fetchMeterRadarReadings(meterIDsToFetchForLine, timeInterval, unitID))); - } - - // Determine which groups are missing data for this time interval - const groupIDsToFetchForLine = state.graph.selectedGroups.filter( - id => shouldFetchGroupRadarReadings(state, id, timeInterval, unitID) - ); - if (groupIDsToFetchForLine.length > 0) { - promises.push(dispatch(fetchGroupRadarReadings(groupIDsToFetchForLine, timeInterval, unitID))); - } - - return Promise.all(promises); - }; -} diff --git a/src/client/app/actions/unsavedWarning.ts b/src/client/app/actions/unsavedWarning.ts deleted file mode 100644 index ac5cbe943..000000000 --- a/src/client/app/actions/unsavedWarning.ts +++ /dev/null @@ -1,27 +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/. */ - - -// /** -// * Notify that there are unsaved changes -// * @param removeFunction The function to remove local changes -// * @param submitFunction The function to submit unsaved changes -// */ -// export function updateUnsavedChanges(removeFunction: any, submitFunction: any): t.UpdateUnsavedChangesAction { -// return { type: ActionType.UpdateUnsavedChanges, removeFunction, submitFunction }; -// } - -// /** -// * Notify that there are no unsaved changes -// */ -// export function removeUnsavedChanges(): t.RemoveUnsavedChangesAction { -// return { type: ActionType.RemoveUnsavedChanges }; -// } - -// /** -// * Notify that the logout button was clicked or unclicked -// */ -// export function flipLogOutState(): t.FlipLogOutStateAction { -// return { type: ActionType.FlipLogOutState }; -// } \ No newline at end of file diff --git a/src/client/app/components/AreaUnitSelectComponent.tsx b/src/client/app/components/AreaUnitSelectComponent.tsx index 8d6cb4eb3..a221cb0ed 100644 --- a/src/client/app/components/AreaUnitSelectComponent.tsx +++ b/src/client/app/components/AreaUnitSelectComponent.tsx @@ -6,8 +6,8 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { useDispatch } from 'react-redux'; import Select from 'react-select'; -import { useAppSelector } from '../redux/hooks'; -import { graphSlice, selectGraphState } from '../reducers/graph'; +import { useAppSelector } from '../redux/reduxHooks'; +import { graphSlice, selectGraphState } from '../redux/slices/graphSlice'; import { selectUnitDataById } from '../redux/api/unitsApi'; import { StringSelectOption } from '../types/items'; import { UnitRepresentType } from '../types/redux/units'; diff --git a/src/client/app/components/BarChartComponent.tsx b/src/client/app/components/BarChartComponent.tsx index cb5624a81..6041d4c49 100644 --- a/src/client/app/components/BarChartComponent.tsx +++ b/src/client/app/components/BarChartComponent.tsx @@ -11,12 +11,12 @@ import { TimeInterval } from '../../../common/TimeInterval'; import { graphSlice, selectAreaUnit, selectBarStacking, selectBarWidthDays, selectGraphAreaNormalization, selectSelectedGroups, selectSelectedMeters, selectSelectedUnit -} from '../reducers/graph'; +} from '../redux/slices/graphSlice'; import { selectGroupDataById } from '../redux/api/groupsApi'; import { selectMeterDataById } from '../redux/api/metersApi'; import { readingsApi } from '../redux/api/readingsApi'; import { selectUnitDataById } from '../redux/api/unitsApi'; -import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { selectBarChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; import { DataType } from '../types/Datasources'; import { UnitRepresentType } from '../types/redux/units'; diff --git a/src/client/app/components/BarControlsComponent.tsx b/src/client/app/components/BarControlsComponent.tsx index a5ee87dd7..239285249 100644 --- a/src/client/app/components/BarControlsComponent.tsx +++ b/src/client/app/components/BarControlsComponent.tsx @@ -3,8 +3,8 @@ import sliderWithoutTooltips, { createSliderWithTooltip } from 'rc-slider'; import 'rc-slider/assets/index.css'; import * as React from 'react'; import { Button, ButtonGroup } from 'reactstrap'; -import { graphSlice, selectBarStacking, selectBarWidthDays } from '../reducers/graph'; -import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { graphSlice, selectBarStacking, selectBarWidthDays } from '../redux/slices/graphSlice'; +import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; diff --git a/src/client/app/components/ChartSelectComponent.tsx b/src/client/app/components/ChartSelectComponent.tsx index f1c83a45c..da44fbcf8 100644 --- a/src/client/app/components/ChartSelectComponent.tsx +++ b/src/client/app/components/ChartSelectComponent.tsx @@ -6,8 +6,8 @@ import * as React from 'react'; import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; -import { useAppDispatch, useAppSelector } from '../redux/hooks'; -import { graphSlice, selectChartToRender } from '../reducers/graph'; +import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; +import { graphSlice, selectChartToRender } from '../redux/slices/graphSlice'; import { ChartTypes } from '../types/redux/graph'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; diff --git a/src/client/app/components/CompareControlsComponent.tsx b/src/client/app/components/CompareControlsComponent.tsx index 709747396..dc0574c85 100644 --- a/src/client/app/components/CompareControlsComponent.tsx +++ b/src/client/app/components/CompareControlsComponent.tsx @@ -1,8 +1,8 @@ import * as moment from 'moment'; import * as React from 'react'; import { Button, ButtonGroup, Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; -import { graphSlice, selectComparePeriod, selectSortingOrder } from '../reducers/graph'; -import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { graphSlice, selectComparePeriod, selectSortingOrder } from '../redux/slices/graphSlice'; +import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { ComparePeriod, SortingOrder } from '../utils/calculateCompare'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; diff --git a/src/client/app/components/DashboardComponent.tsx b/src/client/app/components/DashboardComponent.tsx index 977be964d..f11f2a181 100644 --- a/src/client/app/components/DashboardComponent.tsx +++ b/src/client/app/components/DashboardComponent.tsx @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { useAppSelector } from '../redux/hooks'; +import { useAppSelector } from '../redux/reduxHooks'; import { ChartTypes } from '../types/redux/graph'; import BarChartComponent from './BarChartComponent'; import HistoryComponent from './HistoryComponent'; @@ -12,8 +12,8 @@ import MapChartComponent from './MapChartComponent'; import MultiCompareChartComponent from './MultiCompareChartComponent'; import ThreeDComponent from './ThreeDComponent'; import UIOptionsComponent from './UIOptionsComponent'; -import { selectChartToRender } from '../reducers/graph'; -import { selectOptionsVisibility } from '../reducers/appStateSlice'; +import { selectChartToRender } from '../redux/slices/graphSlice'; +import { selectOptionsVisibility } from '../redux/slices/appStateSlice'; import RadarChartComponent from './RadarChartComponent'; /** diff --git a/src/client/app/components/DateRangeComponent.tsx b/src/client/app/components/DateRangeComponent.tsx index b749ef7a4..76a3de2c8 100644 --- a/src/client/app/components/DateRangeComponent.tsx +++ b/src/client/app/components/DateRangeComponent.tsx @@ -8,12 +8,13 @@ import { Value } from '@wojtekmaj/react-daterange-picker/dist/cjs/shared/types'; import * as React from 'react'; import 'react-calendar/dist/Calendar.css'; import { useDispatch } from 'react-redux'; -import { selectQueryTimeInterval, updateTimeInterval } from '../reducers/graph'; -import { useAppSelector } from '../redux/hooks'; +import { selectQueryTimeInterval, updateTimeInterval } from '../redux/slices/graphSlice'; +import { useAppSelector } from '../redux/reduxHooks'; import { Dispatch } from '../types/redux/actions'; import { dateRangeToTimeInterval, timeIntervalToDateRange } from '../utils/dateRangeCompatibility'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; +import { selectSelectedLanguage } from '../redux/slices/appStateSlice'; // Potential Fixes, for now omitted // import '../styles/DateRangeCustom.css' @@ -25,7 +26,7 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; export default function DateRangeComponent() { const dispatch: Dispatch = useDispatch(); const queryTimeInterval = useAppSelector(selectQueryTimeInterval); - const locale = useAppSelector(state => state.options.selectedLanguage); + const locale = useAppSelector(selectSelectedLanguage); const handleChange = (value: Value) => { dispatch(updateTimeInterval(dateRangeToTimeInterval(value))) } @@ -43,7 +44,7 @@ export default function DateRangeComponent() { minDate={new Date(1970, 0, 1)} maxDate={new Date()} locale={locale} // Formats Dates, and Calendar months base on locale - calendarIcon={null} // TODO Verify Behavior + calendarIcon={null} />

); diff --git a/src/client/app/components/ErrorBarComponent.tsx b/src/client/app/components/ErrorBarComponent.tsx index f8a83cea9..3e4334608 100644 --- a/src/client/app/components/ErrorBarComponent.tsx +++ b/src/client/app/components/ErrorBarComponent.tsx @@ -7,7 +7,7 @@ import { useSelector, useDispatch } from 'react-redux'; import { State } from '../types/redux/state'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; -import { graphSlice } from '../reducers/graph'; +import { graphSlice } from '../redux/slices/graphSlice'; /** * React Component rendering an Error Bar checkbox for toggle operation. diff --git a/src/client/app/components/ExportComponent.tsx b/src/client/app/components/ExportComponent.tsx index 138e180f9..500d0df58 100644 --- a/src/client/app/components/ExportComponent.tsx +++ b/src/client/app/components/ExportComponent.tsx @@ -11,7 +11,7 @@ import { selectGroupDataById } from '../redux/api/groupsApi'; import { selectMeterDataById } from '../redux/api/metersApi'; import { readingsApi } from '../redux/api/readingsApi'; import { selectUnitDataById } from '../redux/api/unitsApi'; -import { useAppSelector } from '../redux/hooks'; +import { useAppSelector } from '../redux/reduxHooks'; import { selectAllChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; import { UserRole } from '../types/items'; import { ConversionData } from '../types/redux/conversions'; @@ -23,7 +23,7 @@ import { barUnitLabel, lineUnitLabel } from '../utils/graphics'; import { hasToken } from '../utils/token'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; -import { selectGraphState, selectShowMinMax } from '../reducers/graph'; +import { selectGraphState, selectShowMinMax } from '../redux/slices/graphSlice'; /** * Creates export buttons and does code for handling export to CSV files. diff --git a/src/client/app/components/FooterComponent.tsx b/src/client/app/components/FooterComponent.tsx index d9cf3fa9d..63937de3e 100644 --- a/src/client/app/components/FooterComponent.tsx +++ b/src/client/app/components/FooterComponent.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { selectOEDVersion } from '../redux/api/versionApi'; -import { useAppSelector } from '../redux/hooks'; +import { useAppSelector } from '../redux/reduxHooks'; /** * diff --git a/src/client/app/components/GraphicRateMenuComponent.tsx b/src/client/app/components/GraphicRateMenuComponent.tsx index 81f3addcf..641220427 100644 --- a/src/client/app/components/GraphicRateMenuComponent.tsx +++ b/src/client/app/components/GraphicRateMenuComponent.tsx @@ -7,8 +7,8 @@ import { FormattedMessage } from 'react-intl'; import { useDispatch } from 'react-redux'; import Select from 'react-select'; import { selectUnitDataById } from '../redux/api/unitsApi'; -import { graphSlice, selectGraphState } from '../reducers/graph'; -import { useAppSelector } from '../redux/hooks'; +import { graphSlice, selectGraphState } from '../redux/slices/graphSlice'; +import { useAppSelector } from '../redux/reduxHooks'; import { SelectOption } from '../types/items'; import { LineGraphRate, LineGraphRates } from '../types/redux/graph'; import { UnitRepresentType } from '../types/redux/units'; diff --git a/src/client/app/components/HeaderButtonsComponent.tsx b/src/client/app/components/HeaderButtonsComponent.tsx index a0fdf422c..eaae4d243 100644 --- a/src/client/app/components/HeaderButtonsComponent.tsx +++ b/src/client/app/components/HeaderButtonsComponent.tsx @@ -8,18 +8,17 @@ 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 { selectOptionsVisibility, toggleOptionsVisibility } from '../reducers/appStateSlice'; -import { unsavedWarningSlice } from '../reducers/unsavedWarning'; +import { selectOptionsVisibility, toggleOptionsVisibility } from '../redux/slices/appStateSlice'; import { authApi } from '../redux/api/authApi'; import { selectOEDVersion } from '../redux/api/versionApi'; -import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { UserRole } from '../types/items'; import { hasPermissions, isRoleAdmin } from '../utils/hasPermissions'; import translate from '../utils/translate'; import LanguageSelectorComponent from './LanguageSelectorComponent'; import TooltipMarkerComponent from './TooltipMarkerComponent'; -import { selectCurrentUser } from '../reducers/currentUser'; -import { selectBaseHelpUrl } from '../reducers/admin'; +import { selectCurrentUser } from '../redux/slices/currentUserSlice'; +import { selectBaseHelpUrl } from '../redux/slices/adminSlice'; /** * React Component that defines the header buttons at the top of a page @@ -79,7 +78,9 @@ export default function HeaderButtonsComponent() { // Information on the current user. const { profile: currentUser } = useAppSelector(selectCurrentUser); // Tracks unsaved changes. - const unsavedChangesState = useAppSelector(state => state.unsavedWarning.hasUnsavedChanges); + // TODO Re-implement AFTER RTK Migration + // const unsavedChangesState = useAppSelector(state => state.unsavedWarning.hasUnsavedChanges); + const unsavedChangesState = false // whether to collapse options when on graphs page const optionsVisibility = useAppSelector(selectOptionsVisibility); @@ -169,7 +170,8 @@ export default function HeaderButtonsComponent() { const handleLogOut = () => { if (unsavedChangesState) { // Unsaved changes so deal with them and then it takes care of logout. - dispatch(unsavedWarningSlice.actions.flipLogOutState()); + // TODO Re-implement AFTER RTK Migration + // dispatch(unsavedWarningSlice.actions.flipLogOutState()); } else { logout() } diff --git a/src/client/app/components/HeaderComponent.tsx b/src/client/app/components/HeaderComponent.tsx index 8528a26cb..b09cb3ec7 100644 --- a/src/client/app/components/HeaderComponent.tsx +++ b/src/client/app/components/HeaderComponent.tsx @@ -4,8 +4,8 @@ import * as React from 'react'; import { Link, useLocation } from 'react-router-dom'; -import { selectOptionsVisibility } from '../reducers/appStateSlice'; -import { useAppSelector } from '../redux/hooks'; +import { selectOptionsVisibility } from '../redux/slices/appStateSlice'; +import { useAppSelector } from '../redux/reduxHooks'; import HeaderButtonsComponent from './HeaderButtonsComponent'; import LogoComponent from './LogoComponent'; import MenuModalComponent from './MenuModalComponent'; diff --git a/src/client/app/components/HistoryComponent.tsx b/src/client/app/components/HistoryComponent.tsx index 5cb2d3d09..16d868ecd 100644 --- a/src/client/app/components/HistoryComponent.tsx +++ b/src/client/app/components/HistoryComponent.tsx @@ -1,9 +1,9 @@ import * as React from 'react'; -import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { selectForwardHistory, selectPrevHistory, historyStepBack, historyStepForward -} from '../reducers/graph'; +} from '../redux/slices/graphSlice'; /** * @returns Renders a history component with previous and next buttons. */ diff --git a/src/client/app/components/LanguageSelectorComponent.tsx b/src/client/app/components/LanguageSelectorComponent.tsx index 44edbb0f7..5a5c3f035 100644 --- a/src/client/app/components/LanguageSelectorComponent.tsx +++ b/src/client/app/components/LanguageSelectorComponent.tsx @@ -5,12 +5,11 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { DropdownItem, DropdownMenu, DropdownToggle, UncontrolledDropdown } from 'reactstrap'; -import { updateSelectedLanguage } from '../actions/options'; -import { selectSelectedLanguage } from '../reducers/options'; +import { selectSelectedLanguage, updateSelectedLanguage } from '../redux/slices/appStateSlice'; import { selectOEDVersion } from '../redux/api/versionApi'; -import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { LanguageTypes } from '../types/redux/i18n'; -import { selectBaseHelpUrl } from '../reducers/admin'; +import { selectBaseHelpUrl } from '../redux/slices/adminSlice'; /** * A component that allows users to select which language the page should be displayed in. diff --git a/src/client/app/components/LineChartComponent.tsx b/src/client/app/components/LineChartComponent.tsx index bc3a5bf99..313a1979f 100644 --- a/src/client/app/components/LineChartComponent.tsx +++ b/src/client/app/components/LineChartComponent.tsx @@ -11,12 +11,12 @@ import { TimeInterval } from '../../../common/TimeInterval'; import { graphSlice, selectAreaUnit, selectGraphAreaNormalization, selectLineGraphRate, selectSelectedGroups, selectSelectedMeters, selectSelectedUnit -} from '../reducers/graph'; +} from '../redux/slices/graphSlice'; import { selectGroupDataById } from '../redux/api/groupsApi'; import { selectMeterDataById } from '../redux/api/metersApi'; import { readingsApi } from '../redux/api/readingsApi'; import { selectUnitDataById } from '../redux/api/unitsApi'; -import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { selectLineChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; import { DataType } from '../types/Datasources'; import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; diff --git a/src/client/app/components/MapChartComponent.tsx b/src/client/app/components/MapChartComponent.tsx index d0656a8f6..161a31f9f 100644 --- a/src/client/app/components/MapChartComponent.tsx +++ b/src/client/app/components/MapChartComponent.tsx @@ -11,12 +11,12 @@ import { selectAreaUnit, selectBarWidthDays, selectGraphAreaNormalization, selectSelectedGroups, selectSelectedMeters, selectSelectedUnit -} from '../reducers/graph'; +} from '../redux/slices/graphSlice'; import { selectGroupDataById } from '../redux/api/groupsApi'; import { selectMeterDataById } from '../redux/api/metersApi'; import { readingsApi } from '../redux/api/readingsApi'; import { selectUnitDataById } from '../redux/api/unitsApi'; -import { useAppSelector } from '../redux/hooks'; +import { useAppSelector } from '../redux/reduxHooks'; import { selectMapChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; import { DataType } from '../types/Datasources'; import { State } from '../types/redux/state'; diff --git a/src/client/app/components/MapControlsComponent.tsx b/src/client/app/components/MapControlsComponent.tsx index 3acfd8be0..43f101a05 100644 --- a/src/client/app/components/MapControlsComponent.tsx +++ b/src/client/app/components/MapControlsComponent.tsx @@ -3,8 +3,8 @@ import translate from '../utils/translate'; import { Button, ButtonGroup } from 'reactstrap'; import TooltipMarkerComponent from './TooltipMarkerComponent'; import MapChartSelectComponent from './MapChartSelectComponent'; -import { useAppDispatch, useAppSelector } from '../redux/hooks'; -import { graphSlice, selectBarWidthDays } from '../reducers/graph'; +import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; +import { graphSlice, selectBarWidthDays } from '../redux/slices/graphSlice'; import * as moment from 'moment'; /** * @returns Map page controls diff --git a/src/client/app/components/MeterAndGroupSelectComponent.tsx b/src/client/app/components/MeterAndGroupSelectComponent.tsx index 7d7e5d745..2db79a037 100644 --- a/src/client/app/components/MeterAndGroupSelectComponent.tsx +++ b/src/client/app/components/MeterAndGroupSelectComponent.tsx @@ -11,8 +11,8 @@ import Select, { import makeAnimated from 'react-select/animated'; import ReactTooltip from 'react-tooltip'; import { Badge } from 'reactstrap'; -import { graphSlice } from '../reducers/graph'; -import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { graphSlice } from '../redux/slices/graphSlice'; +import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { selectAnythingLoading, selectMeterGroupSelectData } from '../redux/selectors/uiSelectors'; import { GroupedOption, SelectOption } from '../types/items'; import { MeterOrGroup } from '../types/redux/graph'; diff --git a/src/client/app/components/MeterDropDownComponent.tsx b/src/client/app/components/MeterDropDownComponent.tsx index 0b692d0af..2e2987a13 100644 --- a/src/client/app/components/MeterDropDownComponent.tsx +++ b/src/client/app/components/MeterDropDownComponent.tsx @@ -4,7 +4,7 @@ import * as React from 'react'; import { NamedIDItem } from '../types/items'; -import { adminSlice } from '../reducers/admin'; +import { adminSlice } from '../redux/slices/adminSlice'; export interface MeterDropDownProps { meters: NamedIDItem[]; diff --git a/src/client/app/components/MultiCompareChartComponent.tsx b/src/client/app/components/MultiCompareChartComponent.tsx index a2397ebf1..196811db9 100644 --- a/src/client/app/components/MultiCompareChartComponent.tsx +++ b/src/client/app/components/MultiCompareChartComponent.tsx @@ -6,11 +6,11 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { UncontrolledAlert } from 'reactstrap'; import CompareChartContainer, { CompareEntity } from '../containers/CompareChartContainer'; -import { selectGraphAreaNormalization, selectSelectedGroups, selectSelectedMeters, selectSortingOrder } from '../reducers/graph'; +import { selectGraphAreaNormalization, selectSelectedGroups, selectSelectedMeters, selectSortingOrder } from '../redux/slices/graphSlice'; import { selectGroupDataById } from '../redux/api/groupsApi'; import { selectMeterDataById } from '../redux/api/metersApi'; import { readingsApi } from '../redux/api/readingsApi'; -import { useAppSelector } from '../redux/hooks'; +import { useAppSelector } from '../redux/reduxHooks'; import { selectCompareChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; import { SortingOrder } from '../utils/calculateCompare'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; diff --git a/src/client/app/components/RadarChartComponent.tsx b/src/client/app/components/RadarChartComponent.tsx index 3f93cfb9d..c9d6b6adb 100644 --- a/src/client/app/components/RadarChartComponent.tsx +++ b/src/client/app/components/RadarChartComponent.tsx @@ -13,11 +13,11 @@ import Locales from '../types/locales'; import { DataType } from '../types/Datasources'; import { lineUnitLabel } from '../utils/graphics'; import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; -import { useAppSelector } from '../redux/hooks'; +import { useAppSelector } from '../redux/reduxHooks'; import { selectAreaUnit, selectGraphAreaNormalization, selectLineGraphRate, selectSelectedGroups, selectSelectedMeters, selectSelectedUnit -} from '../reducers/graph'; +} from '../redux/slices/graphSlice'; import { selectUnitDataById } from '../redux/api/unitsApi'; import { selectMeterDataById } from '../redux/api/metersApi'; import { selectRadarChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; diff --git a/src/client/app/components/ReadingsPerDaySelectComponent.tsx b/src/client/app/components/ReadingsPerDaySelectComponent.tsx index e783ad6b9..9b59e625e 100644 --- a/src/client/app/components/ReadingsPerDaySelectComponent.tsx +++ b/src/client/app/components/ReadingsPerDaySelectComponent.tsx @@ -5,9 +5,9 @@ import * as moment from 'moment'; import * as React from 'react'; import Select from 'react-select'; -import { selectGraphState, selectThreeDReadingInterval, updateThreeDReadingInterval } from '../reducers/graph'; +import { selectGraphState, selectThreeDReadingInterval, updateThreeDReadingInterval } from '../redux/slices/graphSlice'; import { readingsApi } from '../redux/api/readingsApi'; -import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { selectThreeDQueryArgs } from '../redux/selectors/chartQuerySelectors'; import { ChartTypes, ReadingInterval } from '../types/redux/graph'; import translate from '../utils/translate'; diff --git a/src/client/app/components/RouteComponent.tsx b/src/client/app/components/RouteComponent.tsx index 10bc42bc1..584c6f6ee 100644 --- a/src/client/app/components/RouteComponent.tsx +++ b/src/client/app/components/RouteComponent.tsx @@ -4,33 +4,34 @@ import * as React from 'react'; import { IntlProvider } from 'react-intl'; import { RouterProvider, createBrowserRouter } from 'react-router-dom'; -import CreateUserContainer from '../containers/admin/CreateUserContainer'; import UploadCSVContainer from '../containers/csv/UploadCSVContainer'; import MapCalibrationContainer from '../containers/maps/MapCalibrationContainer'; import MapsDetailContainer from '../containers/maps/MapsDetailContainer'; -import { useAppSelector } from '../redux/hooks'; +import { useAppSelector } from '../redux/reduxHooks'; import LocaleTranslationData from '../translations/data'; import { UserRole } from '../types/items'; import AppLayout from './AppLayout'; import HomeComponent from './HomeComponent'; import LoginComponent from './LoginComponent'; import AdminComponent from './admin/AdminComponent'; -import UsersDetailComponentWIP from './admin/UsersDetailComponentWIP'; -import ConversionsDetailComponentWIP from './conversion/ConversionsDetailComponent'; +import UsersDetailComponent from './admin/UsersDetailComponent'; +import ConversionsDetailComponent from './conversion/ConversionsDetailComponent'; import GroupsDetailComponent from './groups/GroupsDetailComponent'; -import MetersDetailComponentWIP from './meters/MetersDetailComponent'; +import MetersDetailComponent from './meters/MetersDetailComponent'; import AdminOutlet from './router/AdminOutlet'; 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'; +import CreateUserComponent from './admin/CreateUserComponent'; /** * @returns the router component Responsible for client side routing. */ export default function RouteComponent() { - const lang = useAppSelector(state => state.options.selectedLanguage) + const lang = useAppSelector(selectSelectedLanguage) const messages = (LocaleTranslationData)[lang]; return ( @@ -48,7 +49,7 @@ const router = createBrowserRouter([ { index: true, element: }, { path: 'login', element: }, { path: 'groups', element: }, - { path: 'meters', element: }, + { path: 'meters', element: }, { path: 'graph', element: }, { element: , @@ -56,10 +57,10 @@ const router = createBrowserRouter([ { path: 'admin', element: }, { path: 'calibration', element: }, { path: 'maps', element: }, - { path: 'users/new', element: }, + { path: 'users/new', element: }, { path: 'units', element: }, - { path: 'conversions', element: }, - { path: 'users', element: } + { path: 'conversions', element: }, + { path: 'users', element: } ] }, { diff --git a/src/client/app/components/ThreeDComponent.tsx b/src/client/app/components/ThreeDComponent.tsx index cfda07c62..6bd88cd09 100644 --- a/src/client/app/components/ThreeDComponent.tsx +++ b/src/client/app/components/ThreeDComponent.tsx @@ -5,12 +5,12 @@ import * as moment from 'moment'; import * as React from 'react'; import Plot from 'react-plotly.js'; -import { selectGraphState } from '../reducers/graph'; +import { selectGraphState } from '../redux/slices/graphSlice'; import { selectGroupDataById } from '../redux/api/groupsApi'; import { selectMeterDataById } from '../redux/api/metersApi'; import { readingsApi } from '../redux/api/readingsApi'; import { selectUnitDataById } from '../redux/api/unitsApi'; -import { useAppSelector } from '../redux/hooks'; +import { useAppSelector } from '../redux/reduxHooks'; import { selectThreeDQueryArgs } from '../redux/selectors/chartQuerySelectors'; import { selectThreeDComponentInfo } from '../redux/selectors/threeDSelectors'; import { ThreeDReading } from '../types/readings'; diff --git a/src/client/app/components/ThreeDPillComponent.tsx b/src/client/app/components/ThreeDPillComponent.tsx index 264fa3ad3..c889f8dc3 100644 --- a/src/client/app/components/ThreeDPillComponent.tsx +++ b/src/client/app/components/ThreeDPillComponent.tsx @@ -4,9 +4,9 @@ import * as React from 'react'; import { Badge } from 'reactstrap'; -import { selectGraphState, selectThreeDState, updateThreeDMeterOrGroupInfo } from '../reducers/graph'; +import { selectGraphState, selectThreeDState, updateThreeDMeterOrGroupInfo } from '../redux/slices/graphSlice'; import { selectGroupDataById } from '../redux/api/groupsApi'; -import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { MeterOrGroup, MeterOrGroupPill } from '../types/redux/graph'; import { AreaUnitType } from '../utils/getAreaUnitConversion'; import { selectMeterDataById } from '../redux/api/metersApi'; diff --git a/src/client/app/components/TooltipHelpComponent.tsx b/src/client/app/components/TooltipHelpComponent.tsx index 88e8247e2..7eadc139a 100644 --- a/src/client/app/components/TooltipHelpComponent.tsx +++ b/src/client/app/components/TooltipHelpComponent.tsx @@ -7,9 +7,9 @@ import { FormattedMessage } from 'react-intl'; import ReactTooltip from 'react-tooltip'; import '../styles/tooltip.css'; import translate from '../utils/translate'; -import { useAppSelector } from '../redux/hooks'; +import { useAppSelector } from '../redux/reduxHooks'; import { selectOEDVersion } from '../redux/api/versionApi'; -import { selectBaseHelpUrl } from '../reducers/admin'; +import { selectBaseHelpUrl } from '../redux/slices/adminSlice'; interface TooltipHelpProps { page: string; // Specifies which page the tip is in. diff --git a/src/client/app/components/UIOptionsComponent.tsx b/src/client/app/components/UIOptionsComponent.tsx index 2c6609b5e..197802bdb 100644 --- a/src/client/app/components/UIOptionsComponent.tsx +++ b/src/client/app/components/UIOptionsComponent.tsx @@ -6,8 +6,8 @@ import * as React from 'react'; import ReactTooltip from 'react-tooltip'; import ExportComponent from '../components/ExportComponent'; import ChartLinkContainer from '../containers/ChartLinkContainer'; -import { selectChartToRender } from '../reducers/graph'; -import { useAppSelector } from '../redux/hooks'; +import { selectChartToRender } from '../redux/slices/graphSlice'; +import { useAppSelector } from '../redux/reduxHooks'; import { ChartTypes } from '../types/redux/graph'; import AreaUnitSelectComponent from './AreaUnitSelectComponent'; import BarControlsComponent from './BarControlsComponent'; diff --git a/src/client/app/components/UnitSelectComponent.tsx b/src/client/app/components/UnitSelectComponent.tsx index 47846c1a9..2b6247ce0 100644 --- a/src/client/app/components/UnitSelectComponent.tsx +++ b/src/client/app/components/UnitSelectComponent.tsx @@ -4,13 +4,13 @@ import * as React from 'react'; import Select from 'react-select'; -import { useAppDispatch, useAppSelector } from '../redux/hooks'; +import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { selectUnitSelectData } from '../redux/selectors/uiSelectors'; import { GroupedOption, SelectOption } from '../types/items'; // import TooltipMarkerComponent from './TooltipMarkerComponent'; // import { FormattedMessage } from 'react-intl'; import { Badge } from 'reactstrap'; -import { graphSlice, selectSelectedUnit } from '../reducers/graph'; +import { graphSlice, selectSelectedUnit } from '../redux/slices/graphSlice'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; import { selectUnitDataById, unitsApi } from '../redux/api/unitsApi'; diff --git a/src/client/app/components/UnsavedWarningComponent.tsx b/src/client/app/components/UnsavedWarningComponent.tsx index 4ce2ba57f..be5281f67 100644 --- a/src/client/app/components/UnsavedWarningComponent.tsx +++ b/src/client/app/components/UnsavedWarningComponent.tsx @@ -1,174 +1,76 @@ -/* 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 */ /* 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 { MutationTrigger } from '@reduxjs/toolkit/dist/query/react/buildHooks'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; -// TODO migrate ReactRouterV6 & hooks -import { Prompt, withRouter, RouteComponentProps } from 'react-router-dom'; -import { deleteToken } from '../utils/token'; -import {store} from '../store'; -import { Modal, ModalBody, ModalFooter, Button } from 'reactstrap'; -import { currentUserSlice } from '../reducers/currentUser'; -import { unsavedWarningSlice } from '../reducers/unsavedWarning'; +import { useBlocker } from 'react-router-dom'; +// TODO migrate ReactRouter v6 & hooks +import { Button, Modal, ModalBody, ModalFooter } from 'reactstrap'; +import { LocaleDataKey } from '../translations/data'; +import { showErrorNotification, showSuccessNotification } from '../utils/notifications'; +import translate from '../utils/translate'; -interface UnsavedWarningProps extends RouteComponentProps { +export interface UnsavedWarningProps { + changes: any; hasUnsavedChanges: boolean; - isLogOutClicked: boolean; - removeFunction: (callback: () => void) => any; - submitFunction: (successCallback: () => void, failureCallback: () => void) => any; - removeUnsavedChanges(): ReturnType; - flipLogOutState(): ReturnType; + successMessage: LocaleDataKey; + failureMessage: LocaleDataKey; + submitChanges: MutationTrigger; } -class UnsavedWarningComponent extends React.Component { - state = { - warningVisible: false, - confirmedToLeave: false, - nextLocation: '' - } - - constructor(props: UnsavedWarningProps) { - super(props); - this.closeWarning = this.closeWarning.bind(this); - this.handleSubmitClicked = this.handleSubmitClicked.bind(this); - this.handleLeaveClicked = this.handleLeaveClicked.bind(this); - } - - componentDidUpdate() { - const { hasUnsavedChanges } = this.props; - if (hasUnsavedChanges) { - // Block reloading page or closing OED tab - window.onbeforeunload = () => true; - } else { - window.onbeforeunload = () => undefined; - } - } - - render() { - return ( - <> - { - const { confirmedToLeave } = this.state; - const { hasUnsavedChanges } = this.props; - if (!confirmedToLeave && hasUnsavedChanges) { - this.setState({ - warningVisible: true, - nextLocation: nextLocation.pathname - }); - - const currentLocation = this.props.history.location.pathname; - if ((currentLocation === '/maps' && nextLocation.pathname === '/calibration') || - (currentLocation === '/calibration' && nextLocation.pathname === '/maps')) { - // Don't warn users if they go between /maps and /calibration - return true; - } - return false; - } - return true; - }} - /> - - - - - - - - - - - ) - } - - /** - * Call when the user clicks the cancel button - */ - private closeWarning() { - this.setState({ - warningVisible: false - }); +/** + * @param props unsavedChanges props + * @returns Component that prompts before navigating away from current page + */ +export function UnsavedWarningComponent(props: UnsavedWarningProps) { + const { hasUnsavedChanges, submitChanges, changes } = props + const blocker = useBlocker(hasUnsavedChanges) + const handleSubmit = async () => { + submitChanges(changes) + .unwrap() + .then(() => { + //TODO translate me + showSuccessNotification(translate('success, TODO translate me')) + if (blocker.state === 'blocked') { + blocker.proceed() + } + }) + .catch(() => { + //TODO translate me + showErrorNotification(translate('Failure, TODO translate me')) + if (blocker.state === 'blocked') { + blocker.proceed() + } + }) } - - /** - * Called when the user clicks the leave button or when the submit function throws an error - * Replace local changes with the original data - */ - private handleLeaveClicked() { - const { nextLocation } = this.state; - if (nextLocation) { - this.setState({ - confirmedToLeave: true, - warningVisible: false - }, () => { - this.props.removeFunction(() => { - if (this.props.isLogOutClicked) { - // Set the logout state to false - this.props.flipLogOutState(); - // Delete token when users click log out - this.handleLogOut(); - } - this.props.removeUnsavedChanges(); - // Unblock reloading page and closing tab - window.onbeforeunload = () => undefined; - // Navigate to the path that the user wants - this.props.history.push(this.state.nextLocation); - }); - }); + React.useEffect(() => { + const handleBeforeUnload = (e: BeforeUnloadEvent) => { + if (blocker.state === 'blocked') { + e.preventDefault(); + } } - } - /** - * Called when successfully submitting the unsaved changes - * Redirect to the desire path and turn off the unsaved warning - */ - private handleSubmitLeave() { - const { nextLocation } = this.state; - if (nextLocation) { - this.setState({ - confirmedToLeave: true, - warningVisible: false - }, () => { - if (this.props.isLogOutClicked) { - // Set the logout state to false - this.props.flipLogOutState(); - // Delete token when users click log out - this.handleLogOut(); - } - this.props.removeUnsavedChanges(); - // Unblock reloading page and closing tab - window.onbeforeunload = () => undefined; - // Navigate to the path that the user wants - this.props.history.push(this.state.nextLocation); - }); - } - } + window.addEventListener('beforeunload', handleBeforeUnload) + return () => window.removeEventListener('beforeunload', handleBeforeUnload) + }, [hasUnsavedChanges, blocker]) - private handleSubmitClicked() { - this.props.submitFunction(() => { - this.handleSubmitLeave(); - }, () => { - this.handleLeaveClicked(); - }); - } + return ( - private handleLogOut() { - deleteToken(); - store.dispatch(currentUserSlice.actions.clearCurrentUser()); - } + + + + + + + + + ) } - -export default withRouter(UnsavedWarningComponent); diff --git a/src/client/app/components/UnsavedWarningComponentWIP.tsx b/src/client/app/components/UnsavedWarningComponentWIP.tsx deleted file mode 100644 index b3004d6b1..000000000 --- a/src/client/app/components/UnsavedWarningComponentWIP.tsx +++ /dev/null @@ -1,76 +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 { MutationTrigger } from '@reduxjs/toolkit/dist/query/react/buildHooks'; -import * as React from 'react'; -import { FormattedMessage } from 'react-intl'; -import { useBlocker } from 'react-router-dom'; -// TODO migrate ReactRouter v6 & hooks -import { Button, Modal, ModalBody, ModalFooter } from 'reactstrap'; -import { LocaleDataKey } from '../translations/data'; -import { showErrorNotification, showSuccessNotification } from '../utils/notifications'; -import translate from '../utils/translate'; - -export interface UnsavedWarningProps { - changes: any; - hasUnsavedChanges: boolean; - successMessage: LocaleDataKey; - failureMessage: LocaleDataKey; - submitChanges: MutationTrigger; -} - -/** - * @param props unsavedChanges Boolean - * @returns Component that prompts before navigating away from current page - */ -export function UnsavedWarningComponentWIP(props: UnsavedWarningProps) { - const { hasUnsavedChanges, submitChanges, changes } = props - const blocker = useBlocker(hasUnsavedChanges) - const handleSubmit = async () => { - submitChanges(changes) - .unwrap() - .then(() => { - showSuccessNotification(translate('updated.preferences')) - if (blocker.state === 'blocked') { - blocker.proceed() - } - }) - .catch(() => { - showErrorNotification(translate('failed.to.submit.changes')) - if (blocker.state === 'blocked') { - blocker.proceed() - } - }) - } - React.useEffect(() => { - const handleBeforeUnload = (e: BeforeUnloadEvent) => { - if (blocker.state === 'blocked') { - e.preventDefault(); - } - } - - window.addEventListener('beforeunload', handleBeforeUnload) - return () => window.removeEventListener('beforeunload', handleBeforeUnload) - }, [hasUnsavedChanges, blocker]) - - // console.log(props) - - return ( - - - - - - - - - - ) -} diff --git a/src/client/app/components/admin/AdminComponent.tsx b/src/client/app/components/admin/AdminComponent.tsx index 0fbcc854c..f35ada6e4 100644 --- a/src/client/app/components/admin/AdminComponent.tsx +++ b/src/client/app/components/admin/AdminComponent.tsx @@ -7,7 +7,7 @@ import { FormattedMessage } from 'react-intl'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; // import PreferencesContainer from '../../containers/admin/PreferencesContainer'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import PreferencesComponentWIP from './PreferencesComponentWIP'; +import PreferencesComponent from './PreferencesComponent'; import ManageUsersLinkButtonComponent from './users/ManageUsersLinkButtonComponent'; /** @@ -50,8 +50,7 @@ export default function AdminComponent() {
- {/* */} - +
diff --git a/src/client/app/components/admin/CreateUserComponent.tsx b/src/client/app/components/admin/CreateUserComponent.tsx index 5ad662f78..6ccbbc313 100644 --- a/src/client/app/components/admin/CreateUserComponent.tsx +++ b/src/client/app/components/admin/CreateUserComponent.tsx @@ -3,77 +3,87 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { UserRole } from '../../types/items'; -import { Alert, Button, Input } from 'reactstrap'; import { FormattedMessage } from 'react-intl'; +import { Button, Input } from 'reactstrap'; +import { userApi } from '../../redux/api/userApi'; +import { NewUser, UserRole } from '../../types/items'; +import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; +import translate from '../../utils/translate'; +import { useNavigate } from 'react-router-dom'; -interface CreateUserFormProps { - email: string; - password: string; - confirmPassword: string; - doPasswordsMatch: boolean; - role: UserRole; - submittedOnce: boolean; - handleEmailChange: (val: string) => void; - handlePasswordChange: (val: string) => void; - handleConfirmPasswordChange: (val: string) => void; - handleRoleChange: (val: UserRole) => void; - submitNewUser: () => void; -} /** * Component that defines the form to create a new user - * @param props defined above - * @returns Create User element + * @returns Create User Page */ -export default function CreateUserFormComponent(props: CreateUserFormProps) { +export default function CreateUserComponent() { + const [email, setEmail] = React.useState(''); + const [password, setPassword] = React.useState(''); + const [confirmPassword, setConfirmPassword] = React.useState(''); + const [role, setRole] = React.useState(UserRole.ADMIN); + const [createUser] = userApi.useCreateUserMutation(); + const nav = useNavigate() - const formInputStyle: React.CSSProperties = { - paddingBottom: '5px' - } - const titleStyle: React.CSSProperties = { - textAlign: 'center' - }; + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const newUser: NewUser = { email, role, password } + createUser(newUser) + .unwrap() + .then(() => { + showSuccessNotification(translate('users.successfully.create.user')) + nav('/users') - const tableStyle: React.CSSProperties = { - marginLeft: '25%', - marginRight: '25%', - width: '50%' - }; + }) + .catch(() => { + showErrorNotification(translate('users.failed.to.create.user')); + }) + } return ( -
-

-
-
{ e.preventDefault(); props.submitNewUser(); }}> -
-
- props.handleEmailChange(target.value)} required value={props.email} /> -
- {props.submittedOnce && !props.doPasswordsMatch && - Error: Passwords Do Not Match - } -
-
- props.handlePasswordChange(target.value)} required value={props.password} /> -
-
-
- props.handleConfirmPasswordChange(target.value)} required value={props.confirmPassword} /> -
-
-
- props.handleRoleChange(target.value as UserRole)} value={props.role}> - {Object.entries(UserRole).map(([role, val]) => ( - - ))} - -
-
- -
-
+
+
+

+
+
+
+
+ setEmail(target.value)} required value={email} /> +
+
+
+ setPassword(target.value)} required value={password} /> +
+
+
+ setConfirmPassword(target.value)} required value={confirmPassword} /> +
+
+
+ setRole(target.value as UserRole)} value={role}> + {Object.entries(UserRole).map(([role, val]) => ( + + ))} + +
+
+ +
+
+
+ ) -} \ No newline at end of file +} +const formInputStyle: React.CSSProperties = { + paddingBottom: '5px' +}; +const titleStyle: React.CSSProperties = { + textAlign: 'center' +}; + +const tableStyle: React.CSSProperties = { + marginLeft: '25%', + marginRight: '25%', + width: '50%' +}; \ No newline at end of file diff --git a/src/client/app/components/admin/CreateUserComponentWIP.tsx b/src/client/app/components/admin/CreateUserComponentWIP.tsx deleted file mode 100644 index 9b3bbb78a..000000000 --- a/src/client/app/components/admin/CreateUserComponentWIP.tsx +++ /dev/null @@ -1,86 +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 * as React from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Button, Input } from 'reactstrap'; -import { userApi } from '../../redux/api/userApi'; -import { NewUser, UserRole } from '../../types/items'; -import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; -import translate from '../../utils/translate'; - - -/** - * Component that defines the form to create a new user - * @returns Create User Page - */ -export default function CreateUserComponentWIP() { - const [email, setEmail] = React.useState(''); - const [password, setPassword] = React.useState(''); - const [confirmPassword, setConfirmPassword] = React.useState(''); - const [role, setRole] = React.useState(UserRole.ADMIN); - const [createUser] = userApi.useCreateUserMutation(); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - const newUser: NewUser = { email, role, password } - createUser(newUser) - .unwrap() - .then(() => { - showSuccessNotification(translate('users.successfully.create.user')) - - }) - .catch(() => { - showErrorNotification(translate('users.failed.to.create.user')); - }) - - } - return ( -
-
-

-
-
-
-
- setEmail(target.value)} required value={email} /> -
-
-
- setPassword(target.value)} required value={password} /> -
-
-
- setConfirmPassword(target.value)} required value={confirmPassword} /> -
-
-
- setRole(target.value as UserRole)} value={role}> - {Object.entries(UserRole).map(([role, val]) => ( - - ))} - -
-
- -
-
-
-
-
- - ) -} -const formInputStyle: React.CSSProperties = { - paddingBottom: '5px' -}; -const titleStyle: React.CSSProperties = { - textAlign: 'center' -}; - -const tableStyle: React.CSSProperties = { - marginLeft: '25%', - marginRight: '25%', - width: '50%' -}; \ No newline at end of file diff --git a/src/client/app/components/admin/PreferencesComponent.tsx b/src/client/app/components/admin/PreferencesComponent.tsx index f333c735b..f33c6b273 100644 --- a/src/client/app/components/admin/PreferencesComponent.tsx +++ b/src/client/app/components/admin/PreferencesComponent.tsx @@ -2,545 +2,352 @@ * 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 _ from 'lodash'; import * as React from 'react'; -import { Input, Button } from 'reactstrap'; +import { FormattedMessage } from 'react-intl'; +import { Button, Input } from 'reactstrap'; +import { UnsavedWarningComponent } from '../UnsavedWarningComponent'; +import { preferencesApi } from '../../redux/api/preferencesApi'; +import { PreferenceRequestItem, TrueFalseType } from '../../types/items'; import { ChartTypes } from '../../types/redux/graph'; -import { defineMessages, FormattedMessage, injectIntl, WrappedComponentProps } from 'react-intl'; import { LanguageTypes } from '../../types/redux/i18n'; -import TimeZoneSelect from '../TimeZoneSelect'; -import { store } from '../../store'; -import { fetchPreferencesIfNeeded, submitPreferences } from '../../actions/admin'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; +import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; import translate from '../../utils/translate'; -import { TrueFalseType } from '../../types/items'; -import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; -import { adminSlice } from '../../reducers/admin'; - -interface PreferencesProps { - displayTitle: string; - defaultChartToRender: ChartTypes; - defaultBarStacking: boolean; - defaultAreaNormalization: boolean; - defaultTimeZone: string; - defaultLanguage: LanguageTypes; - disableSubmitPreferences: boolean; - defaultWarningFileSize: number; - defaultFileSizeLimit: number; - defaultAreaUnit: AreaUnitType; - defaultMeterReadingFrequency: string; - defaultMeterMinimumValue: number; - defaultMeterMaximumValue: number; - defaultMeterMinimumDate: string; - defaultMeterMaximumDate: string; - defaultMeterReadingGap: number; - defaultMeterMaximumErrors: number; - defaultMeterDisableChecks: boolean; - defaultHelpUrl: string; - updateDisplayTitle(title: string): ReturnType; - updateDefaultChartType(defaultChartToRender: ChartTypes): ReturnType; - toggleDefaultBarStacking(): ReturnType; - toggleDefaultAreaNormalization(): ReturnType; - updateDefaultLanguage(defaultLanguage: LanguageTypes): ReturnType; - submitPreferences(): Promise; - updateDefaultTimeZone(timeZone: string): ReturnType; - updateDefaultWarningFileSize(defaultWarningFileSize: number): ReturnType; - updateDefaultFileSizeLimit(defaultFileSizeLimit: number): ReturnType; - updateDefaultAreaUnit(defaultAreaUnit: AreaUnitType): ReturnType; - updateDefaultMeterReadingFrequency(defaultMeterReadingFrequency: string): ReturnType; - updateDefaultMeterMinimumValue(defaultMeterMinimumValue: number): ReturnType; - updateDefaultMeterMaximumValue(defaultMeterMaximumValue: number): ReturnType; - updateDefaultMeterMinimumDate(defaultMeterMinimumDate: string): ReturnType; - updateDefaultMeterMaximumDate(defaultMeterMaximumDate: string): ReturnType; - updateDefaultMeterReadingGap(defaultMeterReadingGap: number): ReturnType; - updateDefaultMeterMaximumErrors(defaultMeterMaximumErrors: number): ReturnType; - updateDefaultMeterDisableChecks(defaultMeterDisableChecks: boolean): ReturnType; - updateDefaultHelpUrl(defaultHelpUrl: string): ReturnType; -} +import TimeZoneSelect from '../TimeZoneSelect'; +import { defaultAdminState } from '../../redux/slices/adminSlice'; -type PreferencesPropsWithIntl = PreferencesProps & WrappedComponentProps; // TODO: Add warning for invalid data -class PreferencesComponent extends React.Component { - constructor(props: PreferencesPropsWithIntl) { - super(props); - this.handleDisplayTitleChange = this.handleDisplayTitleChange.bind(this); - this.handleDefaultChartToRenderChange = this.handleDefaultChartToRenderChange.bind(this); - this.handleDefaultBarStackingChange = this.handleDefaultBarStackingChange.bind(this); - this.handleDefaultTimeZoneChange = this.handleDefaultTimeZoneChange.bind(this); - this.handleDefaultLanguageChange = this.handleDefaultLanguageChange.bind(this); - this.handleSubmitPreferences = this.handleSubmitPreferences.bind(this); - this.handleDefaultWarningFileSizeChange = this.handleDefaultWarningFileSizeChange.bind(this); - this.handleDefaultFileSizeLimitChange = this.handleDefaultFileSizeLimitChange.bind(this); - this.handleDefaultAreaNormalizationChange = this.handleDefaultAreaNormalizationChange.bind(this); - this.handleDefaultAreaUnitChange = this.handleDefaultAreaUnitChange.bind(this); - this.handleDefaultMeterReadingFrequencyChange = this.handleDefaultMeterReadingFrequencyChange.bind(this); - this.handleDefaultMeterMinimumValueChange = this.handleDefaultMeterMinimumValueChange.bind(this); - this.handleDefaultMeterMaximumValueChange = this.handleDefaultMeterMaximumValueChange.bind(this); - this.handleDefaultMeterMinimumDateChange = this.handleDefaultMeterMinimumDateChange.bind(this); - this.handleDefaultMeterMaximumDateChange = this.handleDefaultMeterMaximumDateChange.bind(this); - this.handleDefaultMeterReadingGapChange = this.handleDefaultMeterReadingGapChange.bind(this); - this.handleDefaultMeterMaximumErrorsChange = this.handleDefaultMeterMaximumErrorsChange.bind(this); - this.handleDefaultMeterDisableChecksChange = this.handleDefaultMeterDisableChecksChange.bind(this); - this.handleDefaultHelpUrlChange = this.handleDefaultHelpUrlChange.bind(this); +/** + * @returns Preferences Component for Administrative use + */ +export default function PreferencesComponent() { + const { data: adminPreferences = defaultAdminState } = preferencesApi.useGetPreferencesQuery(); + const [localAdminPref, setLocalAdminPref] = React.useState(_.cloneDeep(adminPreferences)) + const [submitPreferences] = preferencesApi.useSubmitPreferencesMutation(); + const [hasChanges, setHasChanges] = React.useState(false); + + // mutation will invalidate preferences tag and will be re-fetched. + // On query response, reset local changes to response + React.useEffect(() => { setLocalAdminPref(_.cloneDeep(adminPreferences)) }, [adminPreferences]) + // Compare the API response against the localState to determine changes + React.useEffect(() => { setHasChanges(!_.isEqual(adminPreferences, localAdminPref)) }, [localAdminPref, adminPreferences]) + + const makeLocalChanges = (key: keyof PreferenceRequestItem, value: PreferenceRequestItem[keyof PreferenceRequestItem]) => { + setLocalAdminPref({ ...localAdminPref, [key]: value }) } - public render() { - const labelStyle: React.CSSProperties = { - fontWeight: 'bold', - margin: 0 - }; - const bottomPaddingStyle: React.CSSProperties = { - paddingBottom: '15px' - }; - - const titleStyle: React.CSSProperties = { - fontWeight: 'bold', - margin: 0, - paddingBottom: '5px' - }; - const messages = defineMessages({ name: { id: 'name' } }); - - return ( + return ( +
+ +
+

+ {`${translate('default.site.title')}:`} +

+ makeLocalChanges('displayTitle', e.target.value)} + maxLength={50} + /> +
-
-

- : -

- + : +

+ { + Object.values(ChartTypes).map(chartType => ( +
+ +
+ )) + } +
+

+ : +

+
+
-
-

- : -

-
- -
-
- -
-
- -
-
- -
-
- -
-
+ {translate('default.bar.stacking')} + +
+
+ +
+

- : + {translate('default.area.unit')} +

-
+
-
+
-
-

- : -

-
- -
-
- -
-
-
-

- : -

-
- -
-
- -
-
- -
-
-
-

- : -

- -
-
-

- : -

- -
-
-

- : -

- -
- {/* Reuse same style as title. */} -
-

- : -

- -
-
-

- : -

- -
-
-

- : -

- -
-
-

- : -

- -
-
-

- : -

- -
-
-

- : -

- -
-
-

- : -

- +
+
+

+ {translate('default.language')} +

+
+
-
-

- : -

- - {Object.keys(TrueFalseType).map(key => { - return () - })} - +
+
-
-

- : -

- +
+
-
- ); - } - - private removeUnsavedChangesFunction(callback: () => void) { - // The function is called to reset all the inputs to the initial state - store.dispatch(fetchPreferencesIfNeeded()).then(callback); - } - - private submitUnsavedChangesFunction(successCallback: () => void, failureCallback: () => void) { - // The function is called to submit the unsaved changes - store.dispatch(submitPreferences()).then(successCallback, failureCallback); - } - - private updateUnsavedChanges() { - // Notify that there are unsaved changes - store.dispatch(unsavedWarningSlice.actions.updateUnsavedChanges({ - removeFunction: this.removeUnsavedChangesFunction, - submitFunction: this.submitUnsavedChangesFunction - })); - } - - private removeUnsavedChanges() { - // Notify that there are no unsaved changes - store.dispatch(unsavedWarningSlice.actions.removeUnsavedChanges()); - } - - private handleDisplayTitleChange(e: { target: HTMLInputElement; }) { - this.props.updateDisplayTitle(e.target.value); - this.updateUnsavedChanges(); - } - - private handleDefaultChartToRenderChange(e: React.FormEvent) { - this.props.updateDefaultChartType((e.target as HTMLInputElement).value as ChartTypes); - this.updateUnsavedChanges(); - } - - private handleDefaultBarStackingChange() { - this.props.toggleDefaultBarStacking(); - this.updateUnsavedChanges(); - } - - private handleDefaultAreaNormalizationChange() { - this.props.toggleDefaultAreaNormalization(); - this.updateUnsavedChanges(); - } - - private handleDefaultAreaUnitChange(e: { target: HTMLInputElement; }) { - this.props.updateDefaultAreaUnit(e.target.value as AreaUnitType); - this.updateUnsavedChanges(); - } - - private handleDefaultLanguageChange(e: React.FormEvent) { - this.props.updateDefaultLanguage((e.target as HTMLInputElement).value as LanguageTypes); - this.updateUnsavedChanges(); - } - - private handleDefaultTimeZoneChange(value: string) { - this.props.updateDefaultTimeZone(value); - this.updateUnsavedChanges(); - } - - private handleSubmitPreferences() { - this.props.submitPreferences(); - this.removeUnsavedChanges(); - } - - private handleDefaultWarningFileSizeChange(e: { target: HTMLInputElement; }) { - this.props.updateDefaultWarningFileSize(parseFloat(e.target.value)); - this.updateUnsavedChanges(); - } - - private handleDefaultFileSizeLimitChange(e: { target: HTMLInputElement; }) { - this.props.updateDefaultFileSizeLimit(parseFloat(e.target.value)); - this.updateUnsavedChanges(); - } - - private handleDefaultMeterReadingFrequencyChange(e: { target: HTMLInputElement; }) { - this.props.updateDefaultMeterReadingFrequency(e.target.value); - this.updateUnsavedChanges(); - } - - private handleDefaultMeterMinimumValueChange(e: { target: HTMLInputElement; }) { - this.props.updateDefaultMeterMinimumValue(parseFloat(e.target.value)); - this.updateUnsavedChanges(); - } - - private handleDefaultMeterMaximumValueChange(e: { target: HTMLInputElement; }) { - this.props.updateDefaultMeterMaximumValue(parseFloat(e.target.value)); - this.updateUnsavedChanges(); - } - - private handleDefaultMeterMinimumDateChange(e: { target: HTMLInputElement; }) { - this.props.updateDefaultMeterMinimumDate(e.target.value); - this.updateUnsavedChanges(); - } +
+

+ {`${translate('default.time.zone')}:`} +

+ makeLocalChanges('defaultTimezone', e)} /> +
+
+

+ {`${translate('default.warning.file.size')}:`} - private handleDefaultMeterMaximumDateChange(e: { target: HTMLInputElement; }) { - this.props.updateDefaultMeterMaximumDate(e.target.value); - this.updateUnsavedChanges(); - } +

+ makeLocalChanges('defaultWarningFileSize', e.target.value)} + maxLength={50} + /> +
+
+

+ {`${translate('default.file.size.limit')}:`} +

+ makeLocalChanges('defaultFileSizeLimit', e.target.value)} + maxLength={50} + /> +
+ {/* Reuse same style as title. */} +
+

+ {`${translate('default.meter.reading.frequency')}:`} +

+ makeLocalChanges('defaultMeterReadingFrequency', e.target.value)} + /> +
+
+

+ {`${translate('default.meter.minimum.value')}:`} +

+ makeLocalChanges('defaultMeterMinimumValue', e.target.value)} + maxLength={50} + /> +
+
+

+ {`${translate('default.meter.maximum.value')}:`} +

+ makeLocalChanges('defaultMeterMaximumValue', e.target.value)} + maxLength={50} + /> +
+
+

+ {`${translate('default.meter.minimum.date')}:`} +

+ makeLocalChanges('defaultMeterMinimumDate', e.target.value)} + placeholder='YYYY-MM-DD HH:MM:SS' + /> +
+
+

+ {`${translate('default.meter.maximum.date')}:`} +

+ makeLocalChanges('defaultMeterMaximumDate', e.target.value)} + placeholder='YYYY-MM-DD HH:MM:SS' + /> +
+
+

+ {`${translate('default.meter.reading.gap')}:`} +

+ makeLocalChanges('defaultMeterReadingGap', e.target.value)} + maxLength={50} + /> +
+
+

+ {`${translate('default.meter.maximum.errors')}:`} +

+ makeLocalChanges('defaultMeterMaximumErrors', e.target.value)} + maxLength={50} + /> +
+
+

+ {`${translate('default.meter.disable.checks')}:`} +

+ makeLocalChanges('defaultMeterDisableChecks', e.target.value)}> + {Object.keys(TrueFalseType).map(key => { + return () + })} + +
+
+

+ : +

+ makeLocalChanges('defaultHelpUrl', e.target.value)} + /> +
+ +
+ ); +} - private handleDefaultMeterReadingGapChange(e: { target: HTMLInputElement; }) { - this.props.updateDefaultMeterReadingGap(parseFloat(e.target.value)); - this.updateUnsavedChanges(); - } - private handleDefaultMeterMaximumErrorsChange(e: { target: HTMLInputElement; }) { - this.props.updateDefaultMeterMaximumErrors(parseInt(e.target.value)); - this.updateUnsavedChanges(); - } - private handleDefaultMeterDisableChecksChange(e: { target: HTMLInputElement; }) { - this.props.updateDefaultMeterDisableChecks(JSON.parse(e.target.value)) - this.updateUnsavedChanges(); - } - private handleDefaultHelpUrlChange(e: { target: HTMLInputElement; }) { - this.props.updateDefaultHelpUrl(e.target.value); - this.updateUnsavedChanges(); - } -} +const labelStyle: React.CSSProperties = { + fontWeight: 'bold', + margin: 0 +}; +const bottomPaddingStyle: React.CSSProperties = { + paddingBottom: '15px' +}; -export default injectIntl(PreferencesComponent); +const titleStyle: React.CSSProperties = { + fontWeight: 'bold', + margin: 0, + paddingBottom: '5px' +}; \ No newline at end of file diff --git a/src/client/app/components/admin/PreferencesComponentWIP.tsx b/src/client/app/components/admin/PreferencesComponentWIP.tsx deleted file mode 100644 index 4de4a21c1..000000000 --- a/src/client/app/components/admin/PreferencesComponentWIP.tsx +++ /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 * as _ from 'lodash'; -import * as React from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Button, Input } from 'reactstrap'; -import { UnsavedWarningComponentWIP } from '../../components/UnsavedWarningComponentWIP'; -import { preferencesApi } from '../../redux/api/preferencesApi'; -import { PreferenceRequestItem, TrueFalseType } from '../../types/items'; -import { ChartTypes } from '../../types/redux/graph'; -import { LanguageTypes } from '../../types/redux/i18n'; -import { AreaUnitType } from '../../utils/getAreaUnitConversion'; -import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; -import translate from '../../utils/translate'; -import TimeZoneSelect from '../TimeZoneSelect'; -import { defaultAdminState } from '../../reducers/admin'; - - -// TODO: Add warning for invalid data -/** - * @returns Preferences Component for Administrative use - */ -export default function PreferencesComponentWIP() { - const { data: adminPreferences = defaultAdminState } = preferencesApi.useGetPreferencesQuery(); - const [localAdminPref, setLocalAdminPref] = React.useState(_.cloneDeep(adminPreferences)) - const [submitPreferences] = preferencesApi.useSubmitPreferencesMutation(); - const [hasChanges, setHasChanges] = React.useState(false); - - // mutation will invalidate preferences tag and will be re-fetched. - // On query response, reset local changes to response - React.useEffect(() => { setLocalAdminPref(_.cloneDeep(adminPreferences)) }, [adminPreferences]) - // Compare the API response against the localState to determine changes - React.useEffect(() => { setHasChanges(!_.isEqual(adminPreferences, localAdminPref)) }, [localAdminPref, adminPreferences]) - - const makeLocalChanges = (key: keyof PreferenceRequestItem, value: PreferenceRequestItem[keyof PreferenceRequestItem]) => { - setLocalAdminPref({ ...localAdminPref, [key]: value }) - } - - return ( -
- -
-

- {`${translate('default.site.title')}:`} -

- makeLocalChanges('displayTitle', e.target.value)} - maxLength={50} - /> -
-
-

- : -

- { - Object.values(ChartTypes).map(chartType => ( -
- -
- )) - } -
-

- : -

-
- -
-
- -
-
-

- {translate('default.area.unit')} - -

-
- -
-
- -
-
-
-

- {translate('default.language')} -

-
- -
-
- -
-
- -
-
-
-

- {`${translate('default.time.zone')}:`} -

- makeLocalChanges('defaultTimezone', e)} /> -
-
-

- {`${translate('default.warning.file.size')}:`} - -

- makeLocalChanges('defaultWarningFileSize', e.target.value)} - maxLength={50} - /> -
-
-

- {`${translate('default.file.size.limit')}:`} -

- makeLocalChanges('defaultFileSizeLimit', e.target.value)} - maxLength={50} - /> -
- {/* Reuse same style as title. */} -
-

- {`${translate('default.meter.reading.frequency')}:`} -

- makeLocalChanges('defaultMeterReadingFrequency', e.target.value)} - /> -
-
-

- {`${translate('default.meter.minimum.value')}:`} -

- makeLocalChanges('defaultMeterMinimumValue', e.target.value)} - maxLength={50} - /> -
-
-

- {`${translate('default.meter.maximum.value')}:`} -

- makeLocalChanges('defaultMeterMaximumValue', e.target.value)} - maxLength={50} - /> -
-
-

- {`${translate('default.meter.minimum.date')}:`} -

- makeLocalChanges('defaultMeterMinimumDate', e.target.value)} - placeholder='YYYY-MM-DD HH:MM:SS' - /> -
-
-

- {`${translate('default.meter.maximum.date')}:`} -

- makeLocalChanges('defaultMeterMaximumDate', e.target.value)} - placeholder='YYYY-MM-DD HH:MM:SS' - /> -
-
-

- {`${translate('default.meter.reading.gap')}:`} -

- makeLocalChanges('defaultMeterReadingGap', e.target.value)} - maxLength={50} - /> -
-
-

- {`${translate('default.meter.maximum.errors')}:`} -

- makeLocalChanges('defaultMeterMaximumErrors', e.target.value)} - maxLength={50} - /> -
-
-

- {`${translate('default.meter.disable.checks')}:`} -

- makeLocalChanges('defaultMeterDisableChecks', e.target.value)}> - {Object.keys(TrueFalseType).map(key => { - return () - })} - -
-
-

- : -

- makeLocalChanges('defaultHelpUrl', e.target.value)} - /> -
- -
- ); -} - - - - - -const labelStyle: React.CSSProperties = { - fontWeight: 'bold', - margin: 0 -}; -const bottomPaddingStyle: React.CSSProperties = { - paddingBottom: '15px' -}; - -const titleStyle: React.CSSProperties = { - fontWeight: 'bold', - margin: 0, - paddingBottom: '5px' -}; \ No newline at end of file diff --git a/src/client/app/components/admin/UsersDetailComponent.tsx b/src/client/app/components/admin/UsersDetailComponent.tsx index 562cebc24..d29d97be2 100644 --- a/src/client/app/components/admin/UsersDetailComponent.tsx +++ b/src/client/app/components/admin/UsersDetailComponent.tsx @@ -2,77 +2,75 @@ * 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 _ from 'lodash'; import * as React from 'react'; -import { User, UserRole } from '../../types/items'; +import { FormattedMessage } from 'react-intl'; import { Button, Input, Table } from 'reactstrap'; -import CreateUserLinkButtonComponent from './users/CreateUserLinkButtonComponent'; -import TooltipHelpComponent from '../../components/TooltipHelpComponent'; +import TooltipHelpComponent from '../TooltipHelpComponent'; +import { userApi } from '../../redux/api/userApi'; +import { User, UserRole } from '../../types/items'; +import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; +import translate from '../../utils/translate'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import { FormattedMessage } from 'react-intl'; -import UnsavedWarningContainer from '../../containers/UnsavedWarningContainer'; -import { store } from '../../store' -import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; +import { UnsavedWarningComponent } from '../UnsavedWarningComponent'; +import CreateUserLinkButtonComponent from './users/CreateUserLinkButtonComponent'; -interface UserDisplayComponentProps { - users: User[]; - deleteUser: (email: string) => Promise; - edited: boolean; - editUser: (email: string, newRole: UserRole) => void; - submitUserEdits: () => Promise; -} /** * Component which shows user details - * @param props defined above * @returns User Detail element */ -export default function UserDetailComponent(props: UserDisplayComponentProps) { - const titleStyle: React.CSSProperties = { - textAlign: 'center' - }; - - const tableStyle: React.CSSProperties = { - marginLeft: '10%', - marginRight: '10%' - }; +export default function UserDetailComponent() { + const { data: users = [] } = userApi.useGetUsersQuery(undefined); + const [submitUserEdits] = userApi.useEditUsersMutation(); + const [submitDeleteUser] = userApi.useDeleteUsersMutation(); + const [localUsersChanges, setLocalUsersChanges] = React.useState([]); + const [hasChanges, setHasChanges] = React.useState(false); - const buttonsStyle: React.CSSProperties = { - display: 'flex', - justifyContent: 'space-between' - } - - const tooltipStyle = { - display: 'inline-block', - fontSize: '50%' - }; - const removeUnsavedChangesFunction = (callback: () => void) => { - // This function is called to reset all the inputs to the initial state - // Do not need to do anything since unsaved changes will be removed after leaving this page - callback(); + React.useEffect(() => { setLocalUsersChanges(users) }, [users]) + React.useEffect(() => { !_.isEqual(users, localUsersChanges) ? setHasChanges(true) : setHasChanges(false) }, [localUsersChanges, users]) + const submitChanges = async () => { + submitUserEdits(localUsersChanges) + .unwrap() + .then(() => { + showSuccessNotification(translate('users.successfully.edit.users')); + }) + .catch(() => { + showErrorNotification(translate('users.failed.to.edit.users')) + }) } - const submitUnsavedChangesFunction = (successCallback: () => void, failureCallback: () => void) => { - // This function is called to submit the unsaved changes - props.submitUserEdits().then(successCallback, failureCallback); + const editUser = (e: React.ChangeEvent, targetUser: User) => { + // copy user, and update role + const updatedUser: User = { ...targetUser, role: e.target.value as UserRole } + // make new list from existing local user state + const updatedList = localUsersChanges.map(user => (user.email === targetUser.email) ? updatedUser : user) + setLocalUsersChanges(updatedList) + // editUser(user.email, target.value as UserRole); } - const addUnsavedChanges = () => { - // Notify that there are unsaved changes - store.dispatch(unsavedWarningSlice.actions.updateUnsavedChanges({ - removeFunction: removeUnsavedChangesFunction, - submitFunction: submitUnsavedChangesFunction - })); + const deleteUser = (email: string) => { + submitDeleteUser(email) + .unwrap() + .then(() => { + showSuccessNotification(translate('users.successfully.delete.user')) + }) + .catch(() => { + showErrorNotification(translate('users.failed.to.delete.user')); + }) } - const clearUnsavedChanges = () => { - // Notify that there are no unsaved changes - store.dispatch(unsavedWarningSlice.actions.removeUnsavedChanges()); - } return (
- +

@@ -91,17 +89,14 @@ export default function UserDetailComponent(props: UserDisplayComponentProps) { - {props.users.map(user => ( + {localUsersChanges.map(user => ( {user.email} { - props.editUser(user.email, target.value as UserRole); - addUnsavedChanges(); - }} + onChange={e => editUser(e, user)} > {Object.entries(UserRole).map(([role, val]) => ( @@ -109,7 +104,7 @@ export default function UserDetailComponent(props: UserDisplayComponentProps) { - @@ -121,11 +116,8 @@ export default function UserDetailComponent(props: UserDisplayComponentProps) { @@ -135,3 +127,22 @@ export default function UserDetailComponent(props: UserDisplayComponentProps) {

) } + +const titleStyle: React.CSSProperties = { + textAlign: 'center' +}; + +const tableStyle: React.CSSProperties = { + marginLeft: '10%', + marginRight: '10%' +}; + +const buttonsStyle: React.CSSProperties = { + display: 'flex', + justifyContent: 'space-between' +} + +const tooltipStyle = { + display: 'inline-block', + fontSize: '50%' +}; diff --git a/src/client/app/components/admin/UsersDetailComponentWIP.tsx b/src/client/app/components/admin/UsersDetailComponentWIP.tsx deleted file mode 100644 index 9e53254b7..000000000 --- a/src/client/app/components/admin/UsersDetailComponentWIP.tsx +++ /dev/null @@ -1,140 +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 * as _ from 'lodash'; -import * as React from 'react'; -import { FormattedMessage } from 'react-intl'; -import { Button, Input, Table } from 'reactstrap'; -import TooltipHelpComponent from '../../components/TooltipHelpComponent'; -import { userApi } from '../../redux/api/userApi'; -import { User, UserRole } from '../../types/items'; -import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; -import translate from '../../utils/translate'; -import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import { UnsavedWarningComponentWIP } from '../UnsavedWarningComponentWIP'; -import CreateUserLinkButtonComponent from './users/CreateUserLinkButtonComponent'; - - -/** - * Component which shows user details - * @returns User Detail element - */ -export default function UserDetailComponentWIP() { - const { data: users = [] } = userApi.useGetUsersQuery(undefined); - const [submitUserEdits] = userApi.useEditUsersMutation(); - const [localUsersChanges, setLocalUsersChanges] = React.useState([]); - const [hasChanges, setHasChanges] = React.useState(false); - - - React.useEffect(() => { setLocalUsersChanges(users) }, [users]) - React.useEffect(() => { !_.isEqual(users, localUsersChanges) ? setHasChanges(true) : setHasChanges(false) }, [localUsersChanges, users]) - const submitChanges = async () => { - submitUserEdits(localUsersChanges) - .unwrap() - .then(() => { - showSuccessNotification(translate('users.successfully.edit.users')); - }) - .catch(() => { - showErrorNotification(translate('users.failed.to.edit.users')) - }) - } - - const editUser = (e: React.ChangeEvent, targetUser: User) => { - // copy user, and update role - const updatedUser: User = { ...targetUser, role: e.target.value as UserRole } - // make new list from existing local user state - const updatedList = localUsersChanges.map(user => (user.email === targetUser.email) ? updatedUser : user) - setLocalUsersChanges(updatedList) - // editUser(user.email, target.value as UserRole); - } - - const deleteUser = (email: string) => { - setLocalUsersChanges(localUsersChanges.filter(user => user.email !== email)) - } - - - return ( -
- - -
-

- -
- -
-

-
- - - - - - - - - - {localUsersChanges.map(user => ( - - - - - - ))} - -
{user.email} - editUser(e, user)} - > - {Object.entries(UserRole).map(([role, val]) => ( - - ))} - - - -
-
- - -
-
-
-
- ) -} - -const titleStyle: React.CSSProperties = { - textAlign: 'center' -}; - -const tableStyle: React.CSSProperties = { - marginLeft: '10%', - marginRight: '10%' -}; - -const buttonsStyle: React.CSSProperties = { - display: 'flex', - justifyContent: 'space-between' -} - -const tooltipStyle = { - display: 'inline-block', - fontSize: '50%' -}; diff --git a/src/client/app/components/conversion/ConversionViewComponent.tsx b/src/client/app/components/conversion/ConversionViewComponent.tsx index 3f1ba7adb..418931908 100644 --- a/src/client/app/components/conversion/ConversionViewComponent.tsx +++ b/src/client/app/components/conversion/ConversionViewComponent.tsx @@ -10,8 +10,8 @@ import { Button } from 'reactstrap'; import { ConversionData } from 'types/redux/conversions'; import '../../styles/card-page.css'; import translate from '../../utils/translate'; -import EditConversionModalComponentWIP from './EditConversionModalComponent'; -import { useAppSelector } from '../../redux/hooks'; +import EditConversionModalComponent from './EditConversionModalComponent'; +import { useAppSelector } from '../../redux/reduxHooks'; import { selectUnitDataById } from '../../redux/api/unitsApi'; interface ConversionViewComponentProps { @@ -79,7 +79,7 @@ export default function ConversionViewComponent(props: ConversionViewComponentPr {/* Creates a child ConversionModalEditComponent */} -
- +
{/* Attempt to create a ConversionViewComponent for each ConversionData in Conversions State after sorting by @@ -80,7 +80,7 @@ export default function ConversionsDetailComponent() { (((unitDataById[conversionB.sourceId].identifier + unitDataById[conversionB.destinationId].identifier).toLowerCase() > (unitDataById[conversionA.sourceId].identifier + unitDataById[conversionA.destinationId].identifier).toLowerCase()) ? -1 : 0)) .map(conversionData => ( - ' + conversionData.destinationId} /> diff --git a/src/client/app/components/conversion/CreateConversionModalComponent.tsx b/src/client/app/components/conversion/CreateConversionModalComponent.tsx index e97258270..9b828a551 100644 --- a/src/client/app/components/conversion/CreateConversionModalComponent.tsx +++ b/src/client/app/components/conversion/CreateConversionModalComponent.tsx @@ -9,7 +9,7 @@ import { FormattedMessage } from 'react-intl'; import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; import TooltipHelpComponent from '../TooltipHelpComponent'; import { conversionsApi } from '../../redux/api/conversionsApi'; -import { useAppSelector } from '../../redux/hooks'; +import { useAppSelector } from '../../redux/reduxHooks'; import { selectDefaultCreateConversionValues, selectIsValidConversion } from '../../redux/selectors/adminSelectors'; import '../../styles/modal.css'; import { tooltipBaseStyle } from '../../styles/modalStyle'; diff --git a/src/client/app/components/conversion/EditConversionModalComponent.tsx b/src/client/app/components/conversion/EditConversionModalComponent.tsx index ae10f916a..cb36d3ebe 100644 --- a/src/client/app/components/conversion/EditConversionModalComponent.tsx +++ b/src/client/app/components/conversion/EditConversionModalComponent.tsx @@ -9,7 +9,7 @@ import { Button, Col, Container, FormGroup, Input, Label, Modal, ModalBody, Moda import TooltipHelpComponent from '../TooltipHelpComponent'; import { conversionsApi } from '../../redux/api/conversionsApi'; import { selectUnitDataById } from '../../redux/api/unitsApi'; -import { useAppSelector } from '../../redux/hooks'; +import { useAppSelector } from '../../redux/reduxHooks'; import '../../styles/modal.css'; import { tooltipBaseStyle } from '../../styles/modalStyle'; import { TrueFalseType } from '../../types/items'; diff --git a/src/client/app/components/groups/CreateGroupModalComponent.tsx b/src/client/app/components/groups/CreateGroupModalComponent.tsx index 6d3dc364e..3f4546d72 100644 --- a/src/client/app/components/groups/CreateGroupModalComponent.tsx +++ b/src/client/app/components/groups/CreateGroupModalComponent.tsx @@ -13,7 +13,7 @@ import { GroupData } from 'types/redux/groups'; import TooltipHelpComponent from '../TooltipHelpComponent'; import { groupsApi, selectGroupDataById } from '../../redux/api/groupsApi'; import { selectUnitDataById } from '../../redux/api/unitsApi'; -import { useAppSelector } from '../../redux/hooks'; +import { useAppSelector } from '../../redux/reduxHooks'; import { selectPossibleGraphicUnits } from '../../redux/selectors/adminSelectors'; import '../../styles/modal.css'; import { tooltipBaseStyle } from '../../styles/modalStyle'; @@ -38,7 +38,7 @@ import { selectMeterDataById } from '../../redux/api/metersApi'; * Defines the create group modal form * @returns Group create element */ -export default function CreateGroupModalComponentWIP() { +export default function CreateGroupModalComponent() { const [createGroup] = groupsApi.useCreateGroupMutation() // Meters state diff --git a/src/client/app/components/groups/EditGroupModalComponent.tsx b/src/client/app/components/groups/EditGroupModalComponent.tsx index be459ab75..aa2cd68cf 100644 --- a/src/client/app/components/groups/EditGroupModalComponent.tsx +++ b/src/client/app/components/groups/EditGroupModalComponent.tsx @@ -13,9 +13,9 @@ import { } from 'reactstrap'; import TooltipHelpComponent from '../TooltipHelpComponent'; import { groupsApi, selectGroupDataById } from '../../redux/api/groupsApi'; -import { useAppSelector } from '../../redux/hooks'; +import { useAppSelector } from '../../redux/reduxHooks'; import { selectPossibleGraphicUnits } from '../../redux/selectors/adminSelectors'; -import { selectIsAdmin } from '../../reducers/currentUser'; +import { selectIsAdmin } from '../../redux/slices/currentUserSlice'; import { store } from '../../store'; import '../../styles/card-page.css'; import '../../styles/modal.css'; @@ -55,7 +55,7 @@ interface EditGroupModalComponentProps { * @param props state variables needed to define the component * @returns Group edit element */ -export default function EditGroupModalComponentWIP(props: EditGroupModalComponentProps) { +export default function EditGroupModalComponent(props: EditGroupModalComponentProps) { const [submitGroupEdits] = groupsApi.useEditGroupMutation() const [deleteGroup] = groupsApi.useDeleteGroupMutation() // Meter state diff --git a/src/client/app/components/groups/GroupViewComponent.tsx b/src/client/app/components/groups/GroupViewComponent.tsx index 669400040..3b68edccb 100644 --- a/src/client/app/components/groups/GroupViewComponent.tsx +++ b/src/client/app/components/groups/GroupViewComponent.tsx @@ -9,12 +9,12 @@ import { FormattedMessage } from 'react-intl'; import { Button } from 'reactstrap'; import { GroupData } from 'types/redux/groups'; import { selectUnitDataById } from '../../redux/api/unitsApi'; -import { useAppSelector } from '../../redux/hooks'; -import { selectIsAdmin } from '../../reducers/currentUser'; +import { useAppSelector } from '../../redux/reduxHooks'; +import { selectIsAdmin } from '../../redux/slices/currentUserSlice'; import '../../styles/card-page.css'; import { noUnitTranslated } from '../../utils/input'; import translate from '../../utils/translate'; -import EditGroupModalComponentWIP from './EditGroupModalComponent'; +import EditGroupModalComponent from './EditGroupModalComponent'; interface GroupViewComponentProps { group: GroupData; @@ -25,7 +25,7 @@ interface GroupViewComponentProps { * @param props variables passed in to define * @returns Group info card element */ -export default function GroupViewComponentWIP(props: GroupViewComponentProps) { +export default function GroupViewComponent(props: GroupViewComponentProps) { // Don't check if admin since only an admin is allowed to route to this page. @@ -77,7 +77,7 @@ export default function GroupViewComponentWIP(props: GroupViewComponentProps) { {loggedInAsAdmin ? : } {/* Creates a child GroupModalEditComponent */} - {/* The actual button for create is inside this component. */} - < CreateGroupModalComponentWIP + < CreateGroupModalComponent />
} {
{Object.values(visibleGroups) - .map(groupData => ( ())} diff --git a/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx b/src/client/app/components/maps/MapCalibrationInitiateComponent.tsx index 92db6c358..bcda78fa2 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 { CalibrationModeTypes, MapMetadata } from '../../types/redux/map'; import { ChangeEvent } from 'react'; -import { logToServer } from '../../actions/logs'; +import { logToServer } from '../../redux/actions/logs'; import { FormattedMessage, injectIntl, WrappedComponentProps } from 'react-intl'; /** diff --git a/src/client/app/components/maps/MapViewComponent.tsx b/src/client/app/components/maps/MapViewComponent.tsx index f3d8f207c..ba7216dbb 100644 --- a/src/client/app/components/maps/MapViewComponent.tsx +++ b/src/client/app/components/maps/MapViewComponent.tsx @@ -9,9 +9,6 @@ import { hasToken } from '../../utils/token'; import { FormattedMessage, injectIntl, WrappedComponentProps } from 'react-intl'; import { CalibrationModeTypes, MapMetadata } from '../../types/redux/map'; import * as moment from 'moment'; -import { store } from '../../store'; -import { fetchMapsDetails, submitEditedMaps, confirmEditedMaps } from '../../actions/map'; -import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; interface MapViewProps { // The ID of the map to be displayed @@ -93,24 +90,28 @@ class MapViewComponent extends React.Component void) { - // This function is called to reset all the inputs to the initial state - store.dispatch(confirmEditedMaps()).then(() => { - store.dispatch(fetchMapsDetails()).then(callback); - }); - } + // Re-implement After RTK migration + // private removeUnsavedChangesFunction(callback: () => void) { + // // This function is called to reset all the inputs to the initial state + // store.dispatch(confirmEditedMaps()).then(() => { + // store.dispatch(fetchMapsDetails()).then(callback); + // }); + // } - private submitUnsavedChangesFunction(successCallback: () => void, failureCallback: () => void) { - // This function is called to submit the unsaved changes - store.dispatch(submitEditedMaps()).then(successCallback, failureCallback); - } + // Re-implement After RTK migration + // private submitUnsavedChangesFunction(successCallback: () => void, failureCallback: () => void) { + // // This function is called to submit the unsaved changes + // store.dispatch(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 - })); + // 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) { diff --git a/src/client/app/components/maps/MapsDetailComponent.tsx b/src/client/app/components/maps/MapsDetailComponent.tsx index 94bdf54d0..378df51f2 100644 --- a/src/client/app/components/maps/MapsDetailComponent.tsx +++ b/src/client/app/components/maps/MapsDetailComponent.tsx @@ -8,8 +8,6 @@ import { Link } from 'react-router-dom'; import { Button, Table } from 'reactstrap'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import MapViewContainer from '../../containers/maps/MapViewContainer'; -import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; -import { store } from '../../store'; import { hasToken } from '../../utils/token'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; @@ -110,7 +108,7 @@ export default class MapsDetailComponent extends React.Component); editMeter({ meterData: submitState, shouldRefreshViews: shouldRefreshReadingViews }) - dispatch(unsavedWarningSlice.actions.removeUnsavedChanges()); } else { // Tell user that not going to update due to input issues. notifyUser(translate('meter.input.error')); diff --git a/src/client/app/components/meters/MeterViewComponent.tsx b/src/client/app/components/meters/MeterViewComponent.tsx index 8007060a3..7d608a799 100644 --- a/src/client/app/components/meters/MeterViewComponent.tsx +++ b/src/client/app/components/meters/MeterViewComponent.tsx @@ -7,12 +7,12 @@ import { useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { Button } from 'reactstrap'; import { MeterData } from 'types/redux/meters'; -import { useAppSelector } from '../../redux/hooks'; +import { useAppSelector } from '../../redux/reduxHooks'; import { selectGraphicName, selectUnitName } from '../../redux/selectors/adminSelectors'; import '../../styles/card-page.css'; import translate from '../../utils/translate'; -import EditMeterModalComponentWIP from './EditMeterModalComponent'; -import { selectIsAdmin } from '../../reducers/currentUser'; +import EditMeterModalComponent from './EditMeterModalComponent'; +import { selectIsAdmin } from '../../redux/slices/currentUserSlice'; interface MeterViewComponentProps { meter: MeterData; @@ -80,7 +80,7 @@ export default function MeterViewComponent(props: MeterViewComponentProps) { {/* Creates a child MeterModalEditComponent */} - {isAdmin &&
- +
} { @@ -47,7 +47,7 @@ export default function MetersDetailComponent() { {/* Optional Chaining to prevent from crashing upon startup race conditions*/} {Object.values(visibleMeters) .map(MeterData => ( - diff --git a/src/client/app/components/router/GraphLinkComponent.tsx b/src/client/app/components/router/GraphLinkComponent.tsx index e7e83d9f5..a5d5aae93 100644 --- a/src/client/app/components/router/GraphLinkComponent.tsx +++ b/src/client/app/components/router/GraphLinkComponent.tsx @@ -7,17 +7,17 @@ import InitializingComponent from '../router/InitializingComponent'; import moment from 'moment'; import * as React from 'react'; import { Navigate, useSearchParams } from 'react-router-dom'; -import { graphSlice } from '../../reducers/graph'; +import { graphSlice } from '../../redux/slices/graphSlice'; import { useWaitForInit } from '../../redux/componentHooks'; -import { useAppDispatch } from '../../redux/hooks'; +import { useAppDispatch } from '../../redux/reduxHooks'; import { validateComparePeriod, validateSortingOrder } from '../../utils/calculateCompare'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; import { showErrorNotification } from '../../utils/notifications'; import translate from '../../utils/translate'; import { TimeInterval } from '../../../../common/TimeInterval'; import { ChartTypes, LineGraphRate, MeterOrGroup } from '../../types/redux/graph'; -import { changeSelectedMap } from '../../actions/map'; -import { appStateSlice } from '../../reducers/appStateSlice'; +import { changeSelectedMap } from '../../redux/actions/map'; +import { appStateSlice } from '../../redux/slices/appStateSlice'; export const GraphLink = () => { const dispatch = useAppDispatch(); diff --git a/src/client/app/components/unit/CreateUnitModalComponent.tsx b/src/client/app/components/unit/CreateUnitModalComponent.tsx index e5a34d9eb..a2e1c4030 100644 --- a/src/client/app/components/unit/CreateUnitModalComponent.tsx +++ b/src/client/app/components/unit/CreateUnitModalComponent.tsx @@ -2,26 +2,26 @@ * 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 { useDispatch } from 'react-redux'; import { useEffect, useState } from 'react'; import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; import { FormattedMessage } from 'react-intl'; -import translate from '../../utils/translate'; import '../../styles/modal.css'; import { TrueFalseType } from '../../types/items'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { UnitRepresentType, DisplayableType, UnitType } from '../../types/redux/units'; -import { addUnit } from '../../actions/units'; import { tooltipBaseStyle } from '../../styles/modalStyle'; -import { Dispatch } from 'types/redux/actions'; +import { unitsApi } from '../../redux/api/unitsApi'; +import { useTranslate } from '../../redux/componentHooks'; +import { showSuccessNotification, showErrorNotification } from '../../utils/notifications'; /** * Defines the create unit modal form * @returns Unit create element */ export default function CreateUnitModalComponent() { - const dispatch: Dispatch = useDispatch(); + const [submitCreateUnit] = unitsApi.useAddUnitMutation(); + const translate = useTranslate() const defaultValues = { name: '', @@ -101,7 +101,14 @@ export default function CreateUnitModalComponent() { state.typeOfUnit = UnitType.suffix; } // Add the new unit and update the store - dispatch(addUnit(state)); + submitCreateUnit(state) + .unwrap() + .then(() => { + showSuccessNotification(translate('unit.successfully.create.unit')); + }) + .catch(() => { + showErrorNotification(translate('unit.failed.to.create.unit')); + }) resetState(); }; diff --git a/src/client/app/components/unit/EditUnitModalComponent.tsx b/src/client/app/components/unit/EditUnitModalComponent.tsx index c0ee65083..f01be808c 100644 --- a/src/client/app/components/unit/EditUnitModalComponent.tsx +++ b/src/client/app/components/unit/EditUnitModalComponent.tsx @@ -2,27 +2,24 @@ * 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 { store } from '../../store'; +import { store } from '../../store'; //Realize that * is already imported from react import { useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useDispatch } from 'react-redux'; import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; -import { Dispatch } from 'types/redux/actions'; -import { submitEditedUnit } from '../../actions/units'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; -import { unsavedWarningSlice } from '../../reducers/unsavedWarning'; import { selectConversionsDetails } from '../../redux/api/conversionsApi'; import { selectMeterDataById } from '../../redux/api/metersApi'; -import { useAppSelector } from '../../redux/hooks'; +import { useAppSelector } from '../../redux/reduxHooks'; import '../../styles/modal.css'; import { tooltipBaseStyle } from '../../styles/modalStyle'; import { TrueFalseType } from '../../types/items'; import { DisplayableType, UnitData, UnitRepresentType, UnitType } from '../../types/redux/units'; import { notifyUser } from '../../utils/input'; -import translate from '../../utils/translate'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; - +import { unitsApi } from '../../redux/api/unitsApi'; +import { showSuccessNotification, showErrorNotification } from '../../utils/notifications'; +import { useTranslate } from '../../redux/componentHooks'; interface EditUnitModalComponentProps { show: boolean; unit: UnitData; @@ -36,7 +33,8 @@ interface EditUnitModalComponentProps { * @returns Unit edit element */ export default function EditUnitModalComponent(props: EditUnitModalComponentProps) { - const dispatch: Dispatch = useDispatch(); + const [submitEditedUnit] = unitsApi.useEditUnitMutation(); + const translate = useTranslate(); // Set existing unit values const values = { @@ -105,7 +103,7 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp const shouldUpdateUnit = (): boolean => { // true if inputted values are okay and there are changes. let inputOk = true; - const meterDataByID = selectMeterDataById(store.getState()) + const meterDataByID = selectMeterDataById(store.getState()) // Check for case 1 if (props.unit.typeOfUnit === UnitType.meter && state.typeOfUnit !== UnitType.meter) { @@ -163,14 +161,20 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp if (state.typeOfUnit != UnitType.suffix && state.suffix != '') { state.typeOfUnit = UnitType.suffix; } - // Save our changes by dispatching the submitEditedUnit action - dispatch(submitEditedUnit(state, shouldRedoCik, shouldRefreshReadingViews)); + // Save our changes by dispatching the submitEditedUnit mutation + submitEditedUnit({ editedUnit: state, shouldRedoCik, shouldRefreshReadingViews }) + .unwrap() + .then(() => { + showSuccessNotification(translate('unit.successfully.edited.unit')); + }) + .catch(() => { + showErrorNotification(translate('unit.failed.to.edit.unit')); + }) // The updated unit is not fetched to save time. However, the identifier might have been // automatically set if it was empty. Mimic that here. if (state.identifier === '') { state.identifier = state.name; } - dispatch(unsavedWarningSlice.actions.removeUnsavedChanges()); } } diff --git a/src/client/app/components/unit/UnitsDetailComponent.tsx b/src/client/app/components/unit/UnitsDetailComponent.tsx index 41823be83..5d76d0ea0 100644 --- a/src/client/app/components/unit/UnitsDetailComponent.tsx +++ b/src/client/app/components/unit/UnitsDetailComponent.tsx @@ -7,7 +7,7 @@ import { FormattedMessage } from 'react-intl'; import SpinnerComponent from '../../components/SpinnerComponent'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { selectAllUnits, selectUnitDataResult } from '../../redux/api/unitsApi'; -import { useAppSelector } from '../../redux/hooks'; +import { useAppSelector } from '../../redux/reduxHooks'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateUnitModalComponent from './CreateUnitModalComponent'; import UnitViewComponent from './UnitViewComponent'; diff --git a/src/client/app/containers/BarChartContainer.ts b/src/client/app/containers/BarChartContainer.ts index a6521c209..41f9e1de9 100644 --- a/src/client/app/containers/BarChartContainer.ts +++ b/src/client/app/containers/BarChartContainer.ts @@ -1,10 +1,8 @@ -/* eslint-disable */ -//@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/. */ + import * as _ from 'lodash'; import * as moment from 'moment'; import { connect } from 'react-redux'; @@ -18,6 +16,14 @@ import { barUnitLabel } from '../utils/graphics'; import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; import { UnitRepresentType } from '../types/redux/units'; +// TODO Marked For Deletion after RTK migration solidified +/* 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 */ + + /** * Passes the current redux state of the barchart, and turns it into props for the React * component, which is what will be visible on the page. Makes it possible to access diff --git a/src/client/app/containers/ChartLinkContainer.ts b/src/client/app/containers/ChartLinkContainer.ts index ec219785f..03d0cba82 100644 --- a/src/client/app/containers/ChartLinkContainer.ts +++ b/src/client/app/containers/ChartLinkContainer.ts @@ -6,7 +6,7 @@ import { connect } from 'react-redux'; import ChartLinkComponent from '../components/ChartLinkComponent'; import { ChartTypes } from '../types/redux/graph'; import { RootState } from '../store'; -import { selectChartToRender, selectGraphState } from '../reducers/graph'; +import { selectChartToRender, selectGraphState } from '../redux/slices/graphSlice'; /** * Passes the current redux state of the chart link text, and turns it into props for the React diff --git a/src/client/app/containers/CompareChartContainer.ts b/src/client/app/containers/CompareChartContainer.ts index 8d1b6fb05..bb06b324c 100644 --- a/src/client/app/containers/CompareChartContainer.ts +++ b/src/client/app/containers/CompareChartContainer.ts @@ -16,7 +16,12 @@ import { selectUnitDataById } from '../redux/api/unitsApi'; import { RootState } from '../store'; import { selectGroupDataById } from '../redux/api/groupsApi'; import { selectMeterDataById } from '../redux/api/metersApi'; -import { selectAreaUnit, selectComparePeriod, selectCompareTimeInterval, selectGraphAreaNormalization, selectSelectedUnit } from '../reducers/graph'; +import { + selectAreaUnit, selectComparePeriod, + selectCompareTimeInterval, selectGraphAreaNormalization, + selectSelectedUnit +} from '../redux/slices/graphSlice'; + export interface CompareEntity { id: number; isGroup: boolean; @@ -227,7 +232,7 @@ function mapStateToProps(state: RootState, ownProps: CompareChartContainerProps) locales: Locales // makes locales available for use } }; - props.config.locale = state.options.selectedLanguage; + props.config.locale = state.appState.selectedLanguage; return props; } diff --git a/src/client/app/containers/LineChartContainer.ts b/src/client/app/containers/LineChartContainer.ts index f69fa1fb5..331ff35ff 100644 --- a/src/client/app/containers/LineChartContainer.ts +++ b/src/client/app/containers/LineChartContainer.ts @@ -1,9 +1,13 @@ -/* eslint-disable */ -//@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 Marked For Deletion after RTK migration solidified +/* eslint-disable jsdoc/check-param-names */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// @ts-nocheck + import * as _ from 'lodash'; import * as moment from 'moment'; import { connect } from 'react-redux'; @@ -16,6 +20,7 @@ import { DataType } from '../types/Datasources'; import { lineUnitLabel } from '../utils/graphics'; import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; +/* eslint-disable jsdoc/require-param */ function mapStateToProps(state: State) { const timeInterval = state.graph.queryTimeInterval; const unitID = state.graph.selectedUnit; diff --git a/src/client/app/containers/MapChartContainer.ts b/src/client/app/containers/MapChartContainer.ts index 130e65b07..d720432df 100644 --- a/src/client/app/containers/MapChartContainer.ts +++ b/src/client/app/containers/MapChartContainer.ts @@ -1,10 +1,13 @@ -/* eslint-disable */ -//@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 Marked For Deletion after RTK migration solidified +/* eslint-disable jsdoc/check-param-names */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// @ts-nocheck + import * as _ from 'lodash'; import * as moment from 'moment'; import Plot, { PlotParams } from 'react-plotly.js'; diff --git a/src/client/app/containers/MeterDropdownContainer.ts b/src/client/app/containers/MeterDropdownContainer.ts deleted file mode 100644 index 33d409f70..000000000 --- a/src/client/app/containers/MeterDropdownContainer.ts +++ /dev/null @@ -1,24 +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 * as _ from 'lodash'; -import { connect } from 'react-redux'; -import { selectMeterDataById } from 'redux/api/metersApi'; -import MeterDropdownComponent from '../components/MeterDropDownComponent'; -import { adminSlice } from '../reducers/admin'; -import { RootState } from '../store'; -import { Dispatch } from '../types/redux/actions'; - -function mapStateToProps(state: RootState) { - return { - meters: _.sortBy(_.values(selectMeterDataById(state)).map(meter => ({ id: meter.id, name: meter.name })), 'name') - }; -} -function mapDispatchToProps(dispatch: Dispatch) { - return { - updateSelectedMeter: (meterID: number) => dispatch(adminSlice.actions.updateImportMeter(meterID)) - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(MeterDropdownComponent); diff --git a/src/client/app/containers/RadarChartComponent.tsx b/src/client/app/containers/RadarChartComponent.tsx deleted file mode 100644 index abc522534..000000000 --- a/src/client/app/containers/RadarChartComponent.tsx +++ /dev/null @@ -1,335 +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 * as _ from 'lodash'; -import * as moment from 'moment'; -import * as React from 'react' -import getGraphColor from '../utils/getGraphColor'; -import translate from '../utils/translate'; -import Plot from 'react-plotly.js'; -import { Layout } from 'plotly.js'; -import Locales from '../types/locales'; -import { DataType } from '../types/Datasources'; -import { lineUnitLabel } from '../utils/graphics'; -import { AreaUnitType, getAreaUnitConversion } from '../utils/getAreaUnitConversion'; -import { useAppSelector } from '../redux/hooks'; -import { - selectAreaUnit, selectGraphAreaNormalization, selectLineGraphRate, - selectSelectedGroups, selectSelectedMeters, selectSelectedUnit -} from '../reducers/graph'; -import { selectUnitDataById } from '../redux/api/unitsApi'; -import { selectMeterDataById } from '../redux/api/metersApi'; -import { selectRadarChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; -import { readingsApi } from '../redux/api/readingsApi'; -import { selectGroupDataById } from '../redux/api/groupsApi'; -import LogoSpinner from '../components/LogoSpinner'; - -export default function RadarChartComponent() { - const { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } = useAppSelector(selectRadarChartQueryArgs) - const { data: meterReadings, isLoading: meterIsLoading } = readingsApi.useLineQuery(meterArgs, { skip: meterShouldSkip }); - const { data: groupData, isLoading: groupIsLoading } = readingsApi.useLineQuery(groupArgs, { skip: groupShouldSkip }); - const datasets: any[] = []; - // Time range selected - // graphic unit selected - const graphingUnit = useAppSelector(selectSelectedUnit); - // The current selected rate - const currentSelectedRate = useAppSelector(selectLineGraphRate); - const unitDataById = useAppSelector(selectUnitDataById); - - const areaNormalization = useAppSelector(selectGraphAreaNormalization); - const selectedAreaUnit = useAppSelector(selectAreaUnit); - const selectedMeters = useAppSelector(selectSelectedMeters); - const selectedGroups = useAppSelector(selectSelectedGroups); - const meterDataById = useAppSelector(selectMeterDataById); - const groupDataById = useAppSelector(selectGroupDataById); - - if (meterIsLoading || groupIsLoading) { - return - // return - } - - let unitLabel = ''; - let needsRateScaling = false; - // 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 = unitDataById[graphingUnit]; - if (selectUnitState !== undefined) { - // Determine the r-axis label and if the rate needs to be scaled. - const returned = lineUnitLabel(selectUnitState, currentSelectedRate, areaNormalization, selectedAreaUnit); - unitLabel = returned.unitLabel - needsRateScaling = returned.needsRateScaling; - } - } - // The rate will be 1 if it is per hour (since state readings are per hour) or no rate scaling so no change. - const rateScaling = needsRateScaling ? currentSelectedRate.rate : 1; - - // Add all valid data from existing meters to the radar plot - for (const meterID of selectedMeters) { - if (meterReadings) { - const 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)) { - // Convert the meter area into the proper unit if normalizing by area or use 1 if not so won't change reading values. - const areaScaling = areaNormalization ? - meterArea * getAreaUnitConversion(meterDataById[meterID].areaUnit, selectedAreaUnit) : 1; - // Divide areaScaling into the rate so have complete scaling factor for readings. - const scaling = rateScaling / areaScaling; - const readingsData = meterReadings[meterID] - if (readingsData) { - const label = meterDataById[meterID].identifier; - const colorID = meterID; - // if (readingsData.readings === undefined) { - // throw new Error('Unacceptable condition: readingsData.readings is undefined.'); - // } - // Create two arrays for the distance (rData) and angle (thetaData) values. Fill the array with the data from the line readings. - // HoverText is the popup value show for each reading. - const thetaData: string[] = []; - const rData: number[] = []; - const hoverText: string[] = []; - const readings = _.values(readingsData); - readings.forEach(reading => { - // As usual, we want to interpret the readings in UTC. We lose the timezone as these start/endTimestamp - // are equivalent to Unix timestamp in milliseconds. - const st = moment.utc(reading.startTimestamp); - // Time reading is in the middle of the start and end timestamp - const timeReading = st.add(moment.utc(reading.endTimestamp).diff(st) / 2); - // The angular value is the date, internationalized. - thetaData.push(timeReading.format('ddd, ll LTS')); - // The scaling is the factor to change the reading by. - const readingValue = reading.reading * scaling; - rData.push(readingValue); - hoverText.push(` ${timeReading.format('ddd, ll LTS')}
${label}: ${readingValue.toPrecision(6)} ${unitLabel}`); - }); - - // This variable contains all the elements (plot values, line type, etc.) assigned to the data parameter of the Plotly object - datasets.push({ - name: label, - theta: thetaData, - r: rData, - text: hoverText, - hoverinfo: 'text', - type: 'scatterpolar', - mode: 'lines', - line: { - shape: 'spline', - width: 2, - color: getGraphColor(colorID, DataType.Meter) - } - }); - } - } - } - } - - // Add all valid data from existing groups to the radar plot - for (const groupID of selectedGroups) { - // const byGroupID = state.readings.line.byGroupID[groupID]; - if (groupData) { - const groupArea = groupDataById[groupID].area; - // We either don't care about area, or we do in which case there needs to be a nonzero area. - if (!areaNormalization || (groupArea > 0 && groupDataById[groupID].areaUnit != AreaUnitType.none)) { - // Convert the group area into the proper unit if normalizing by area or use 1 if not so won't change reading values. - const areaScaling = areaNormalization ? - groupArea * getAreaUnitConversion(groupDataById[groupID].areaUnit, selectedAreaUnit) : 1; - // Divide areaScaling into the rate so have complete scaling factor for readings. - const scaling = rateScaling / areaScaling; - const readingsData = groupData[groupID] - if (readingsData) { - const label = groupDataById[groupID].name; - const colorID = groupID; - // if (readingsData.readings === undefined) { - // throw new Error('Unacceptable condition: readingsData.readings is undefined.'); - // } - // Create two arrays for the distance (rData) and angle (thetaData) values. Fill the array with the data from the line readings. - // HoverText is the popup value show for each reading. - const thetaData: string[] = []; - const rData: number[] = []; - const hoverText: string[] = []; - const readings = _.values(readingsData); - readings.forEach(reading => { - // As usual, we want to interpret the readings in UTC. We lose the timezone as these start/endTimestamp - // are equivalent to Unix timestamp in milliseconds. - const st = moment.utc(reading.startTimestamp); - // Time reading is in the middle of the start and end timestamp - const timeReading = st.add(moment.utc(reading.endTimestamp).diff(st) / 2); - // The angular value is the date, internationalized. - thetaData.push(timeReading.format('ddd, ll LTS')); - // The scaling is the factor to change the reading by. - const readingValue = reading.reading * scaling; - rData.push(readingValue); - hoverText.push(` ${timeReading.format('ddd, ll LTS')}
${label}: ${readingValue.toPrecision(6)} ${unitLabel}`); - }); - - // This variable contains all the elements (plot values, line type, etc.) assigned to the data parameter of the Plotly object - datasets.push({ - name: label, - theta: thetaData, - r: rData, - text: hoverText, - hoverinfo: 'text', - type: 'scatterpolar', - mode: 'lines', - line: { - shape: 'spline', - width: 2, - color: getGraphColor(colorID, DataType.Meter) - } - }); - } - } - } - } - - let layout: Partial; - // TODO See 3D code for functions that can be used for layout and notices. - if (datasets.length === 0) { - // There are no meters so tell user. - // Customize the layout of the plot - // See https://community.plotly.com/t/replacing-an-empty-graph-with-a-message/31497 for showing text not plot. - layout = { - 'xaxis': { - 'visible': false - }, - 'yaxis': { - 'visible': false - }, - 'annotations': [ - { - 'text': `${translate('select.meter.group')}`, - 'xref': 'paper', - 'yref': 'paper', - 'showarrow': false, - 'font': { - 'size': 28 - } - } - ] - } - } else { - // Plotly scatterpolar plots have the unfortunate attribute that if a smaller number of plotting - // points is done first then that impacts the labeling of the polar coordinate where you can get - // duplicated labels and the points on the separate lines are separated. It is unclear if this is - // intentional or a bug that will go away. To deal with this, the lines are ordered by size. - // Descending (reverse) sort datasets by size of readings. Use r but theta should be the same. - datasets.sort((a, b) => { - return b.r.length - a.r.length; - }); - if (datasets[0].r.length === 0) { - // The longest line (first one) has no data so there is no data in any of the lines. - // Customize the layout of the plot - // See https://community.plotly.com/t/replacing-an-empty-graph-with-a-message/31497 for showing text not plot. - // There is no data so tell user - likely due to date range outside where readings. - // Remove plotting data even though none there is an empty r & theta that gives empty graphic. - datasets.splice(0, datasets.length); - layout = { - 'xaxis': { - 'visible': false - }, - 'yaxis': { - 'visible': false - }, - 'annotations': [ - { - 'text': `${translate('radar.no.data')}`, - 'xref': 'paper', - 'yref': 'paper', - 'showarrow': false, - 'font': { - 'size': 28 - } - } - ] - } - } else { - // Check if all the values for the dates are compatible. Plotly does not like having different dates in different - // scatterpolar lines. Lots of attempts to get this to work failed so not going to allow since not that common. - // Compare the dates (theta) for line with the max points (index 0) to see if it has all the points in all other lines. - let ok = true; - for (let i = 1; i < datasets.length; i++) { - // Current line to consider. - const currentLine: string[] = datasets[i].theta; - // See if all points in current line are in max length line. && means get false if any false. - ok = ok && currentLine.every(v => datasets[0].theta.includes(v)); - } - if (!ok) { - // Not all points align on all lines so inform user. - // Remove plotting data. - datasets.splice(0, datasets.length); - // The lines are not compatible so tell user. - layout = { - 'xaxis': { - 'visible': false - }, - 'yaxis': { - 'visible': false - }, - 'annotations': [ - { - 'text': `${translate('radar.lines.incompatible')}`, - 'xref': 'paper', - 'yref': 'paper', - 'showarrow': false, - 'font': { - 'size': 28 - } - } - ] - } - } else { - // Data available and okay so plot. - // Maximum number of ticks, represents 12 months. Too many is cluttered so this seems good value. - // Plotly shows less if only a few points. - const maxTicks = 12; - layout = { - autosize: true, - showlegend: true, - height: 800, - legend: { - x: 0, - y: 1.1, - orientation: 'h' - }, - polar: { - radialaxis: { - title: unitLabel, - showgrid: true, - gridcolor: '#ddd' - }, - angularaxis: { - // TODO Attempts to format the dates to remove the time did not work with plotly - // choosing the tick values which is desirable. Also want time if limited time range. - direction: 'clockwise', - showgrid: true, - gridcolor: '#ddd', - nticks: maxTicks - } - }, - margin: { - t: 10, - b: -20 - } - }; - } - } - } - - // props.config.locale = state.options.selectedLanguage; - return ( -
- console.log(e)} - style={{ width: '100%', height: '80%' }} - useResizeHandler={true} - config={{ - displayModeBar: true, - responsive: true, - locales: Locales // makes locales available for use - }} - layout={layout} - /> -
- ) -} \ No newline at end of file diff --git a/src/client/app/containers/RadarChartContainer.ts b/src/client/app/containers/RadarChartContainer.ts index 7d1376245..b61e04157 100644 --- a/src/client/app/containers/RadarChartContainer.ts +++ b/src/client/app/containers/RadarChartContainer.ts @@ -2,6 +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/. */ +// TODO Marked For Deletion after RTK migration solidified +/* 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 * as _ from 'lodash'; import * as moment from 'moment'; import { connect } from 'react-redux'; diff --git a/src/client/app/containers/UnsavedWarningContainer.ts b/src/client/app/containers/UnsavedWarningContainer.ts deleted file mode 100644 index 67bb21d30..000000000 --- a/src/client/app/containers/UnsavedWarningContainer.ts +++ /dev/null @@ -1,27 +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 UnsavedWarningComponent from '../components/UnsavedWarningComponent'; -import { State } from '../types/redux/state'; -import { Dispatch } from '../types/redux/actions'; -import { unsavedWarningSlice } from '../reducers/unsavedWarning'; - -function mapStateToProps(state: State) { - return { - hasUnsavedChanges: state.unsavedWarning.hasUnsavedChanges, - isLogOutClicked: state.unsavedWarning.isLogOutClicked, - removeFunction: state.unsavedWarning.removeFunction, - submitFunction: state.unsavedWarning.submitFunction - } -} - -function mapDispatchToProps(dispatch: Dispatch) { - return { - removeUnsavedChanges: () => dispatch(unsavedWarningSlice.actions.removeUnsavedChanges()), - flipLogOutState: () => dispatch(unsavedWarningSlice.actions.flipLogOutState()) - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(UnsavedWarningComponent); diff --git a/src/client/app/containers/admin/CreateUserContainer.tsx b/src/client/app/containers/admin/CreateUserContainer.tsx deleted file mode 100644 index 6c26ef5dc..000000000 --- a/src/client/app/containers/admin/CreateUserContainer.tsx +++ /dev/null @@ -1,78 +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 * as React from 'react'; -import CreateUserComponent from '../../components/admin/CreateUserComponent'; -import { UserRole } from '../../types/items'; -import { usersApi } from '../../utils/api'; -import { browserHistory } from '../../utils/history'; -import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; -import translate from '../../utils/translate'; - -export default class CreateUserFormContainer extends React.Component<{}>{ - constructor(props: {}) { - super(props); - this.handleEmailChange = this.handleEmailChange.bind(this); - this.handlePasswordChange = this.handlePasswordChange.bind(this); - this.handleConfirmPasswordChange = this.handleConfirmPasswordChange.bind(this); - this.handleRoleChange = this.handleRoleChange.bind(this); - this.submitNewUser = this.submitNewUser.bind(this); - } - - state = { - email: '', - password: '', - confirmPassword: '', - role: UserRole.ADMIN, - submittedOnce: false - } - - private handleEmailChange = (newEmail: string) => { - this.setState({ email: newEmail }) - } - private handlePasswordChange = (newPassword: string) => { - this.setState({ password: newPassword }) - } - private handleConfirmPasswordChange = (newConfirmPassword: string) => { - this.setState({ confirmPassword: newConfirmPassword }) - } - private handleRoleChange = (newRole: UserRole) => { - this.setState({ role: newRole }) - } - private submitNewUser = async () => { - this.setState({ submittedOnce: true }) - if (this.state.password === this.state.confirmPassword) { - try { - await usersApi.createUser({ - email: this.state.email, - password: this.state.password, - role: this.state.role - }); - showSuccessNotification(translate('users.successfully.create.user')) - browserHistory.push('/users'); - } catch (error) { - showErrorNotification(translate('users.failed.to.create.user') + ' with error: ' + error.response.data.message); - } - } - } - public render() { - return ( -
- -
- ) - } -} \ No newline at end of file diff --git a/src/client/app/containers/admin/PreferencesContainer.ts b/src/client/app/containers/admin/PreferencesContainer.ts deleted file mode 100644 index 0035babdd..000000000 --- a/src/client/app/containers/admin/PreferencesContainer.ts +++ /dev/null @@ -1,90 +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 PreferencesComponent from '../../components/admin/PreferencesComponent'; -import { submitPreferencesIfNeeded } from '../../actions/admin'; -import { State } from '../../types/redux/state'; -import { Dispatch } from '../../types/redux/actions'; -import { ChartTypes } from '../../types/redux/graph'; -import { LanguageTypes } from '../../types/redux/i18n'; -import { AreaUnitType } from '../../utils/getAreaUnitConversion'; -import { adminSlice } from '../../reducers/admin'; - -function mapStateToProps(state: State) { - return { - displayTitle: state.admin.displayTitle, - defaultChartToRender: state.admin.defaultChartToRender, - defaultTimeZone: state.admin.defaultTimezone, - defaultBarStacking: state.admin.defaultBarStacking, - defaultLanguage: state.admin.defaultLanguage, - disableSubmitPreferences: state.admin.submitted, - defaultWarningFileSize: state.admin.defaultWarningFileSize, - defaultFileSizeLimit: state.admin.defaultFileSizeLimit, - defaultAreaNormalization: state.admin.defaultAreaNormalization, - defaultAreaUnit: state.admin.defaultAreaUnit, - defaultMeterReadingFrequency: state.admin.defaultMeterReadingFrequency, - defaultMeterMinimumValue: state.admin.defaultMeterMinimumValue, - defaultMeterMaximumValue: state.admin.defaultMeterMaximumValue, - defaultMeterMinimumDate: state.admin.defaultMeterMinimumDate, - defaultMeterMaximumDate: state.admin.defaultMeterMaximumDate, - defaultMeterReadingGap: state.admin.defaultMeterReadingGap, - defaultMeterMaximumErrors: state.admin.defaultMeterMaximumErrors, - defaultMeterDisableChecks: state.admin.defaultMeterDisableChecks, - defaultHelpUrl: state.admin.defaultHelpUrl - }; -} - -function mapDispatchToProps(dispatch: Dispatch) { - return { - updateDisplayTitle: (displayTitle: string) => dispatch(adminSlice.actions.updateDisplayTitle(displayTitle)), - - updateDefaultChartType: (defaultChartToRender: ChartTypes) => dispatch(adminSlice.actions.updateDefaultChartToRender(defaultChartToRender)), - - updateDefaultTimeZone: (timeZone: string) => dispatch(adminSlice.actions.updateDefaultTimeZone(timeZone)), - - toggleDefaultBarStacking: () => dispatch(adminSlice.actions.toggleDefaultBarStacking()), - - updateDefaultLanguage: (defaultLanguage: LanguageTypes) => dispatch(adminSlice.actions.updateDefaultLanguage(defaultLanguage)), - - submitPreferences: () => dispatch(submitPreferencesIfNeeded()), - - updateDefaultWarningFileSize: (defaultWarningFileSize: number) => dispatch(adminSlice.actions.updateDefaultWarningFileSize(defaultWarningFileSize)), - - updateDefaultFileSizeLimit: (defaultFileSizeLimit: number) => dispatch(adminSlice.actions.updateDefaultFileSizeLimit(defaultFileSizeLimit)), - - toggleDefaultAreaNormalization: () => dispatch(adminSlice.actions.toggleDefaultAreaNormalization()), - - updateDefaultAreaUnit: (defaultAreaUnit: AreaUnitType) => dispatch(adminSlice.actions.updateDefaultAreaUnit(defaultAreaUnit)), - - updateDefaultMeterReadingFrequency: (defaultMeterReadingFrequency: string) => - dispatch(adminSlice.actions.updateDefaultMeterReadingFrequency(defaultMeterReadingFrequency)), - - updateDefaultMeterMinimumValue: (defaultMeterMinimumValue: number) => - dispatch(adminSlice.actions.updateDefaultMeterMinimumValue(defaultMeterMinimumValue)), - - updateDefaultMeterMaximumValue: (defaultMeterMaximumValue: number) => - dispatch(adminSlice.actions.updateDefaultMeterMaximumValue(defaultMeterMaximumValue)), - - updateDefaultMeterMinimumDate: (defaultMeterMinimumDate: string) => - dispatch(adminSlice.actions.updateDefaultMeterMinimumDate(defaultMeterMinimumDate)), - - updateDefaultMeterMaximumDate: (defaultMeterMaximumDate: string) => - dispatch(adminSlice.actions.updateDefaultMeterMaximumDate(defaultMeterMaximumDate)), - - updateDefaultMeterReadingGap: (defaultMeterReadingGap: number) => - dispatch(adminSlice.actions.updateDefaultMeterReadingGap(defaultMeterReadingGap)), - - updateDefaultMeterMaximumErrors: (defaultMeterMaximumErrors: number) => - dispatch(adminSlice.actions.updateDefaultMeterMaximumErrors(defaultMeterMaximumErrors)), - - updateDefaultMeterDisableChecks: (defaultMeterDisableChecks: boolean) => - dispatch(adminSlice.actions.updateDefaultMeterDisableChecks(defaultMeterDisableChecks)), - - updateDefaultHelpUrl: (defaultHelpUrl: string) => - dispatch(adminSlice.actions.updateDefaultHelpUrl(defaultHelpUrl)) - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(PreferencesComponent); diff --git a/src/client/app/containers/admin/UsersDetailContainer.tsx b/src/client/app/containers/admin/UsersDetailContainer.tsx deleted file mode 100644 index c8deef654..000000000 --- a/src/client/app/containers/admin/UsersDetailContainer.tsx +++ /dev/null @@ -1,93 +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 * as _ from 'lodash'; -import * as React from 'react'; -import UserDetailComponent from '../../components/admin/UsersDetailComponent'; -import { User, UserRole } from '../../types/items'; -import { usersApi } from '../../utils/api'; -import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; -import translate from '../../utils/translate'; - -interface UsersDisplayContainerProps { - fetchUsers: () => User[]; -} - -interface UsersDisplayContainerState { - users: User[], - history: User[][] -} - -export default class UsersDetailContainer extends React.Component { - constructor(props: UsersDisplayContainerProps) { - super(props); - this.deleteUser = this.deleteUser.bind(this); - this.editUser = this.editUser.bind(this); - this.fetchUsers = this.fetchUsers.bind(this); - this.submitUserEdits = this.submitUserEdits.bind(this); - } - - state: UsersDisplayContainerState = { - users: [], - history: [] - } - - async componentDidMount() { - const users = await this.fetchUsers(); - this.setState({ users, history: [_.cloneDeep(users)] }); - } - - private async fetchUsers() { - return await usersApi.getUsers(); - } - - private editUser(email: string, newRole: UserRole) { - const newUsers = _.cloneDeep(this.state.users); - const targetUser = newUsers.find(user => user.email === email); - if (targetUser !== undefined) { - targetUser.role = newRole; - this.setState(prevState => ({ - users: newUsers, - history: [...prevState.history, newUsers] - })); - } - } - - private async submitUserEdits() { - try { - await usersApi.editUsers(this.state.users); - showSuccessNotification(translate('users.successfully.edit.users')); - this.setState(currentState => ({ - history: [_.cloneDeep(currentState.users)] - })); - } catch (error) { - showErrorNotification(translate('users.failed.to.edit.users')); - } - } - - private async deleteUser(email: string) { - try { - await usersApi.deleteUser(email); - const users = await this.fetchUsers(); - this.setState({ users }); - showSuccessNotification(translate('users.successfully.delete.user')); - } catch (error) { - showErrorNotification(translate('users.failed.to.delete.user')); - } - } - - public render() { - return ( -
- -
- ) - } -} \ No newline at end of file diff --git a/src/client/app/containers/maps/MapCalibrationChartDisplayContainer.ts b/src/client/app/containers/maps/MapCalibrationChartDisplayContainer.ts index 80bd93ebe..7ee65d64c 100644 --- a/src/client/app/containers/maps/MapCalibrationChartDisplayContainer.ts +++ b/src/client/app/containers/maps/MapCalibrationChartDisplayContainer.ts @@ -7,7 +7,7 @@ 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 '../../actions/map'; +import { updateCurrentCartesian } from '../../redux/actions/map'; import { store } from '../../store'; import { CalibrationSettings } from '../../types/redux/map'; import Locales from '../../types/locales' diff --git a/src/client/app/containers/maps/MapCalibrationInfoDisplayContainer.ts b/src/client/app/containers/maps/MapCalibrationInfoDisplayContainer.ts index 858e9ecde..94e27983c 100644 --- a/src/client/app/containers/maps/MapCalibrationInfoDisplayContainer.ts +++ b/src/client/app/containers/maps/MapCalibrationInfoDisplayContainer.ts @@ -6,9 +6,9 @@ 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 '../../actions/map'; +import {changeGridDisplay, dropCalibration, offerCurrentGPS, submitCalibratingMap} from '../../redux/actions/map'; import {GPSPoint} from '../../utils/calibration'; -import {logToServer} from '../../actions/logs'; +import {logToServer} from '../../redux/actions/logs'; import translate from '../../utils/translate'; function mapStateToProps(state: State) { diff --git a/src/client/app/containers/maps/MapCalibrationInitiateContainer.ts b/src/client/app/containers/maps/MapCalibrationInitiateContainer.ts index 4613d0e41..400da9e45 100644 --- a/src/client/app/containers/maps/MapCalibrationInitiateContainer.ts +++ b/src/client/app/containers/maps/MapCalibrationInitiateContainer.ts @@ -5,7 +5,7 @@ import MapCalibrationInitiateComponent from '../../components/maps/MapCalibrationInitiateComponent'; import { connect } from 'react-redux'; import { Dispatch } from '../../types/redux/actions'; -import {updateMapMode, updateMapSource} from '../../actions/map'; +import {updateMapMode, updateMapSource} from '../../redux/actions/map'; import {CalibrationModeTypes, MapMetadata} from '../../types/redux/map'; import {State} from '../../types/redux/state'; diff --git a/src/client/app/containers/maps/MapViewContainer.tsx b/src/client/app/containers/maps/MapViewContainer.tsx index ae52da5d5..7fdecbac8 100644 --- a/src/client/app/containers/maps/MapViewContainer.tsx +++ b/src/client/app/containers/maps/MapViewContainer.tsx @@ -7,7 +7,7 @@ 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 '../../actions/map'; +import {editMapDetails, removeMap, setCalibration} from '../../redux/actions/map'; function mapStateToProps(state: State, ownProps: {id: number}) { let map = state.maps.byMapID[ownProps.id]; diff --git a/src/client/app/containers/maps/MapsDetailContainer.tsx b/src/client/app/containers/maps/MapsDetailContainer.tsx index 2a1be0505..011a82e59 100644 --- a/src/client/app/containers/maps/MapsDetailContainer.tsx +++ b/src/client/app/containers/maps/MapsDetailContainer.tsx @@ -5,7 +5,7 @@ import { connect } from 'react-redux'; import { State } from '../../types/redux/state'; import {Dispatch} from '../../types/redux/actions'; -import {fetchMapsDetails, setNewMap, submitEditedMaps} from '../../actions/map'; +import {fetchMapsDetails, setNewMap, submitEditedMaps} from '../../redux/actions/map'; import MapsDetailComponent from '../../components/maps/MapsDetailComponent'; function mapStateToProps(state: State) { diff --git a/src/client/app/index.tsx b/src/client/app/index.tsx index a800cee28..3a6a767cb 100644 --- a/src/client/app/index.tsx +++ b/src/client/app/index.tsx @@ -8,7 +8,7 @@ import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux'; import { store } from './store'; import RouteComponent from './components/RouteComponent'; -import { initApp } from './reducers/appStateSlice'; +import { initApp } from './redux/slices/appStateSlice'; import './styles/index.css'; store.dispatch(initApp()) diff --git a/src/client/app/initScript.ts b/src/client/app/initScript.ts deleted file mode 100644 index 044353a61..000000000 --- a/src/client/app/initScript.ts +++ /dev/null @@ -1,54 +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 { appStateSlice } from './reducers/appStateSlice'; -import { currentUserSlice } from './reducers/currentUser'; -import { authApi } from './redux/api/authApi'; -import { conversionsApi } from './redux/api/conversionsApi'; -import { groupsApi } from './redux/api/groupsApi'; -import { metersApi } from './redux/api/metersApi'; -import { preferencesApi } from './redux/api/preferencesApi'; -import { unitsApi } from './redux/api/unitsApi'; -import { userApi } from './redux/api/userApi'; -import { versionApi } from './redux/api/versionApi'; -import { store } from './store'; -import { deleteToken, getToken, hasToken } from './utils/token'; - - -// Method initiates many data fetching calls on startup before react begins to render -export const initializeApp = async () => { - // 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. - store.dispatch(versionApi.endpoints.getVersion.initiate()) - store.dispatch(preferencesApi.endpoints.getPreferences.initiate()) - store.dispatch(unitsApi.endpoints.getUnitsDetails.initiate()) - store.dispatch(conversionsApi.endpoints.getConversionsDetails.initiate()) - store.dispatch(conversionsApi.endpoints.getConversionArray.initiate()) - - // If user is an admin, they receive additional meter details. - // To avoid sending duplicate requests upon startup, verify user then fetch - if (hasToken()) { - // User has a session token verify before requesting meter/group details - try { - await store.dispatch(authApi.endpoints.verifyToken.initiate(getToken())) - // Token is valid if not errored out by this point, - // Apis will now use the token in headers via baseAPI's Prepare Headers - store.dispatch(currentUserSlice.actions.setUserToken(getToken())) - // Get userDetails with verified token in headers - await store.dispatch(userApi.endpoints.getUserDetails.initiate(undefined, { subscribe: false })) - .unwrap() - .catch(e => { throw (e) }) - - } catch { - // User had a token that isn't valid or getUserDetails threw an error. - // Assume token is invalid. Delete if any - deleteToken() - } - - } - // Request meter/group/details post-auth - store.dispatch(metersApi.endpoints.getMeters.initiate()) - store.dispatch(groupsApi.endpoints.getGroups.initiate()) - store.dispatch(appStateSlice.actions.setInitComplete(true)) -} diff --git a/src/client/app/reducers/barReadings.ts b/src/client/app/reducers/barReadings.ts deleted file mode 100644 index 6ab5f0437..000000000 --- a/src/client/app/reducers/barReadings.ts +++ /dev/null @@ -1,137 +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 { BarReadingsAction, BarReadingsState } from '../types/redux/barReadings'; -import { ActionType } from '../types/redux/actions'; - -const defaultState: BarReadingsState = { - byMeterID: {}, - byGroupID: {}, - isFetching: false, - metersFetching: false, - groupsFetching: false -}; - -export default function readings(state = defaultState, action: BarReadingsAction) { - switch (action.type) { - case ActionType.RequestMeterBarReadings: { - const timeInterval = action.timeInterval.toString(); - const barDuration = action.barDuration.toISOString(); - const unitID = action.unitID; - const newState = { - ...state, - byMeterID: { - ...state.byMeterID - }, - metersFetching: true, - isFetching: true - }; - - for (const meterID of action.meterIDs) { - // Create group entry and time interval entry if needed - if (newState.byMeterID[meterID] === undefined) { - newState.byMeterID[meterID] = {}; - } - if (newState.byMeterID[meterID][timeInterval] === undefined) { - newState.byMeterID[meterID][timeInterval] = {}; - } - if (newState.byMeterID[meterID][timeInterval][barDuration] === undefined) { - newState.byMeterID[meterID][timeInterval][barDuration] = {}; - } - - // Retain existing data if there is any - if (newState.byMeterID[meterID][timeInterval][barDuration][unitID] === undefined) { - newState.byMeterID[meterID][timeInterval][barDuration][unitID] = { isFetching: true }; - } else { - newState.byMeterID[meterID][timeInterval][barDuration][unitID] = - { ...newState.byMeterID[meterID][timeInterval][barDuration][unitID], isFetching: true }; - } - } - - return newState; - } - case ActionType.RequestGroupBarReadings: { - const timeInterval = action.timeInterval.toString(); - const barDuration = action.barDuration.toISOString(); - const unitID = action.unitID; - const newState = { - ...state, - byGroupID: { - ...state.byGroupID - }, - groupsFetching: true, - isFetching: true - }; - - for (const groupID of action.groupIDs) { - // Create group entry and time interval entry if needed - if (newState.byGroupID[groupID] === undefined) { - newState.byGroupID[groupID] = {}; - } - if (newState.byGroupID[groupID][timeInterval] === undefined) { - newState.byGroupID[groupID][timeInterval] = {}; - } - if (newState.byGroupID[groupID][timeInterval][barDuration] === undefined) { - newState.byGroupID[groupID][timeInterval][barDuration] = {}; - } - - // Retain existing data if there is any - if (newState.byGroupID[groupID][timeInterval][barDuration][unitID] === undefined) { - newState.byGroupID[groupID][timeInterval][barDuration][unitID] = { isFetching: true }; - } else { - newState.byGroupID[groupID][timeInterval][barDuration][unitID] = - { ...newState.byGroupID[groupID][timeInterval][barDuration][unitID], isFetching: true }; - } - } - - return newState; - } - case ActionType.ReceiveMeterBarReadings: { - const timeInterval = action.timeInterval.toString(); - const barDuration = action.barDuration.toISOString(); - const unitID = action.unitID; - const newState = { - ...state, - byMeterID: { - ...state.byMeterID - }, - metersFetching: false - }; - - for (const meterID of action.meterIDs) { - const readingsForMeter = action.readings[meterID]; - newState.byMeterID[meterID][timeInterval][barDuration][unitID] = { isFetching: false, readings: readingsForMeter }; - } - if (!state.groupsFetching) { - newState.isFetching = false; - } - - return newState; - } - case ActionType.ReceiveGroupBarReadings: { - const timeInterval = action.timeInterval.toString(); - const barDuration = action.barDuration.toISOString(); - const unitID = action.unitID; - const newState = { - ...state, - byGroupID: { - ...state.byGroupID - }, - groupsFetching: false - }; - - for (const groupID of action.groupIDs) { - const readingsForGroup = action.readings[groupID]; - newState.byGroupID[groupID][timeInterval][barDuration][unitID] = { isFetching: false, readings: readingsForGroup }; - } - if (!state.metersFetching) { - newState.isFetching = false; - } - - return newState; - } - default: - return state; - } -} diff --git a/src/client/app/reducers/compareReadings.ts b/src/client/app/reducers/compareReadings.ts deleted file mode 100644 index a52766f1e..000000000 --- a/src/client/app/reducers/compareReadings.ts +++ /dev/null @@ -1,137 +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 { CompareReadingsAction, CompareReadingsState } from '../types/redux/compareReadings'; -import { ActionType } from '../types/redux/actions'; - -const defaultState: CompareReadingsState = { - byMeterID: {}, - byGroupID: {}, - isFetching: false, - metersFetching: false, - groupsFetching: false -}; - -export default function readings(state = defaultState, action: CompareReadingsAction) { - switch (action.type) { - case ActionType.RequestMeterCompareReadings: { - const newState = { - ...state, - byMeterID: { - ...state.byMeterID - }, - metersFetching: true, - isFetching: true - }; - const timeInterval = action.timeInterval.toString(); - const compareShift = action.compareShift.toISOString(); - const unitID = action.unitID; - for (const meterID of action.meterIDs) { - // Create group entry and time interval entry if needed - if (newState.byMeterID[meterID] === undefined) { - newState.byMeterID[meterID] = {}; - } - if (newState.byMeterID[meterID][timeInterval] === undefined) { - newState.byMeterID[meterID][timeInterval] = {}; - } - if (newState.byMeterID[meterID][timeInterval][compareShift] === undefined) { - newState.byMeterID[meterID][timeInterval][compareShift] = {}; - } - - // Retain existing data if there is any - if (newState.byMeterID[meterID][timeInterval][compareShift][unitID] === undefined) { - newState.byMeterID[meterID][timeInterval][compareShift][unitID] = { isFetching: true }; - } else { - newState.byMeterID[meterID][timeInterval][compareShift][unitID] = - { ...newState.byMeterID[meterID][timeInterval][compareShift][unitID], isFetching: true }; - } - } - return newState; - } - case ActionType.RequestGroupCompareReadings: { - const newState = { - ...state, - byGroupID: { - ...state.byGroupID - }, - groupsFetching: true, - isFetching: true - }; - const timeInterval = action.timeInterval.toString(); - const compareShift = action.compareShift.toISOString(); - const unitID = action.unitID; - for (const groupID of action.groupIDs) { - // Create group entry and time interval entry if needed - if (newState.byGroupID[groupID] === undefined) { - newState.byGroupID[groupID] = {}; - } - if (newState.byGroupID[groupID][timeInterval] === undefined) { - newState.byGroupID[groupID][timeInterval] = {}; - } - if (newState.byGroupID[groupID][timeInterval][compareShift] === undefined) { - newState.byGroupID[groupID][timeInterval][compareShift] = {}; - } - - // Retain existing data if there is any - if (newState.byGroupID[groupID][timeInterval][compareShift][unitID] === undefined) { - newState.byGroupID[groupID][timeInterval][compareShift][unitID] = { isFetching: true }; - } else { - newState.byGroupID[groupID][timeInterval][compareShift][unitID] = - { ...newState.byGroupID[groupID][timeInterval][compareShift][unitID], isFetching: true }; - } - } - return newState; - } - case ActionType.ReceiveMeterCompareReadings: { - const newState = { - ...state, - byMeterID: { - ...state.byMeterID - }, - metersFetching: false - }; - const timeInterval = action.timeInterval.toString(); - const compareShift = action.compareShift.toISOString(); - const unitID = action.unitID; - for (const meterID of action.meterIDs) { - const readingForMeter = action.readings[meterID]; - newState.byMeterID[meterID][timeInterval][compareShift][unitID] = { - isFetching: false, - curr_use: readingForMeter.curr_use, - prev_use: readingForMeter.prev_use - }; - } - if (!state.groupsFetching) { - newState.isFetching = false; - } - return newState; - } - case ActionType.ReceiveGroupCompareReadings: { - const newState = { - ...state, - byGroupID: { - ...state.byGroupID - }, - groupsFetching: false - }; - const timeInterval = action.timeInterval.toString(); - const compareShift = action.compareShift.toISOString(); - const unitID = action.unitID; - for (const groupID of action.groupIDs) { - const readingForGroup = action.readings[groupID]; - newState.byGroupID[groupID][timeInterval][compareShift][unitID] = { - isFetching: false, - curr_use: readingForGroup.curr_use, - prev_use: readingForGroup.prev_use - }; - } - if (!state.metersFetching) { - newState.isFetching = false; - } - return newState; - } - default: - return state; - } -} diff --git a/src/client/app/reducers/conversions.ts b/src/client/app/reducers/conversions.ts deleted file mode 100644 index 479e484cf..000000000 --- a/src/client/app/reducers/conversions.ts +++ /dev/null @@ -1,82 +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 { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { conversionsApi } from '../redux/api/conversionsApi'; -import * as t from '../types/redux/conversions'; -import { ConversionsState } from '../types/redux/conversions'; - -const defaultState: ConversionsState = { - hasBeenFetchedOnce: false, - isFetching: false, - selectedConversions: [], - submitting: [], - conversions: [] -}; - - -export const conversionsSlice = createSlice({ - name: 'conversions', - initialState: defaultState, - reducers: { - conversionsFetchedOnce: state => { - state.hasBeenFetchedOnce = true; - }, - requestConversionsDetails: state => { - state.isFetching = true; - }, - receiveConversionsDetails: (state, action: PayloadAction) => { - state.isFetching = false; - state.conversions = action.payload; - }, - changeDisplayedConversions: (state, action: PayloadAction) => { - state.selectedConversions = action.payload; - }, - submitEditedConversion: (state, action: PayloadAction) => { - state.submitting.push(action.payload); - }, - confirmEditedConversion: (state, action: PayloadAction) => { - // Overwrite the conversion data at the edited conversion's index with the edited conversion's conversion data - // The passed in id should be correct as it is inherited from the pre-edited conversion - // See EditConversionModalComponent line 134 for details (starts with if(conversionHasChanges)) - const conversions = state.conversions; - const conversionDataIndex = conversions.findIndex(conversionData => ( - conversionData.sourceId === action.payload.sourceId - && - conversionData.destinationId === action.payload.destinationId - )); - conversions[conversionDataIndex] = action.payload; - }, - deleteSubmittedConversion: (state, action: PayloadAction) => { - // Remove the current submitting conversion from the submitting state - const submitting = state.submitting; - // Search the array of ConversionData in submitting for an object with source/destination ids matching that of the action payload - const conversionDataIndex = submitting.findIndex(conversionData => ( - conversionData.sourceId === action.payload.sourceId - && - conversionData.destinationId === action.payload.destinationId - )); - // Remove the object from the submitting array - submitting.splice(conversionDataIndex, 1); - }, - deleteConversion: (state, action: PayloadAction) => { - // Retrieve conversions state - const conversions = state.conversions; - // Search the array of ConversionData in conversions for an object with source/destination ids matching that of the action payload - const conversionDataIndex = conversions.findIndex(conversionData => ( - conversionData.sourceId === action.payload.sourceId - && - conversionData.destinationId === action.payload.destinationId - )); - // Remove the ConversionData from the conversions array - conversions.splice(conversionDataIndex, 1); - } - }, - extraReducers: builder => { - builder.addMatcher(conversionsApi.endpoints.getConversionsDetails.matchFulfilled, - (state, action) => { - state.conversions = action.payload - }) - } -}); \ No newline at end of file diff --git a/src/client/app/reducers/groups.ts b/src/client/app/reducers/groups.ts deleted file mode 100644 index 3f77c219a..000000000 --- a/src/client/app/reducers/groups.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* eslint-disable */ -//@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/. */ - -import { PayloadAction, createSlice } from '@reduxjs/toolkit'; -import * as _ from 'lodash'; -import { groupsApi } from '../redux/api/groupsApi'; -import * as t from '../types/redux/groups'; -import { GroupsState } from '../types/redux/groups'; - -const defaultState: GroupsState = { - byGroupID: {}, - selectedGroups: [] - // TODO groupInEditing: { - // dirty: false - // }, -}; - -export const groupsSlice = createSlice({ - name: 'groups', - initialState: defaultState, - reducers: { - receiveGroupsDetails: (state, action: PayloadAction) => { - const newGroups = action.payload.map(group => ({ - ...group, - isFetching: false, - // Sometimes OED fetches both the details and the child meters/groups as separate actions. Since the order they will happen is - // uncertain, we need to preserve the child meters/groups if they exist. If not, put empty so no issues when accessing in other - // places. Note this may be the wrong values but they should refresh quickly once all actions are done. - childGroups: (state.byGroupID[group.id] && state.byGroupID[group.id].childGroups) ? state.byGroupID[group.id].childGroups : [], - childMeters: (state.byGroupID[group.id] && state.byGroupID[group.id].childMeters) ? state.byGroupID[group.id].childMeters : [], - selectedGroups: [], - selectedMeters: [] - })); - // newGroups is an array: this converts it into a nested object where the key to each group is its ID. - // Without this, byGroupID will not be keyed by group ID. - // TODO FIX TYPES HERE Weird interaction here - state.byGroupID = _.keyBy(newGroups, 'id'); - }, - receiveGroupChildren: (state, action: PayloadAction<{ groupID: number, data: { meters: number[], groups: number[], deepMeters: number[] } }>) => { - state.byGroupID[action.payload.groupID].childGroups = action.payload.data.groups; - state.byGroupID[action.payload.groupID].childMeters = action.payload.data.meters; - state.byGroupID[action.payload.groupID].deepMeters = action.payload.data.deepMeters; - }, - receiveAllGroupsChildren: (state, action: PayloadAction) => { - // For each group that received data, set the children meters and groups. - for (const groupInfo of action.payload) { - // Group id of the current item - const groupId = groupInfo.groupId; - // Reset the newState for this group to have child meters/groups. - state.byGroupID[groupId].childMeters = groupInfo.childMeters; - state.byGroupID[groupId].childGroups = groupInfo.childGroups; - } - } - }, - // TODO Much of this logic is duplicated due to migration trying not to change too much at once. - // When no longer needed remove base reducers if applicable, or delete slice entirely and rely solely on api cache - extraReducers: builder => { - builder.addMatcher(groupsApi.endpoints.getGroups.matchFulfilled, - (state, { payload }) => { state.byGroupID = payload }) - .addMatcher(groupsApi.endpoints.getAllGroupsChildren.matchFulfilled, - (state, action) => { - // For each group that received data, set the children meters and groups. - for (const groupInfo of action.payload) { - // Group id of the current item - const groupId = groupInfo.groupId; - // Reset the newState for this group to have child meters/groups. - state.byGroupID[groupId].childMeters = groupInfo.childMeters; - state.byGroupID[groupId].childGroups = groupInfo.childGroups; - } - }) - - }, - selectors: { - selectGroupState: state => state - } -}) diff --git a/src/client/app/reducers/lineReadings.ts b/src/client/app/reducers/lineReadings.ts deleted file mode 100644 index 775645665..000000000 --- a/src/client/app/reducers/lineReadings.ts +++ /dev/null @@ -1,123 +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 { LineReadingsAction, LineReadingsState } from '../types/redux/lineReadings'; -import { ActionType } from '../types/redux/actions'; - -const defaultState: LineReadingsState = { - byMeterID: {}, - byGroupID: {}, - isFetching: false, - metersFetching: false, - groupsFetching: false -}; - -export default function readings(state = defaultState, action: LineReadingsAction) { - switch (action.type) { - case ActionType.RequestMeterLineReadings: { - const timeInterval = action.timeInterval.toString(); - const unitID = action.unitID; - const newState = { - ...state, - byMeterID: { - ...state.byMeterID - }, - metersFetching: true, - isFetching: true - }; - - for (const meterID of action.meterIDs) { - // Create meter wrapper if needed - if (newState.byMeterID[meterID] === undefined) { - newState.byMeterID[meterID] = {}; - } - if (newState.byMeterID[meterID][timeInterval] === undefined) { - newState.byMeterID[meterID][timeInterval] = {}; - } - - // Preserve existing data - if (newState.byMeterID[meterID][timeInterval][unitID] === undefined) { - newState.byMeterID[meterID][timeInterval][unitID] = { isFetching: true }; - } else { - newState.byMeterID[meterID][timeInterval][unitID] = { ...newState.byMeterID[meterID][timeInterval][unitID], isFetching: true }; - } - } - return newState; - } - case ActionType.RequestGroupLineReadings: { - const timeInterval = action.timeInterval.toString(); - const unitID = action.unitID; - const newState = { - ...state, - byGroupID: { - ...state.byGroupID - }, - groupsFetching: true, - isFetching: true - }; - - for (const groupID of action.groupIDs) { - // Create group wrapper - if (newState.byGroupID[groupID] === undefined) { - newState.byGroupID[groupID] = {}; - } - if (newState.byGroupID[groupID][timeInterval] === undefined) { - newState.byGroupID[groupID][timeInterval] = {}; - } - - // Preserve existing data - if (newState.byGroupID[groupID][timeInterval][unitID] === undefined) { - newState.byGroupID[groupID][timeInterval][unitID] = { isFetching: true }; - } else { - newState.byGroupID[groupID][timeInterval][unitID] = { ...newState.byGroupID[groupID][timeInterval][unitID], isFetching: true }; - } - } - return newState; - } - case ActionType.ReceiveMeterLineReadings: { - const timeInterval = action.timeInterval.toString(); - const unitID = action.unitID; - const newState: LineReadingsState = { - ...state, - byMeterID: { - ...state.byMeterID - }, - metersFetching: false - }; - - for (const meterID of action.meterIDs) { - const readingsForMeter = action.readings[meterID]; - newState.byMeterID[meterID][timeInterval][unitID] = { isFetching: false, readings: readingsForMeter }; - } - if (!state.groupsFetching) { - newState.isFetching = false; - } - - return newState; - } - case ActionType.ReceiveGroupLineReadings: { - const timeInterval = action.timeInterval.toString(); - const unitID = action.unitID; - const newState: LineReadingsState = { - ...state, - byGroupID: { - ...state.byGroupID - }, - groupsFetching: false - }; - - for (const groupID of action.groupIDs) { - const readingsForGroup = action.readings[groupID]; - newState.byGroupID[groupID][timeInterval][unitID] = { isFetching: false, readings: readingsForGroup }; - } - if (!state.metersFetching) { - newState.isFetching = false; - } - - return newState; - } - default: - return state; - } -} diff --git a/src/client/app/reducers/meters.ts b/src/client/app/reducers/meters.ts deleted file mode 100644 index c5ebf4184..000000000 --- a/src/client/app/reducers/meters.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* eslint-disable */ -//@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/. */ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import * as _ from 'lodash'; -import { metersApi } from '../redux/api/metersApi'; -import * as t from '../types/redux/meters'; -import { MetersState } from '../types/redux/meters'; -import { durationFormat } from '../utils/durationFormat'; - -const defaultState: MetersState = { - hasBeenFetchedOnce: false, - isFetching: false, - byMeterID: {}, - submitting: [] -}; - - -export const metersSlice = createSlice({ - name: 'meters', - initialState: defaultState, - reducers: { - confirmMetersFetchedOnce: state => { - state.hasBeenFetchedOnce = true; - }, - requestMetersDetails: state => { - state.isFetching = true; - }, - receiveMetersDetails: (state, action: PayloadAction) => { - state.isFetching = false; - state.byMeterID = _.keyBy(action.payload, meter => meter.id); - }, - submitEditedMeter: (state, action: PayloadAction) => { - state.submitting.push(action.payload); - }, - confirmEditedMeter: (state, action: PayloadAction) => { - action.payload.readingFrequency = durationFormat(action.payload.readingFrequency); - state.byMeterID[action.payload.id] = action.payload; - }, - confirmAddMeter: (state, action: PayloadAction) => { - action.payload.readingFrequency = durationFormat(action.payload.readingFrequency); - state.byMeterID[action.payload.id] = action.payload; - }, - deleteSubmittedMeter: (state, action: PayloadAction) => { - state.submitting.splice(state.submitting.indexOf(action.payload)); - } - }, - extraReducers: builder => { - builder.addMatcher( - metersApi.endpoints.getMeters.matchFulfilled, - (state, { payload }) => { - state.byMeterID = payload - } - ) - }, - selectors: { - selectMeterState: state => state - - } -}); diff --git a/src/client/app/reducers/options.ts b/src/client/app/reducers/options.ts deleted file mode 100644 index a49a3a47d..000000000 --- a/src/client/app/reducers/options.ts +++ /dev/null @@ -1,35 +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 { preferencesApi } from '../redux/api/preferencesApi'; -import { LanguageTypes } from '../types/redux/i18n'; -import { OptionsState } from '../types/redux/options'; -import { createSlice } from '@reduxjs/toolkit' -import type { PayloadAction } from '@reduxjs/toolkit' -import * as moment from 'moment' - -const defaultState: OptionsState = { - selectedLanguage: LanguageTypes.en -}; - -export const optionsSlice = createSlice({ - name: 'options', - initialState: defaultState, - reducers: { - updateSelectedLanguage: (state, action: PayloadAction) => { - state.selectedLanguage = action.payload - } - }, - extraReducers: builder => { - builder.addMatcher(preferencesApi.endpoints.getPreferences.matchFulfilled, (state, action) => { - state.selectedLanguage = action.payload.defaultLanguage - moment.locale(action.payload.defaultLanguage); - }) - }, - selectors: { - selectSelectedLanguage: state => state.selectedLanguage - } -}); - -export const { selectSelectedLanguage } = optionsSlice.selectors \ No newline at end of file diff --git a/src/client/app/reducers/radarReadings.ts b/src/client/app/reducers/radarReadings.ts deleted file mode 100644 index af21e5994..000000000 --- a/src/client/app/reducers/radarReadings.ts +++ /dev/null @@ -1,127 +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 { RadarReadingsAction, RadarReadingsState } from '../types/redux/radarReadings'; -import { ActionType } from '../types/redux/actions'; - -// TODO Since radar is using line values in the end, should it have separate state/actions/reducers? - -const defaultState: RadarReadingsState = { - byMeterID: {}, - byGroupID: {}, - isFetching: false, - metersFetching: false, - groupsFetching: false -}; - -export default function readings(state = defaultState, action: RadarReadingsAction) { - switch (action.type) { - case ActionType.RequestMeterLineReadings: { - const timeInterval = action.timeInterval.toString(); - const unitID = action.unitID; - const newState = { - ...state, - byMeterID: { - ...state.byMeterID - }, - metersFetching: true, - isFetching: true - }; - - for (const meterID of action.meterIDs) { - // Create meter wrapper if needed - if (newState.byMeterID[meterID] === undefined) { - newState.byMeterID[meterID] = {}; - } - if (newState.byMeterID[meterID][timeInterval] === undefined) { - newState.byMeterID[meterID][timeInterval] = {}; - } - - // Preserve existing data - if (newState.byMeterID[meterID][timeInterval][unitID] === undefined) { - newState.byMeterID[meterID][timeInterval][unitID] = { isFetching: true }; - } else { - newState.byMeterID[meterID][timeInterval][unitID] = { ...newState.byMeterID[meterID][timeInterval][unitID], isFetching: true }; - } - } - - return newState; - } - case ActionType.RequestGroupLineReadings: { - const timeInterval = action.timeInterval.toString(); - const unitID = action.unitID; - const newState = { - ...state, - byGroupID: { - ...state.byGroupID - }, - groupsFetching: true, - isFetching: true - }; - - for (const groupID of action.groupIDs) { - // Create group wrapper - if (newState.byGroupID[groupID] === undefined) { - newState.byGroupID[groupID] = {}; - } - if (newState.byGroupID[groupID][timeInterval] === undefined) { - newState.byGroupID[groupID][timeInterval] = {}; - } - - // Preserve existing data - if (newState.byGroupID[groupID][timeInterval][unitID] === undefined) { - newState.byGroupID[groupID][timeInterval][unitID] = { isFetching: true }; - } else { - newState.byGroupID[groupID][timeInterval][unitID] = { ...newState.byGroupID[groupID][timeInterval][unitID], isFetching: true }; - } - } - - return newState; - } - case ActionType.ReceiveMeterLineReadings: { - const timeInterval = action.timeInterval.toString(); - const unitID = action.unitID; - const newState: RadarReadingsState = { - ...state, - byMeterID: { - ...state.byMeterID - }, - metersFetching: false - }; - - for (const meterID of action.meterIDs) { - const readingsForMeter = action.readings[meterID]; - newState.byMeterID[meterID][timeInterval][unitID] = { isFetching: false, readings: readingsForMeter }; - } - if (!state.groupsFetching) { - newState.isFetching = false; - } - - return newState; - } - case ActionType.ReceiveGroupLineReadings: { - const timeInterval = action.timeInterval.toString(); - const unitID = action.unitID; - const newState: RadarReadingsState = { - ...state, - byGroupID: { - ...state.byGroupID - }, - groupsFetching: false - }; - - for (const groupID of action.groupIDs) { - const readingsForGroup = action.readings[groupID]; - newState.byGroupID[groupID][timeInterval][unitID] = { isFetching: false, readings: readingsForGroup }; - } - if (!state.metersFetching) { - newState.isFetching = false; - } - - return newState; - } - default: - return state; - } -} diff --git a/src/client/app/reducers/units.ts b/src/client/app/reducers/units.ts deleted file mode 100644 index 0869b4e90..000000000 --- a/src/client/app/reducers/units.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* eslint-disable */ -//@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/. */ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import * as _ from 'lodash'; -import { unitsApi } from '../redux/api/unitsApi'; -import * as t from '../types/redux/units'; -import { UnitsState } from '../types/redux/units'; - -const defaultState: UnitsState = { - hasBeenFetchedOnce: false, - isFetching: false, - selectedUnits: [], - submitting: [], - units: {} -}; - -export const unitsSlice = createSlice({ - name: 'units', - initialState: defaultState, - reducers: { - confirmUnitsFetchedOnce: state => { - state.hasBeenFetchedOnce = true; - }, - requestUnitsDetails: state => { - state.isFetching = true; - }, - receiveUnitsDetails: (state, action: PayloadAction) => { - state.isFetching = false; - state.units = _.keyBy(action.payload, unit => unit.id); - }, - changeDisplayedUnits: (state, action: PayloadAction) => { - state.selectedUnits = action.payload; - }, - submitEditedUnit: (state, action: PayloadAction) => { - state.submitting.push(action.payload); - }, - confirmEditedUnit: (state, action: PayloadAction) => { - state.units[action.payload.id] = action.payload; - }, - confirmUnitEdits: (state, action: PayloadAction) => { - state.submitting.splice(state.submitting.indexOf(action.payload), 1); - } - }, - extraReducers: builder => { - builder.addMatcher(unitsApi.endpoints.getUnitsDetails.matchFulfilled, - (state, action) => { state.units = action.payload } - ) - }, - selectors: { - selectUnitsState: state => state, - selectUnitDataById: state => state.units - } -}); diff --git a/src/client/app/reducers/unsavedWarning.ts b/src/client/app/reducers/unsavedWarning.ts deleted file mode 100644 index 963e32403..000000000 --- a/src/client/app/reducers/unsavedWarning.ts +++ /dev/null @@ -1,75 +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 { UnsavedWarningState } from '../types/redux/unsavedWarning'; -import { any } from 'prop-types'; -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; - -const defaultState: UnsavedWarningState = { - hasUnsavedChanges: false, - isLogOutClicked: false, - removeFunction: () => any, - submitFunction: () => any -}; - -export const unsavedWarningSlice = createSlice({ - name: 'unsavedWarning', - initialState: defaultState, - reducers: { - // case ActionType.UpdateUnsavedChanges: - // return { - // ...state, - // hasUnsavedChanges: true, - // removeFunction: action.removeFunction, - // submitFunction: action.submitFunction - // } - updateUnsavedChanges: (state, action: PayloadAction<{ removeFunction: any, submitFunction: any }>) => { - state.hasUnsavedChanges = true; - state.removeFunction = action.payload.removeFunction; - state.submitFunction = action.payload.submitFunction; - }, - // case ActionType.RemoveUnsavedChanges: - // return { - // ...state, - // hasUnsavedChanges: false - // } - removeUnsavedChanges: state => { - state.hasUnsavedChanges = false; - }, - // case ActionType.FlipLogOutState: - // return { - // ...state, - // isLogOutClicked: !state.isLogOutClicked - // } - flipLogOutState: state => { - state.isLogOutClicked = !state.isLogOutClicked; - } - - } -} -); - -// export default function unsavedWarning(state = defaultState, action: UnsavedWarningAction) { -// switch (action.type) { -// case ActionType.UpdateUnsavedChanges: -// return { -// ...state, -// hasUnsavedChanges: true, -// removeFunction: action.removeFunction, -// submitFunction: action.submitFunction -// } -// case ActionType.RemoveUnsavedChanges: -// return { -// ...state, -// hasUnsavedChanges: false -// } -// case ActionType.FlipLogOutState: -// return { -// ...state, -// isLogOutClicked: !state.isLogOutClicked -// } -// default: -// return state; -// } -// } \ No newline at end of file diff --git a/src/client/app/actions/admin.ts b/src/client/app/redux/actions/admin.ts similarity index 90% rename from src/client/app/actions/admin.ts rename to src/client/app/redux/actions/admin.ts index 6d399819f..51903d63a 100644 --- a/src/client/app/actions/admin.ts +++ b/src/client/app/redux/actions/admin.ts @@ -2,16 +2,21 @@ * 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 { showErrorNotification, showSuccessNotification } from '../utils/notifications'; -import { Dispatch, GetState, Thunk } from '../types/redux/actions'; -import { State } from '../types/redux/state'; -import { conversionArrayApi, preferencesApi } from '../utils/api'; -import translate from '../utils/translate'; +import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; +import { Dispatch, GetState, Thunk } from '../../types/redux/actions'; +import { State } from '../../types/redux/state'; +import { conversionArrayApi, preferencesApi } from '../../utils/api'; +import translate from '../../utils/translate'; import * as moment from 'moment'; -import { updateSelectedLanguage } from './options'; -import { graphSlice } from '../reducers/graph'; -import { adminSlice } from '../reducers/admin'; - +// import { updateSelectedLanguage } from './options'; +import { graphSlice } from '../slices/graphSlice'; +import { adminSlice } from '../slices/adminSlice'; +// TODO Marked For Deletion after RTK migration solidified +/* 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 */ /** * Dispatches a fetch for admin preferences and sets the state based upon the result */ @@ -35,7 +40,7 @@ function fetchPreferences(): Thunk { } if (preferences.defaultLanguage !== state.options.selectedLanguage) { // if the site default differs from the selected language, update the selected language and the locale - dispatch2(updateSelectedLanguage(preferences.defaultLanguage)); + // dispatch2(updateSelectedLanguage(preferences.defaultLanguage)); moment.locale(preferences.defaultLanguage); } else { // else set moment locale to site default diff --git a/src/client/app/actions/conversions.ts b/src/client/app/redux/actions/conversions.ts similarity index 92% rename from src/client/app/actions/conversions.ts rename to src/client/app/redux/actions/conversions.ts index 15e0cb5f5..d01a4c7cb 100644 --- a/src/client/app/actions/conversions.ts +++ b/src/client/app/redux/actions/conversions.ts @@ -1,12 +1,18 @@ /* 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'; +// TODO Marked For Deletion after RTK migration solidified +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'; diff --git a/src/client/app/actions/currentUser.ts b/src/client/app/redux/actions/currentUser.ts similarity index 74% rename from src/client/app/actions/currentUser.ts rename to src/client/app/redux/actions/currentUser.ts index 7656f6f2a..bc72e4536 100644 --- a/src/client/app/actions/currentUser.ts +++ b/src/client/app/redux/actions/currentUser.ts @@ -2,13 +2,18 @@ * 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 { usersApi, verificationApi } from '../utils/api'; -import { Thunk, Dispatch, GetState } from '../types/redux/actions'; -import { State } from '../types/redux/state'; -import { deleteToken, hasToken } from '../utils/token'; -import { currentUserSlice } from '../reducers/currentUser'; - +import { usersApi, verificationApi } from '../../utils/api'; +import { Thunk, Dispatch, GetState } from '../../types/redux/actions'; +import { State } from '../../types/redux/state'; +import { deleteToken, hasToken } from '../../utils/token'; +import { currentUserSlice } from '../slices/currentUserSlice'; +// TODO Marked For Deletion after RTK migration solidified +/* 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 */ /** * Check if we should fetch the current user's data. This function has the side effect of deleting an invalid token from local storage. * @param state The redux state diff --git a/src/client/app/actions/graph.ts b/src/client/app/redux/actions/graph.ts similarity index 81% rename from src/client/app/actions/graph.ts rename to src/client/app/redux/actions/graph.ts index 641c590c3..760f77de8 100644 --- a/src/client/app/actions/graph.ts +++ b/src/client/app/redux/actions/graph.ts @@ -3,9 +3,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as moment from 'moment'; -import { TimeInterval } from '../../../common/TimeInterval'; -import * as t from '../types/redux/graph'; -import { ComparePeriod, SortingOrder } from '../utils/calculateCompare'; +import { TimeInterval } from '../../../../common/TimeInterval'; +import * as t from '../../types/redux/graph'; +import { ComparePeriod, SortingOrder } from '../../utils/calculateCompare'; export interface LinkOptions { meterIDs?: number[]; diff --git a/src/client/app/actions/groups.ts b/src/client/app/redux/actions/groups.ts similarity index 96% rename from src/client/app/actions/groups.ts rename to src/client/app/redux/actions/groups.ts index 45a2809c6..b86144fad 100644 --- a/src/client/app/actions/groups.ts +++ b/src/client/app/redux/actions/groups.ts @@ -2,6 +2,14 @@ // * 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 Marked For Deletion after RTK migration solidified +/* 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 { Dispatch, GetState, Thunk } from '../types/redux/actions'; // import { State } from '../types/redux/state'; // import { showErrorNotification, showSuccessNotification } from '../utils/notifications'; diff --git a/src/client/app/actions/logs.ts b/src/client/app/redux/actions/logs.ts similarity index 92% rename from src/client/app/actions/logs.ts rename to src/client/app/redux/actions/logs.ts index b168debb3..318becdbe 100644 --- a/src/client/app/actions/logs.ts +++ b/src/client/app/redux/actions/logs.ts @@ -1,8 +1,8 @@ /* 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 { logsApi } from '../utils/api'; +import { LogData } from '../../types/redux/logs'; +import { logsApi } from '../../utils/api'; /** * pass client-side information to server console using logsApi on server-side, mainly for debugging purposes diff --git a/src/client/app/actions/map.ts b/src/client/app/redux/actions/map.ts similarity index 96% rename from src/client/app/actions/map.ts rename to src/client/app/redux/actions/map.ts index 9df0bcb4b..8fb9d33aa 100644 --- a/src/client/app/actions/map.ts +++ b/src/client/app/redux/actions/map.ts @@ -2,9 +2,9 @@ * 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 * as t from '../types/redux/map'; -import {CalibrationModeTypes, MapData, MapMetadata} from '../types/redux/map'; +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, @@ -12,13 +12,13 @@ import { CartesianPoint, Dimensions, GPSPoint -} from '../utils/calibration'; -import {State} from '../types/redux/state'; -import {mapsApi} from '../utils/api'; -import {showErrorNotification, showSuccessNotification} from '../utils/notifications'; -import translate from '../utils/translate'; +} from '../../utils/calibration'; +import {State} from '../../types/redux/state'; +import {mapsApi} from '../../utils/api'; +import {showErrorNotification, showSuccessNotification} from '../../utils/notifications'; +import translate from '../../utils/translate'; import * as moment from 'moment'; -import {browserHistory} from '../utils/history'; +import {browserHistory} from '../../utils/history'; import {logToServer} from './logs'; function requestMapsDetails(): t.RequestMapsDetailsAction { diff --git a/src/client/app/actions/meters.ts b/src/client/app/redux/actions/meters.ts similarity index 87% rename from src/client/app/actions/meters.ts rename to src/client/app/redux/actions/meters.ts index 7497997fb..e68f47ef5 100644 --- a/src/client/app/actions/meters.ts +++ b/src/client/app/redux/actions/meters.ts @@ -1,15 +1,20 @@ -/* eslint-disable */ -//@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/. */ -import { Thunk, Dispatch, GetState } from '../types/redux/actions'; -import { showSuccessNotification } from '../utils/notifications'; -import translate from '../utils/translate'; -import * as t from '../types/redux/meters'; -import { metersApi } from '../utils/api'; + +// TODO Marked For Deletion after RTK migration solidified +/* 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 } from '../../utils/notifications'; +import translate from '../../utils/translate'; +import * as t from '../../types/redux/meters'; +import { metersApi } from '../../utils/api'; import { updateCikAndDBViewsIfNeeded } from './admin'; diff --git a/src/client/app/actions/units.ts b/src/client/app/redux/actions/units.ts similarity index 82% rename from src/client/app/actions/units.ts rename to src/client/app/redux/actions/units.ts index 28595c871..8db8aa941 100644 --- a/src/client/app/actions/units.ts +++ b/src/client/app/redux/actions/units.ts @@ -1,12 +1,19 @@ /* 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/. */ + * 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 { Thunk, Dispatch, GetState } from '../types/redux/actions'; -import { showSuccessNotification, showErrorNotification } from '../utils/notifications'; -import translate from '../utils/translate'; -import * as t from '../types/redux/units'; -import { unitsApi } from '../utils/api'; +// TODO Marked For Deletion after RTK migration solidified +/* 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/units'; +import { unitsApi } from '../../utils/api'; import { updateCikAndDBViewsIfNeeded } from './admin'; import { unitsSlice } from '../reducers/units'; diff --git a/src/client/app/redux/api/authApi.ts b/src/client/app/redux/api/authApi.ts index 26f7b366c..12c102844 100644 --- a/src/client/app/redux/api/authApi.ts +++ b/src/client/app/redux/api/authApi.ts @@ -1,4 +1,4 @@ -import { currentUserSlice } from '../../reducers/currentUser'; +import { currentUserSlice } from '../slices/currentUserSlice'; import { User } from '../../types/items'; import { deleteToken } from '../../utils/token'; import { baseApi } from './baseApi'; diff --git a/src/client/app/redux/api/baseApi.ts b/src/client/app/redux/api/baseApi.ts index 8f7cb63a4..3b9ea0615 100644 --- a/src/client/app/redux/api/baseApi.ts +++ b/src/client/app/redux/api/baseApi.ts @@ -28,7 +28,8 @@ export const baseApi = createApi({ 'GroupChildrenData', 'Preferences', 'Users', - 'ConversionDetails' + 'ConversionDetails', + 'Units' ], // Initially no defined endpoints, Use rtk query's injectEndpoints endpoints: () => ({}) diff --git a/src/client/app/redux/api/conversionsApi.ts b/src/client/app/redux/api/conversionsApi.ts index 1a8793af2..4bd2bc318 100644 --- a/src/client/app/redux/api/conversionsApi.ts +++ b/src/client/app/redux/api/conversionsApi.ts @@ -9,7 +9,9 @@ export const conversionsApi = baseApi.injectEndpoints({ providesTags: ['ConversionDetails'] }), getConversionArray: builder.query({ - query: () => 'api/conversion-array' + query: () => 'api/conversion-array', + providesTags: ['ConversionDetails'] + }), addConversion: builder.mutation({ query: conversion => ({ @@ -18,8 +20,6 @@ export const conversionsApi = baseApi.injectEndpoints({ body: conversion }), onQueryStarted: async (_arg, api) => { - // TODO write more robust logic for error handling, and manually invalidate tags instead? - // TODO Verify Behavior w/ Maintainers api.queryFulfilled .then(() => { api.dispatch( @@ -72,13 +72,10 @@ export const conversionsApi = baseApi.injectEndpoints({ } }), refresh: builder.mutation({ - query: args => ({ + query: ({ redoCik, refreshReadingViews }) => ({ url: 'api/conversion-array/refresh', method: 'POST', - body: { - redoCik: args.redoCik, - refreshReadingViews: args.refreshReadingViews - } + body: { redoCik, refreshReadingViews } }), // TODO check behavior with maintainers, always invalidates, should be conditional? invalidatesTags: ['ConversionDetails'] diff --git a/src/client/app/redux/api/groupsApi.ts b/src/client/app/redux/api/groupsApi.ts index 12179fed9..f547e6ac1 100644 --- a/src/client/app/redux/api/groupsApi.ts +++ b/src/client/app/redux/api/groupsApi.ts @@ -1,8 +1,6 @@ -import { EntityState, createEntityAdapter } from '@reduxjs/toolkit'; +import { EntityState, Update, createEntityAdapter } from '@reduxjs/toolkit'; import * as _ from 'lodash'; -import { CompareReadings } from 'types/readings'; -import { TimeInterval } from '../../../../common/TimeInterval'; -import { selectIsAdmin } from '../../reducers/currentUser'; +import { selectIsAdmin } from '../slices/currentUserSlice'; import { RootState } from '../../store'; import { GroupChildren, GroupData } from '../../types/redux/groups'; import { baseApi } from './baseApi'; @@ -32,9 +30,9 @@ export const groupsApi = baseApi.injectEndpoints({ const state = getState() as RootState // if user is an admin, automatically fetch allGroupChildren and update the if (selectIsAdmin(state)) { - const { data = [] } = await dispatch(groupsApi.endpoints.getAllGroupsChildren.initiate()) + const { data = [] } = await dispatch(groupsApi.endpoints.getAllGroupsChildren.initiate(undefined, { subscribe: false })) // Map the data to the format needed for updateMany - const updates = data.map(childrenInfo => ({ + const updates: Update[] = data.map(childrenInfo => ({ id: childrenInfo.groupId, changes: { childMeters: childrenInfo.childMeters, @@ -79,27 +77,7 @@ export const groupsApi = baseApi.injectEndpoints({ }), getParentIDs: builder.query({ query: groupId => `api/groups/parents/${groupId}` - }), - /** - * Gets compare readings for groups for the given current time range and a shift for previous time range - * @param groupIDs The group IDs to get readings for - * @param timeInterval start and end of current/this compare period - * @param shift how far to shift back in time from current period to previous period - * @param unitID The unit id that the reading should be returned in, i.e., the graphic unit - * @returns CompareReadings in sorted order - */ - getCompareReadingsForGroups: - builder.query({ - query: ({ groupIDs, timeInterval, shift, unitID }) => ({ - url: `/api/compareReadings/groups/${groupIDs.join(',')}`, - params: { - curr_start: timeInterval.getStartTimestamp().toISOString(), - curr_end: timeInterval.getEndTimestamp().toISOString(), - shift: shift.toISOString(), - graphicUnitId: unitID.toString() - } - }) - }) + }) }) }) diff --git a/src/client/app/redux/api/readingsApi.ts b/src/client/app/redux/api/readingsApi.ts index e81b829e0..e9463dd30 100644 --- a/src/client/app/redux/api/readingsApi.ts +++ b/src/client/app/redux/api/readingsApi.ts @@ -13,7 +13,7 @@ import { baseApi } from './baseApi'; export const readingsApi = baseApi.injectEndpoints({ endpoints: builder => ({ threeD: builder.query({ - // ThreeD request only single meters at a time which plays well with default cache behavior + // ThreeD requests only single meters at a time which plays well with default cache behavior // No other properties are necessary for this endpoint // Refer to the line endpoint for an example of an endpoint with custom cache behavior query: ({ id, timeInterval, graphicUnitId, readingInterval, meterOrGroup }) => ({ @@ -110,14 +110,6 @@ export const readingsApi = baseApi.injectEndpoints({ return error ? { error } : { data: data as BarReadings } } }), - /** - * Gets compare readings for meters for the given current time range and a shift for previous time range - * @param meterIDs The meter IDs to get readings for - * @param timeInterval start and end of current/this compare period - * @param shift how far to shift back in time from current period to previous period - * @param unitID The unit id that the reading should be returned in, i.e., the graphic unit - * @returns CompareReadings in sorted order - */ compare: builder.query({ serializeQueryArgs: ({ queryArgs }) => _.omit(queryArgs, 'ids'), merge: (currentCacheData, responseData) => { Object.assign(currentCacheData, responseData) }, diff --git a/src/client/app/redux/api/unitsApi.ts b/src/client/app/redux/api/unitsApi.ts index 77db0ce73..f379a2f37 100644 --- a/src/client/app/redux/api/unitsApi.ts +++ b/src/client/app/redux/api/unitsApi.ts @@ -2,6 +2,7 @@ import { EntityState, createEntityAdapter } from '@reduxjs/toolkit'; import { RootState } from 'store'; import { UnitData } from '../../types/redux/units'; import { baseApi } from './baseApi'; +import { conversionsApi } from './conversionsApi'; export const unitsAdapter = createEntityAdapter({ sortComparer: (unitA, unitB) => unitA.identifier?.localeCompare(unitB.identifier, undefined, { sensitivity: 'accent' }) }); @@ -14,36 +15,48 @@ export const unitsApi = baseApi.injectEndpoints({ query: () => 'api/units', transformResponse: (response: UnitData[]) => { return unitsAdapter.setAll(unitsInitialState, response) - } + }, + providesTags: ['Units'] }), addUnit: builder.mutation({ query: unitDataArgs => ({ url: 'api/units/addUnit', method: 'POST', body: { ...unitDataArgs } - }) + }), + onQueryStarted: (_arg, api) => { + api.queryFulfilled + .then(() => { + api.dispatch( + conversionsApi.endpoints.refresh.initiate({ + redoCik: true, + refreshReadingViews: false + })) + }) + }, + invalidatesTags: ['Units'] }), - editUnit: builder.mutation({ - //TODO VALIDATE BEHAVIOR should invalidate? - query: unitDataArgs => ({ + editUnit: builder.mutation({ + query: ({ editedUnit }) => ({ url: 'api/units/edit', method: 'POST', - body: { ...unitDataArgs } - }) + body: { ...editedUnit } + }), + onQueryStarted: ({ shouldRedoCik, shouldRefreshReadingViews }, api) => { + api.queryFulfilled + .then(() => { + api.dispatch( + conversionsApi.endpoints.refresh.initiate({ + redoCik: shouldRedoCik, + refreshReadingViews: shouldRefreshReadingViews + })) + }) + }, + invalidatesTags: ['Units'] }) }) }) - -/** - * Selects the most recent query status - * @param state - The complete state of the redux store. - * @returns The unit data corresponding to the `unitID` if found, or undefined if not. - * @example - * - * const queryState = useAppSelector(state => selectUnitDataByIdQueryState(state)) - * const {data: unitDataById = {}} = useAppSelector(state => selectUnitDataById(state)) - */ export const selectUnitDataResult = unitsApi.endpoints.getUnitsDetails.select() export const { selectAll: selectAllUnits, diff --git a/src/client/app/redux/api/userApi.ts b/src/client/app/redux/api/userApi.ts index 4a6dab595..cad1f47cc 100644 --- a/src/client/app/redux/api/userApi.ts +++ b/src/client/app/redux/api/userApi.ts @@ -17,7 +17,7 @@ export const userApi = baseApi.injectEndpoints({ query: user => ({ url: 'api/users/create', method: 'POST', - body: { user } + body: { ...user } }), invalidatesTags: ['Users'] }), diff --git a/src/client/app/redux/componentHooks.ts b/src/client/app/redux/componentHooks.ts index dc3e3c0d4..dc6aa2b1a 100644 --- a/src/client/app/redux/componentHooks.ts +++ b/src/client/app/redux/componentHooks.ts @@ -1,7 +1,10 @@ // import * as React from 'react'; -import { selectInitComplete } from '../reducers/appStateSlice'; -import { selectCurrentUserRole, selectIsAdmin } from '../reducers/currentUser'; -import { useAppSelector } from './hooks'; +import { selectInitComplete, selectSelectedLanguage } from './slices/appStateSlice'; +import { selectCurrentUserRole, selectIsAdmin } from './slices/currentUserSlice'; +import { useAppSelector } from './reduxHooks'; +import localeData, { LocaleDataKey } from '../translations/data'; +import { createIntlCache, createIntl, defineMessages } from 'react-intl'; + export const useWaitForInit = () => { @@ -9,4 +12,26 @@ export const useWaitForInit = () => { const userRole = useAppSelector(selectCurrentUserRole); const initComplete = useAppSelector(selectInitComplete); return { isAdmin, userRole, initComplete } -} \ No newline at end of file +} + +// Overloads to support TS key completions +type TranslateFunction = { + (messageID: LocaleDataKey): string; + (messageID: string): string; +} + +// usage +// const translate = useTranslate() +// translate('myKey') +export const useTranslate = () => { + const lang = useAppSelector(selectSelectedLanguage) + const cache = createIntlCache(); + const messages = localeData[lang]; + const intl = createIntl({ locale: lang, messages }, cache); + + const translate: TranslateFunction = (messageID: LocaleDataKey | string) => { + return intl.formatMessage(defineMessages({ [messageID]: { id: messageID } })[messageID]); + } + + return translate +}; \ No newline at end of file diff --git a/src/client/app/redux/middleware/graphHistory.ts b/src/client/app/redux/middleware/graphHistory.ts index ec08583f4..983078c19 100644 --- a/src/client/app/redux/middleware/graphHistory.ts +++ b/src/client/app/redux/middleware/graphHistory.ts @@ -1,6 +1,6 @@ // https://redux-toolkit.js.org/api/createListenerMiddleware#typescript-usage import { isAnyOf } from '@reduxjs/toolkit'; -import { graphSlice, updateHistory } from '../../reducers/graph'; +import { graphSlice, updateHistory } from '../slices/graphSlice'; import { AppStartListening } from './middleware'; export const historyMiddleware = (startListening: AppStartListening) => { diff --git a/src/client/app/reducers/maps.ts b/src/client/app/redux/reducers/maps.ts similarity index 95% rename from src/client/app/reducers/maps.ts rename to src/client/app/redux/reducers/maps.ts index 5c9c64548..14a56245b 100644 --- a/src/client/app/reducers/maps.ts +++ b/src/client/app/redux/reducers/maps.ts @@ -2,11 +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 { MapMetadata, MapsAction, MapState } from '../types/redux/map'; -import { ActionType } from '../types/redux/actions'; +import { MapMetadata, MapsAction, MapState } from '../../types/redux/map'; +import { ActionType } from '../../types/redux/actions'; import * as _ from 'lodash'; -import { CalibratedPoint } from '../utils/calibration'; -import { RootState } from '../store'; +import { CalibratedPoint } from '../../utils/calibration'; +import { RootState } from '../../store'; const defaultState: MapState = { isLoading: false, @@ -19,6 +19,7 @@ const defaultState: MapState = { calibrationSettings: { showGrid: false } }; +// eslint-disable-next-line jsdoc/require-jsdoc export default function maps(state = defaultState, action: MapsAction) { let submitting; let editedMaps; diff --git a/src/client/app/redux/hooks.ts b/src/client/app/redux/reduxHooks.ts similarity index 100% rename from src/client/app/redux/hooks.ts rename to src/client/app/redux/reduxHooks.ts diff --git a/src/client/app/reducers/index.ts b/src/client/app/redux/rootReducer.ts similarity index 54% rename from src/client/app/reducers/index.ts rename to src/client/app/redux/rootReducer.ts index 3827e0121..a6d29340c 100644 --- a/src/client/app/reducers/index.ts +++ b/src/client/app/redux/rootReducer.ts @@ -3,22 +3,18 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { combineReducers } from 'redux'; -import maps from './maps'; -import { adminSlice } from './admin'; -import { currentUserSlice } from './currentUser'; -import { unsavedWarningSlice } from './unsavedWarning'; -import { optionsSlice } from './options'; -import { baseApi } from '../redux/api/baseApi'; -import { graphSlice } from './graph'; -import { appStateSlice } from './appStateSlice'; +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'; export const rootReducer = combineReducers({ appState: appStateSlice.reducer, graph: graphSlice.reducer, admin: adminSlice.reducer, currentUser: currentUserSlice.reducer, - unsavedWarning: unsavedWarningSlice.reducer, - options: optionsSlice.reducer, // RTK Query's Derived Reducers [baseApi.reducerPath]: baseApi.reducer, maps diff --git a/src/client/app/redux/selectors/adminSelectors.ts b/src/client/app/redux/selectors/adminSelectors.ts index 6e29b57e7..f04a57912 100644 --- a/src/client/app/redux/selectors/adminSelectors.ts +++ b/src/client/app/redux/selectors/adminSelectors.ts @@ -1,6 +1,6 @@ import { createSelector } from '@reduxjs/toolkit' import * as _ from 'lodash' -import { selectAdminState } from '../../reducers/admin' +import { selectAdminState } from '../slices/adminSlice' import { selectConversionsDetails } from '../../redux/api/conversionsApi' import { selectAllGroups } from '../../redux/api/groupsApi' import { selectAllMeters, selectMeterById } from '../../redux/api/metersApi' diff --git a/src/client/app/redux/selectors/authVisibilitySelectors.ts b/src/client/app/redux/selectors/authVisibilitySelectors.ts index 1cb07ae38..eab1dabf2 100644 --- a/src/client/app/redux/selectors/authVisibilitySelectors.ts +++ b/src/client/app/redux/selectors/authVisibilitySelectors.ts @@ -1,6 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import * as _ from 'lodash'; -import { selectIsAdmin } from '../../reducers/currentUser'; +import { selectIsAdmin } from '../slices/currentUserSlice'; import { selectAllMeters } from '../../redux/api/metersApi'; import { DisplayableType, UnitType } from '../../types/redux/units'; import { selectAllGroups } from '../api/groupsApi'; diff --git a/src/client/app/redux/selectors/chartQuerySelectors.ts b/src/client/app/redux/selectors/chartQuerySelectors.ts index 834e9a3c1..516a6b545 100644 --- a/src/client/app/redux/selectors/chartQuerySelectors.ts +++ b/src/client/app/redux/selectors/chartQuerySelectors.ts @@ -8,7 +8,7 @@ import { selectCompareTimeInterval, selectQueryTimeInterval, selectSelectedGroups, selectSelectedMeters, selectSelectedUnit, selectThreeDState -} from '../../reducers/graph'; +} from '../slices/graphSlice'; import { MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; import { calculateCompareShift } from '../../utils/calculateCompare'; import { roundTimeIntervalForFetch } from '../../utils/dateRangeCompatibility'; diff --git a/src/client/app/redux/selectors/threeDSelectors.ts b/src/client/app/redux/selectors/threeDSelectors.ts index 773d34001..1686cf38a 100644 --- a/src/client/app/redux/selectors/threeDSelectors.ts +++ b/src/client/app/redux/selectors/threeDSelectors.ts @@ -1,7 +1,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { selectThreeDMeterOrGroup, selectThreeDMeterOrGroupID -} from '../../reducers/graph'; +} from '../slices/graphSlice'; import { selectGroupDataById } from '../../redux/api/groupsApi'; import { MeterOrGroup } from '../../types/redux/graph'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts index d9af59b5f..abcb9b659 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -4,7 +4,7 @@ import { createSelector } from '@reduxjs/toolkit'; import * as _ from 'lodash'; -import { selectMapState } from '../../reducers/maps'; +import { selectMapState } from '../reducers/maps'; import { DataType } from '../../types/Datasources'; import { GroupedOption, SelectOption } from '../../types/items'; import { ChartTypes, MeterOrGroup } from '../../types/redux/graph'; @@ -19,7 +19,7 @@ import { selectChartToRender, selectGraphAreaNormalization, selectSelectedGroups, selectSelectedMeters, selectSelectedUnit -} from '../../reducers/graph'; +} from '../slices/graphSlice'; import { selectGroupDataById } from '../../redux/api/groupsApi'; import { selectUnitDataById } from '../../redux/api/unitsApi'; import { selectVisibleMetersAndGroups, selectVisibleUnitOrSuffixState } from './authVisibilitySelectors'; diff --git a/src/client/app/redux/slices/thunkSlice.ts b/src/client/app/redux/sliceCreators.ts similarity index 100% rename from src/client/app/redux/slices/thunkSlice.ts rename to src/client/app/redux/sliceCreators.ts diff --git a/src/client/app/reducers/admin.ts b/src/client/app/redux/slices/adminSlice.ts similarity index 93% rename from src/client/app/reducers/admin.ts rename to src/client/app/redux/slices/adminSlice.ts index 8f7c312d1..78ba713ba 100644 --- a/src/client/app/reducers/admin.ts +++ b/src/client/app/redux/slices/adminSlice.ts @@ -4,13 +4,13 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import * as moment from 'moment'; -import { preferencesApi } from '../redux/api/preferencesApi'; -import { PreferenceRequestItem } from '../types/items'; -import { AdminState } from '../types/redux/admin'; -import { ChartTypes } from '../types/redux/graph'; -import { LanguageTypes } from '../types/redux/i18n'; -import { durationFormat } from '../utils/durationFormat'; -import { AreaUnitType } from '../utils/getAreaUnitConversion'; +import { preferencesApi } from '../api/preferencesApi'; +import { PreferenceRequestItem } from '../../types/items'; +import { AdminState } from '../../types/redux/admin'; +import { ChartTypes } from '../../types/redux/graph'; +import { LanguageTypes } from '../../types/redux/i18n'; +import { durationFormat } from '../../utils/durationFormat'; +import { AreaUnitType } from '../../utils/getAreaUnitConversion'; export const defaultAdminState: AdminState = { selectedMeter: null, diff --git a/src/client/app/reducers/appStateSlice.ts b/src/client/app/redux/slices/appStateSlice.ts similarity index 68% rename from src/client/app/reducers/appStateSlice.ts rename to src/client/app/redux/slices/appStateSlice.ts index 3f617aa68..898043d22 100644 --- a/src/client/app/reducers/appStateSlice.ts +++ b/src/client/app/redux/slices/appStateSlice.ts @@ -1,25 +1,29 @@ import { fetchMapsDetails } from '../actions/map'; -import { authApi } from '../redux/api/authApi'; -import { conversionsApi } from '../redux/api/conversionsApi'; -import { groupsApi } from '../redux/api/groupsApi'; -import { metersApi } from '../redux/api/metersApi'; -import { preferencesApi } from '../redux/api/preferencesApi'; -import { unitsApi } from '../redux/api/unitsApi'; -import { userApi } from '../redux/api/userApi'; -import { versionApi } from '../redux/api/versionApi'; -import { createThunkSlice } from '../redux/slices/thunkSlice'; -import { deleteToken, getToken, hasToken } from '../utils/token'; -import { currentUserSlice } from './currentUser'; +import { authApi } from '../api/authApi'; +import { conversionsApi } from '../api/conversionsApi'; +import { groupsApi } from '../api/groupsApi'; +import { metersApi } from '../api/metersApi'; +import { preferencesApi } from '../api/preferencesApi'; +import { unitsApi } from '../api/unitsApi'; +import { userApi } from '../api/userApi'; +import { versionApi } from '../api/versionApi'; +import { createThunkSlice } from '../sliceCreators'; +import { deleteToken, getToken, hasToken } from '../../utils/token'; +import { currentUserSlice } from './currentUserSlice'; +import { LanguageTypes } from '../../types/redux/i18n'; +import * as moment from 'moment'; interface appStateSlice { initComplete: boolean; optionsVisibility: boolean; - + selectedLanguage: LanguageTypes; } const defaultState: appStateSlice = { initComplete: false, - optionsVisibility: true + optionsVisibility: true, + selectedLanguage: LanguageTypes.en + } export const appStateSlice = createThunkSlice({ @@ -37,6 +41,9 @@ export const appStateSlice = createThunkSlice({ setOptionsVisibility: create.reducer((state, action) => { state.optionsVisibility = action.payload }), + updateSelectedLanguage: create.reducer((state, action) => { + state.selectedLanguage = action.payload + }), initApp: create.asyncThunk( // Thunk initiates many data fetching calls on startup before react begins to render async (_: void, { dispatch }) => { @@ -71,7 +78,7 @@ export const appStateSlice = createThunkSlice({ // User had a token that isn't valid or getUserDetails threw an error. // Assume token is invalid. Delete if any deleteToken() - dispatch(currentUserSlice.actions.setUserToken(null)) + dispatch(currentUserSlice.actions.clearCurrentUser()) } } @@ -86,11 +93,17 @@ export const appStateSlice = createThunkSlice({ } ) - }), + extraReducers: builder => { + builder.addMatcher(preferencesApi.endpoints.getPreferences.matchFulfilled, (state, action) => { + state.selectedLanguage = action.payload.defaultLanguage + moment.locale(action.payload.defaultLanguage); + }) + }, selectors: { selectInitComplete: state => state.initComplete, - selectOptionsVisibility: state => state.optionsVisibility + selectOptionsVisibility: state => state.optionsVisibility, + selectSelectedLanguage: state => state.selectedLanguage } }) @@ -98,10 +111,12 @@ export const { initApp, setInitComplete, toggleOptionsVisibility, - setOptionsVisibility + setOptionsVisibility, + updateSelectedLanguage } = appStateSlice.actions export const { selectInitComplete, - selectOptionsVisibility + selectOptionsVisibility, + selectSelectedLanguage } = appStateSlice.selectors diff --git a/src/client/app/reducers/currentUser.ts b/src/client/app/redux/slices/currentUserSlice.ts similarity index 87% rename from src/client/app/reducers/currentUser.ts rename to src/client/app/redux/slices/currentUserSlice.ts index 19546ccdb..aa71e33fa 100644 --- a/src/client/app/reducers/currentUser.ts +++ b/src/client/app/redux/slices/currentUserSlice.ts @@ -4,11 +4,11 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; -import { authApi } from '../redux/api/authApi'; -import { userApi } from '../redux/api/userApi'; -import { User, UserRole } from '../types/items'; -import { CurrentUserState } from '../types/redux/currentUser'; -import { setToken } from '../utils/token'; +import { authApi } from '../api/authApi'; +import { userApi } from '../api/userApi'; +import { User, UserRole } from '../../types/items'; +import { CurrentUserState } from '../../types/redux/currentUser'; +import { setToken } from '../../utils/token'; /* * Defines store interactions when version related actions are dispatched to the store. @@ -32,6 +32,7 @@ export const currentUserSlice = createSlice({ }, clearCurrentUser: state => { state.profile = null + state.token = null }, setUserToken: (state, action: PayloadAction) => { state.token = action.payload diff --git a/src/client/app/reducers/graph.ts b/src/client/app/redux/slices/graphSlice.ts similarity index 97% rename from src/client/app/reducers/graph.ts rename to src/client/app/redux/slices/graphSlice.ts index 0fda16e05..139c21df5 100644 --- a/src/client/app/reducers/graph.ts +++ b/src/client/app/redux/slices/graphSlice.ts @@ -5,12 +5,12 @@ import { PayloadAction, createAction, createSlice } from '@reduxjs/toolkit'; import * as moment from 'moment'; import { ActionMeta } from 'react-select'; -import { TimeInterval } from '../../../common/TimeInterval'; -import { preferencesApi } from '../redux/api/preferencesApi'; -import { SelectOption } from '../types/items'; -import { ChartTypes, GraphState, LineGraphRate, MeterOrGroup, ReadingInterval } from '../types/redux/graph'; -import { ComparePeriod, SortingOrder, calculateCompareTimeInterval } from '../utils/calculateCompare'; -import { AreaUnitType } from '../utils/getAreaUnitConversion'; +import { TimeInterval } from '../../../../common/TimeInterval'; +import { preferencesApi } from '../api/preferencesApi'; +import { SelectOption } from '../../types/items'; +import { ChartTypes, GraphState, LineGraphRate, MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; +import { ComparePeriod, SortingOrder, calculateCompareTimeInterval } from '../../utils/calculateCompare'; +import { AreaUnitType } from '../../utils/getAreaUnitConversion'; const defaultState: GraphState = { selectedMeters: [], diff --git a/src/client/app/store.ts b/src/client/app/store.ts index fab239e5a..945357310 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { configureStore } from '@reduxjs/toolkit' -import { rootReducer } from './reducers'; +import { rootReducer } from './redux/rootReducer'; import { baseApi } from './redux/api/baseApi'; import { Dispatch } from './types/redux/actions'; import { listenerMiddleware } from './redux/middleware/middleware'; diff --git a/src/client/app/utils/api/ConversionArrayApi.ts b/src/client/app/utils/api/ConversionArrayApi.ts index 876a8a204..fdb5eefa7 100644 --- a/src/client/app/utils/api/ConversionArrayApi.ts +++ b/src/client/app/utils/api/ConversionArrayApi.ts @@ -4,6 +4,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// TODO Marked For Deletion after RTK migration solidified +/* eslint-disable jsdoc/check-param-names */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// @ts-nocheck + import ApiBackend from './ApiBackend'; export default class ConversionArrayApi { diff --git a/src/client/app/utils/api/ConversionsApi.ts b/src/client/app/utils/api/ConversionsApi.ts index b88f52e9b..c73d943fc 100644 --- a/src/client/app/utils/api/ConversionsApi.ts +++ b/src/client/app/utils/api/ConversionsApi.ts @@ -1,8 +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/. - */ + * 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 Marked For Deletion after RTK migration solidified +/* eslint-disable jsdoc/check-param-names */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// @ts-nocheck import ApiBackend from './ApiBackend'; import { ConversionData } from '../../types/redux/conversions'; diff --git a/src/client/app/utils/api/GroupsApi.ts b/src/client/app/utils/api/GroupsApi.ts index 111c51ed7..190da9269 100644 --- a/src/client/app/utils/api/GroupsApi.ts +++ b/src/client/app/utils/api/GroupsApi.ts @@ -4,6 +4,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// TODO Marked For Deletion after RTK migration solidified +/* eslint-disable jsdoc/check-param-names */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// @ts-nocheck + import ApiBackend from './ApiBackend'; import * as moment from 'moment'; import { CompareReadings } from '../../types/readings'; diff --git a/src/client/app/utils/api/MapsApi.ts b/src/client/app/utils/api/MapsApi.ts index 047c9c3d8..be8a1775e 100644 --- a/src/client/app/utils/api/MapsApi.ts +++ b/src/client/app/utils/api/MapsApi.ts @@ -4,8 +4,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// TODO Marked For Deletion after RTK migration solidified +/* eslint-disable jsdoc/check-param-names */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// @ts-nocheck + import ApiBackend from './ApiBackend'; -import {MapData} from '../../types/redux/map'; +import { MapData } from '../../types/redux/map'; export default class MapsApi { private readonly backend: ApiBackend; @@ -27,7 +33,7 @@ export default class MapsApi { } public async delete(id: number): Promise { - return await this.backend.doPostRequest('/api/maps/delete', {id}); + return await this.backend.doPostRequest('/api/maps/delete', { id }); } public async getMapById(id: number): Promise { @@ -35,6 +41,6 @@ export default class MapsApi { } public async getMapByName(name: string): Promise { - return await this.backend.doGetRequest('/api/maps/getByName', {'name':name}); + return await this.backend.doGetRequest('/api/maps/getByName', { 'name': name }); } } diff --git a/src/client/app/utils/api/MetersApi.ts b/src/client/app/utils/api/MetersApi.ts index 537f18960..fef15bf00 100644 --- a/src/client/app/utils/api/MetersApi.ts +++ b/src/client/app/utils/api/MetersApi.ts @@ -3,7 +3,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/. */ - +// TODO Marked For Deletion after RTK migration solidified +/* eslint-disable jsdoc/check-param-names */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// @ts-nocheck import ApiBackend from './ApiBackend'; import { NamedIDItem } from '../../types/items'; import { CompareReadings, RawReadings } from '../../types/readings'; diff --git a/src/client/app/utils/api/PreferencesApi.ts b/src/client/app/utils/api/PreferencesApi.ts index c404d39c2..eafb1522e 100644 --- a/src/client/app/utils/api/PreferencesApi.ts +++ b/src/client/app/utils/api/PreferencesApi.ts @@ -4,6 +4,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// TODO Marked For Deletion after RTK migration solidified +/* eslint-disable jsdoc/check-param-names */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// @ts-nocheck import ApiBackend from './ApiBackend'; import { PreferenceRequestItem } from '../../types/items'; diff --git a/src/client/app/utils/api/ReadingsApi.ts b/src/client/app/utils/api/ReadingsApi.ts index 080c32fb7..b875ac227 100644 --- a/src/client/app/utils/api/ReadingsApi.ts +++ b/src/client/app/utils/api/ReadingsApi.ts @@ -3,6 +3,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/. */ +// TODO Marked For Deletion after RTK migration solidified +/* eslint-disable jsdoc/check-param-names */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// @ts-nocheck + import * as _ from 'lodash'; import ApiBackend from './ApiBackend'; diff --git a/src/client/app/utils/api/UsersApi.ts b/src/client/app/utils/api/UsersApi.ts index 578f497b0..2c04ccbd2 100644 --- a/src/client/app/utils/api/UsersApi.ts +++ b/src/client/app/utils/api/UsersApi.ts @@ -3,7 +3,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/. */ - +// TODO Marked For Deletion after RTK migration solidified +/* eslint-disable jsdoc/check-param-names */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// @ts-nocheck import ApiBackend from './ApiBackend'; import { User, UserRole } from '../../types/items'; import { hasPermissions } from '../../utils/hasPermissions'; diff --git a/src/client/app/utils/api/VerificationApi.ts b/src/client/app/utils/api/VerificationApi.ts index 3390d9e40..84e4ad337 100644 --- a/src/client/app/utils/api/VerificationApi.ts +++ b/src/client/app/utils/api/VerificationApi.ts @@ -3,7 +3,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/. */ - +// TODO Marked For Deletion after RTK migration solidified +/* eslint-disable jsdoc/check-param-names */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// @ts-nocheck import ApiBackend from './ApiBackend'; import { getToken } from '../token'; import { User } from '../../types/items'; @@ -21,7 +25,7 @@ export default class VerificationApi { public async checkTokenValid(): Promise { // This will not throw an error if the status code is 401 unauthorized or 403 forbidden - const { success } = await this.backend.doPostRequest<{success: boolean}>( + const { success } = await this.backend.doPostRequest<{ success: boolean }>( '/api/verification', { token: getToken() }, undefined, @@ -31,7 +35,7 @@ export default class VerificationApi { } public async login(email: string, password: string): Promise { - const response = await this.backend.doPostRequest('/api/login/', {email, password}); + const response = await this.backend.doPostRequest('/api/login/', { email, password }); return response; } } diff --git a/src/client/app/utils/api/VersionApi.ts b/src/client/app/utils/api/VersionApi.ts index 1da2ef8b1..8505cb4d0 100644 --- a/src/client/app/utils/api/VersionApi.ts +++ b/src/client/app/utils/api/VersionApi.ts @@ -4,6 +4,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// TODO Marked For Deletion after RTK migration solidified +/* eslint-disable jsdoc/check-param-names */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// @ts-nocheck import ApiBackend from './ApiBackend'; export default class VersionApi { diff --git a/src/client/app/utils/api/unitsApi.ts b/src/client/app/utils/api/unitsApi.ts index 991f3eb30..0be1c7caa 100644 --- a/src/client/app/utils/api/unitsApi.ts +++ b/src/client/app/utils/api/unitsApi.ts @@ -1,8 +1,14 @@ /* - * 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/. - */ + * 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 Marked For Deletion after RTK migration solidified +/* eslint-disable jsdoc/check-param-names */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// @ts-nocheck import ApiBackend from './ApiBackend'; import { UnitData, UnitEditData } from '../../types/redux/units'; diff --git a/src/client/app/utils/calibration.ts b/src/client/app/utils/calibration.ts index cd4f7fd99..489401525 100644 --- a/src/client/app/utils/calibration.ts +++ b/src/client/app/utils/calibration.ts @@ -3,7 +3,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { MapMetadata } from '../types/redux/map'; -import { logToServer } from '../actions/logs'; +import { logToServer } from '../redux/actions/logs'; import { DataType } from '../types/Datasources'; import translate from './translate'; diff --git a/src/client/app/utils/determineCompatibleUnits.ts b/src/client/app/utils/determineCompatibleUnits.ts index 141347eb6..e0a852f87 100644 --- a/src/client/app/utils/determineCompatibleUnits.ts +++ b/src/client/app/utils/determineCompatibleUnits.ts @@ -2,7 +2,9 @@ * 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 { store } from '../store'; +// TODO it is a bad practice to import store anywhere other than index.tsx These utils need to be converted into selectors. + +import { store } from '../store'; import * as _ from 'lodash'; import React from 'react'; import { selectPik } from '../redux/api/conversionsApi'; diff --git a/src/client/app/utils/translate.ts b/src/client/app/utils/translate.ts index 202cbb40b..f3286dcdc 100644 --- a/src/client/app/utils/translate.ts +++ b/src/client/app/utils/translate.ts @@ -5,7 +5,7 @@ import { defineMessages, createIntl, createIntlCache } from 'react-intl'; import { LocaleDataKey, TranslationKey } from '../translations/data'; import localeData from '../translations/data'; -import { store } from '../store'; +import { store } from '../store'; // Function overloads to add TS Completions support function translate(messageID: LocaleDataKey): string; @@ -23,7 +23,9 @@ function translate(messageID: LocaleDataKey | string): string { // For now, set the default language to english and any component subscribed to the language state should properly re-render if the language changes let lang: TranslationKey = 'en'; if (store) { - lang = store.getState().options.selectedLanguage; + // TODO Its a bad practice to import store anywhere other than index.tsx + // migrate to useTranslate() from componentHooks.ts + lang = store.getState().appState.selectedLanguage; } /* const state: any = store.getState(); From 1e1b9647d7431b2d9e93c867c568715aa083b320 Mon Sep 17 00:00:00 2001 From: Kaito <134911191+kaito1105@users.noreply.github.com> Date: Sat, 6 Jan 2024 01:27:44 +0000 Subject: [PATCH 053/131] test B5 Co-authored-by: Maryam Bouamama Co-authored-by: Sebastian Ramos --- .../test/web/readingsBarMeterQuantity.js | 20 ++++++++++++++++--- ..._15_mu_kWh_gu_kWh_st_-inf_et_inf_bd_75.csv | 2 ++ 2 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 src/server/test/web/readingsData/expected_bar_ri_15_mu_kWh_gu_kWh_st_-inf_et_inf_bd_75.csv diff --git a/src/server/test/web/readingsBarMeterQuantity.js b/src/server/test/web/readingsBarMeterQuantity.js index 2e54fd58c..df931d955 100644 --- a/src/server/test/web/readingsBarMeterQuantity.js +++ b/src/server/test/web/readingsBarMeterQuantity.js @@ -94,9 +94,23 @@ mocha.describe('readings API', () => { // Check that the API reading is equal to what it is expected to equal expectReadingToEqualExpected(res, expected); }); - - // Add B5 here - + mocha.it('B5: 75 day bars for 15 minute reading intervals and quantity units with +-inf start/end time & kWh as kWh', async () => { + // Load the data into the database + await prepareTest(unitDatakWh, conversionDatakWh, meterDatakWh); + // Get the unit ID since the DB could use any value. + const unitId = await getUnitId('kWh'); + // Load the expected response data from the corresponding csv file + const expected = await parseExpectedCsv('src/server/test/web/readingsData/expected_bar_ri_15_mu_kWh_gu_kWh_st_-inf_et_inf_bd_75.csv'); + // Create a request to the API for unbounded reading times and save the response + const res = await chai.request(app).get(`/api/unitReadings/bar/meters/${METER_ID}`) + .query({ + timeInterval: ETERNITY.toString(), + barWidthDays: 75, + graphicUnitId: unitId + }); + // Check that the API reading is equal to what it is expected to equal + expectReadingToEqualExpected(res, expected); + }); mocha.it('B6: 76 day bars (no values) for 15 minute reading intervals and quantity units with +-inf start/end time & kWh as kWh', async () => { // Load the data into the database await prepareTest(unitDatakWh, conversionDatakWh, meterDatakWh); diff --git a/src/server/test/web/readingsData/expected_bar_ri_15_mu_kWh_gu_kWh_st_-inf_et_inf_bd_75.csv b/src/server/test/web/readingsData/expected_bar_ri_15_mu_kWh_gu_kWh_st_-inf_et_inf_bd_75.csv new file mode 100644 index 000000000..6b72c59c2 --- /dev/null +++ b/src/server/test/web/readingsData/expected_bar_ri_15_mu_kWh_gu_kWh_st_-inf_et_inf_bd_75.csv @@ -0,0 +1,2 @@ +reading,start time,end time +356063.811769456,2022-08-18 00:00:00,2022-11-01 00:00:00 From a9a1cd677774547d077c11d2464b12716319203f Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Mon, 15 Jan 2024 03:40:54 +0000 Subject: [PATCH 054/131] Fix Headers --- src/client/app/components/AppLayout.tsx | 4 ++++ src/client/app/components/BarControlsComponent.tsx | 4 ++++ src/client/app/components/CompareControlsComponent.tsx | 4 ++++ src/client/app/components/HistoryComponent.tsx | 4 ++++ src/client/app/components/LogoSpinner.tsx | 4 ++++ src/client/app/components/MapControlsComponent.tsx | 5 +++++ src/client/app/redux/api/authApi.ts | 4 ++++ src/client/app/redux/api/baseApi.ts | 4 ++++ src/client/app/redux/api/conversionsApi.ts | 4 ++++ src/client/app/redux/api/groupsApi.ts | 4 ++++ src/client/app/redux/api/metersApi.ts | 4 ++++ src/client/app/redux/api/preferencesApi.ts | 4 ++++ src/client/app/redux/api/readingsApi.ts | 4 ++++ src/client/app/redux/api/unitsApi.ts | 4 ++++ src/client/app/redux/api/userApi.ts | 4 ++++ src/client/app/redux/api/versionApi.ts | 4 ++++ src/client/app/redux/componentHooks.ts | 5 ++++- src/client/app/redux/middleware/graphHistory.ts | 5 ++++- src/client/app/redux/middleware/middleware.ts | 4 ++++ src/client/app/redux/reduxHooks.ts | 4 ++++ src/client/app/redux/selectors/adminSelectors.ts | 4 ++++ src/client/app/redux/selectors/authVisibilitySelectors.ts | 4 ++++ src/client/app/redux/selectors/chartQuerySelectors.ts | 4 ++++ src/client/app/redux/selectors/selectors.ts | 4 ++++ src/client/app/redux/selectors/threeDSelectors.ts | 4 ++++ src/client/app/redux/sliceCreators.ts | 4 ++++ src/client/app/redux/slices/appStateSlice.ts | 4 ++++ src/client/app/styles/DateRangeCustom.css | 4 ++++ 28 files changed, 113 insertions(+), 2 deletions(-) diff --git a/src/client/app/components/AppLayout.tsx b/src/client/app/components/AppLayout.tsx index 9944e1615..4d19d39e6 100644 --- a/src/client/app/components/AppLayout.tsx +++ b/src/client/app/components/AppLayout.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 { Outlet } from 'react-router-dom' import { Slide, ToastContainer } from 'react-toastify' diff --git a/src/client/app/components/BarControlsComponent.tsx b/src/client/app/components/BarControlsComponent.tsx index 239285249..bf83b2aa9 100644 --- a/src/client/app/components/BarControlsComponent.tsx +++ b/src/client/app/components/BarControlsComponent.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 moment from 'moment'; import sliderWithoutTooltips, { createSliderWithTooltip } from 'rc-slider'; import 'rc-slider/assets/index.css'; diff --git a/src/client/app/components/CompareControlsComponent.tsx b/src/client/app/components/CompareControlsComponent.tsx index dc0574c85..143c5a03b 100644 --- a/src/client/app/components/CompareControlsComponent.tsx +++ b/src/client/app/components/CompareControlsComponent.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 moment from 'moment'; import * as React from 'react'; import { Button, ButtonGroup, Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'; diff --git a/src/client/app/components/HistoryComponent.tsx b/src/client/app/components/HistoryComponent.tsx index 16d868ecd..b18554908 100644 --- a/src/client/app/components/HistoryComponent.tsx +++ b/src/client/app/components/HistoryComponent.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 { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { diff --git a/src/client/app/components/LogoSpinner.tsx b/src/client/app/components/LogoSpinner.tsx index c3a592865..a638c2634 100644 --- a/src/client/app/components/LogoSpinner.tsx +++ b/src/client/app/components/LogoSpinner.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'; /** diff --git a/src/client/app/components/MapControlsComponent.tsx b/src/client/app/components/MapControlsComponent.tsx index 43f101a05..7e65649f0 100644 --- a/src/client/app/components/MapControlsComponent.tsx +++ b/src/client/app/components/MapControlsComponent.tsx @@ -1,3 +1,8 @@ +/* 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 translate from '../utils/translate'; import { Button, ButtonGroup } from 'reactstrap'; diff --git a/src/client/app/redux/api/authApi.ts b/src/client/app/redux/api/authApi.ts index 12c102844..fe1b2ad59 100644 --- a/src/client/app/redux/api/authApi.ts +++ b/src/client/app/redux/api/authApi.ts @@ -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 { currentUserSlice } from '../slices/currentUserSlice'; import { User } from '../../types/items'; import { deleteToken } from '../../utils/token'; diff --git a/src/client/app/redux/api/baseApi.ts b/src/client/app/redux/api/baseApi.ts index 3b9ea0615..d423f623e 100644 --- a/src/client/app/redux/api/baseApi.ts +++ b/src/client/app/redux/api/baseApi.ts @@ -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 { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; import { RootState } from '../../store'; // TODO Should be env variable? diff --git a/src/client/app/redux/api/conversionsApi.ts b/src/client/app/redux/api/conversionsApi.ts index 4bd2bc318..1a4e323d1 100644 --- a/src/client/app/redux/api/conversionsApi.ts +++ b/src/client/app/redux/api/conversionsApi.ts @@ -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 { createSelector } from '@reduxjs/toolkit'; import { ConversionData } from '../../types/redux/conversions'; import { baseApi } from './baseApi'; diff --git a/src/client/app/redux/api/groupsApi.ts b/src/client/app/redux/api/groupsApi.ts index f547e6ac1..12573be07 100644 --- a/src/client/app/redux/api/groupsApi.ts +++ b/src/client/app/redux/api/groupsApi.ts @@ -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 { EntityState, Update, createEntityAdapter } from '@reduxjs/toolkit'; import * as _ from 'lodash'; import { selectIsAdmin } from '../slices/currentUserSlice'; diff --git a/src/client/app/redux/api/metersApi.ts b/src/client/app/redux/api/metersApi.ts index b856cb73d..eeea86f41 100644 --- a/src/client/app/redux/api/metersApi.ts +++ b/src/client/app/redux/api/metersApi.ts @@ -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 { EntityState, createEntityAdapter } from '@reduxjs/toolkit'; import { NamedIDItem } from 'types/items'; import { RawReadings } from 'types/readings'; diff --git a/src/client/app/redux/api/preferencesApi.ts b/src/client/app/redux/api/preferencesApi.ts index ca67f721e..d2858f308 100644 --- a/src/client/app/redux/api/preferencesApi.ts +++ b/src/client/app/redux/api/preferencesApi.ts @@ -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 { PreferenceRequestItem } from '../../types/items'; import { baseApi } from './baseApi'; diff --git a/src/client/app/redux/api/readingsApi.ts b/src/client/app/redux/api/readingsApi.ts index e9463dd30..e49600b01 100644 --- a/src/client/app/redux/api/readingsApi.ts +++ b/src/client/app/redux/api/readingsApi.ts @@ -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 _ from 'lodash'; import { BarReadingApiArgs, diff --git a/src/client/app/redux/api/unitsApi.ts b/src/client/app/redux/api/unitsApi.ts index f379a2f37..4926a5f30 100644 --- a/src/client/app/redux/api/unitsApi.ts +++ b/src/client/app/redux/api/unitsApi.ts @@ -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 { EntityState, createEntityAdapter } from '@reduxjs/toolkit'; import { RootState } from 'store'; import { UnitData } from '../../types/redux/units'; diff --git a/src/client/app/redux/api/userApi.ts b/src/client/app/redux/api/userApi.ts index cad1f47cc..01cf573f4 100644 --- a/src/client/app/redux/api/userApi.ts +++ b/src/client/app/redux/api/userApi.ts @@ -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 { NewUser, User } from '../../types/items'; // import { authApi } from './authApi'; import { baseApi } from './baseApi'; diff --git a/src/client/app/redux/api/versionApi.ts b/src/client/app/redux/api/versionApi.ts index 2b12e70b2..9c12e4314 100644 --- a/src/client/app/redux/api/versionApi.ts +++ b/src/client/app/redux/api/versionApi.ts @@ -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 { createSelector } from '@reduxjs/toolkit'; import { baseApi } from './baseApi'; diff --git a/src/client/app/redux/componentHooks.ts b/src/client/app/redux/componentHooks.ts index dc6aa2b1a..e2b30302e 100644 --- a/src/client/app/redux/componentHooks.ts +++ b/src/client/app/redux/componentHooks.ts @@ -1,4 +1,7 @@ -// import * as React from 'react'; +/* 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 { selectInitComplete, selectSelectedLanguage } from './slices/appStateSlice'; import { selectCurrentUserRole, selectIsAdmin } from './slices/currentUserSlice'; import { useAppSelector } from './reduxHooks'; diff --git a/src/client/app/redux/middleware/graphHistory.ts b/src/client/app/redux/middleware/graphHistory.ts index 983078c19..c98e2a642 100644 --- a/src/client/app/redux/middleware/graphHistory.ts +++ b/src/client/app/redux/middleware/graphHistory.ts @@ -1,4 +1,7 @@ -// https://redux-toolkit.js.org/api/createListenerMiddleware#typescript-usage +/* 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 { isAnyOf } from '@reduxjs/toolkit'; import { graphSlice, updateHistory } from '../slices/graphSlice'; import { AppStartListening } from './middleware'; diff --git a/src/client/app/redux/middleware/middleware.ts b/src/client/app/redux/middleware/middleware.ts index a4f409ad9..a5af6ad75 100644 --- a/src/client/app/redux/middleware/middleware.ts +++ b/src/client/app/redux/middleware/middleware.ts @@ -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/. */ + // listenerMiddleware.ts // https://redux-toolkit.js.org/api/createListenerMiddleware#typescript-usage import { type TypedStartListening, type TypedAddListener, addListener, createListenerMiddleware } from '@reduxjs/toolkit' diff --git a/src/client/app/redux/reduxHooks.ts b/src/client/app/redux/reduxHooks.ts index 373643200..dfa13b47b 100644 --- a/src/client/app/redux/reduxHooks.ts +++ b/src/client/app/redux/reduxHooks.ts @@ -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 { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' import type { RootState, AppDispatch } from '../store' diff --git a/src/client/app/redux/selectors/adminSelectors.ts b/src/client/app/redux/selectors/adminSelectors.ts index f04a57912..323e98ec3 100644 --- a/src/client/app/redux/selectors/adminSelectors.ts +++ b/src/client/app/redux/selectors/adminSelectors.ts @@ -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 { createSelector } from '@reduxjs/toolkit' import * as _ from 'lodash' import { selectAdminState } from '../slices/adminSlice' diff --git a/src/client/app/redux/selectors/authVisibilitySelectors.ts b/src/client/app/redux/selectors/authVisibilitySelectors.ts index eab1dabf2..158ae1f0c 100644 --- a/src/client/app/redux/selectors/authVisibilitySelectors.ts +++ b/src/client/app/redux/selectors/authVisibilitySelectors.ts @@ -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 { createSelector } from '@reduxjs/toolkit'; import * as _ from 'lodash'; import { selectIsAdmin } from '../slices/currentUserSlice'; diff --git a/src/client/app/redux/selectors/chartQuerySelectors.ts b/src/client/app/redux/selectors/chartQuerySelectors.ts index 516a6b545..18601a9db 100644 --- a/src/client/app/redux/selectors/chartQuerySelectors.ts +++ b/src/client/app/redux/selectors/chartQuerySelectors.ts @@ -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 { createSelector } from '@reduxjs/toolkit'; import * as _ from 'lodash'; import * as moment from 'moment'; diff --git a/src/client/app/redux/selectors/selectors.ts b/src/client/app/redux/selectors/selectors.ts index cd92bcfe9..1c990b618 100644 --- a/src/client/app/redux/selectors/selectors.ts +++ b/src/client/app/redux/selectors/selectors.ts @@ -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 { createSelectorCreator, weakMapMemoize, diff --git a/src/client/app/redux/selectors/threeDSelectors.ts b/src/client/app/redux/selectors/threeDSelectors.ts index 1686cf38a..89b727e76 100644 --- a/src/client/app/redux/selectors/threeDSelectors.ts +++ b/src/client/app/redux/selectors/threeDSelectors.ts @@ -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 { createSelector } from '@reduxjs/toolkit'; import { selectThreeDMeterOrGroup, selectThreeDMeterOrGroupID diff --git a/src/client/app/redux/sliceCreators.ts b/src/client/app/redux/sliceCreators.ts index 7dc8a7c36..232c16ba5 100644 --- a/src/client/app/redux/sliceCreators.ts +++ b/src/client/app/redux/sliceCreators.ts @@ -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 { buildCreateSlice, asyncThunkCreator } from '@reduxjs/toolkit' export const createThunkSlice = buildCreateSlice({ diff --git a/src/client/app/redux/slices/appStateSlice.ts b/src/client/app/redux/slices/appStateSlice.ts index 898043d22..0ee03dbd9 100644 --- a/src/client/app/redux/slices/appStateSlice.ts +++ b/src/client/app/redux/slices/appStateSlice.ts @@ -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 { fetchMapsDetails } from '../actions/map'; import { authApi } from '../api/authApi'; import { conversionsApi } from '../api/conversionsApi'; diff --git a/src/client/app/styles/DateRangeCustom.css b/src/client/app/styles/DateRangeCustom.css index 30796fdeb..853fdec0e 100644 --- a/src/client/app/styles/DateRangeCustom.css +++ b/src/client/app/styles/DateRangeCustom.css @@ -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/. */ + /* not ideal, but fixes inconsistent width issue todo find better approach? */ .react-daterange-picker__wrapper { max-width: fit-content; From 85c6630e2c80df9ef39231c445581fb41ec7b847 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Mon, 15 Jan 2024 04:22:27 +0000 Subject: [PATCH 055/131] Fix failed tests --- .eslintrc.json | 3 +-- package-lock.json | 22 ------------------- package.json | 1 - src/client/app/components/AppLayout.tsx | 2 +- .../app/components/BarControlsComponent.tsx | 2 +- 5 files changed, 3 insertions(+), 27 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 071375750..a210f52ad 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -151,8 +151,7 @@ { // disable jsdoc requirement for reducers and actions "files": [ - "src/client/app/reducers/*.ts", - "src/client/app/actions/*.ts" + "src/client/app/redux/actions/*.ts" ], "rules": { "jsdoc/require-jsdoc": "off", diff --git a/package-lock.json b/package-lock.json index 177a34def..1e5a58acc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,7 +70,6 @@ "@types/react-dom": "~18.2.7", "@types/react-plotly.js": "~2.6.0", "@types/react-redux": "~7.1.25", - "@types/react-router-dom": "~5.3.0", "@typescript-eslint/eslint-plugin": "~6.4.1", "@typescript-eslint/parser": "~6.11.0", "babel-loader": "~8.2.3", @@ -2906,27 +2905,6 @@ "redux": "^4.0.0" } }, - "node_modules/@types/react-router": { - "version": "5.1.20", - "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", - "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", - "dev": true, - "dependencies": { - "@types/history": "^4.7.11", - "@types/react": "*" - } - }, - "node_modules/@types/react-router-dom": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", - "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", - "dev": true, - "dependencies": { - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router": "*" - } - }, "node_modules/@types/react-transition-group": { "version": "4.4.6", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz", diff --git a/package.json b/package.json index 52f203cb4..90c446c02 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,6 @@ "@types/react-dom": "~18.2.7", "@types/react-plotly.js": "~2.6.0", "@types/react-redux": "~7.1.25", - "@types/react-router-dom": "~5.3.0", "@typescript-eslint/eslint-plugin": "~6.4.1", "@typescript-eslint/parser": "~6.11.0", "babel-loader": "~8.2.3", diff --git a/src/client/app/components/AppLayout.tsx b/src/client/app/components/AppLayout.tsx index 4d19d39e6..3225caea3 100644 --- a/src/client/app/components/AppLayout.tsx +++ b/src/client/app/components/AppLayout.tsx @@ -1,7 +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 { Outlet } from 'react-router-dom' import { Slide, ToastContainer } from 'react-toastify' diff --git a/src/client/app/components/BarControlsComponent.tsx b/src/client/app/components/BarControlsComponent.tsx index bf83b2aa9..b30ea8b55 100644 --- a/src/client/app/components/BarControlsComponent.tsx +++ b/src/client/app/components/BarControlsComponent.tsx @@ -1,7 +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 moment from 'moment'; import sliderWithoutTooltips, { createSliderWithTooltip } from 'rc-slider'; import 'rc-slider/assets/index.css'; From ec2e5753883b00ff8274cf7062cf7c627d5a3782 Mon Sep 17 00:00:00 2001 From: Steven Huss-Lederman Date: Tue, 30 Jan 2024 13:32:39 -0600 Subject: [PATCH 056/131] format, console.log removed, simple edits --- .../app/components/LineChartComponent.tsx | 2 - .../app/components/MapChartComponent.tsx | 3 +- .../app/components/RadarChartComponent.tsx | 3 +- .../app/components/admin/AdminComponent.tsx | 1 - .../components/admin/PreferencesComponent.tsx | 8 +- .../components/admin/UsersDetailComponent.tsx | 3 +- .../CreateConversionModalComponent.tsx | 5 +- .../EditConversionModalComponent.tsx | 5 +- .../groups/CreateGroupModalComponent.tsx | 9 +-- .../groups/EditGroupModalComponent.tsx | 1 - .../components/groups/GroupViewComponent.tsx | 1 - .../groups/GroupsDetailComponent.tsx | 6 +- .../meters/CreateMeterModalComponent.tsx | 1 - .../app/components/router/ErrorComponent.tsx | 5 +- .../components/router/GraphLinkComponent.tsx | 76 +++++++++++-------- .../router/InitializingComponent.tsx | 5 +- src/client/app/redux/api/conversionsApi.ts | 3 +- src/client/app/redux/api/preferencesApi.ts | 1 - .../app/redux/selectors/adminSelectors.ts | 9 +-- .../app/redux/slices/currentUserSlice.ts | 2 +- src/client/app/translations/data.ts | 14 +++- 21 files changed, 77 insertions(+), 86 deletions(-) diff --git a/src/client/app/components/LineChartComponent.tsx b/src/client/app/components/LineChartComponent.tsx index 313a1979f..7d08e478b 100644 --- a/src/client/app/components/LineChartComponent.tsx +++ b/src/client/app/components/LineChartComponent.tsx @@ -245,7 +245,6 @@ export default function LineChartComponent() { return } }) - // console.log(datasets.length, datasets) // Customize the layout of the plot // See https://community.plotly.com/t/replacing-an-empty-graph-with-a-message/31497 for showing text not plot. if (datasets.length === 0) { @@ -263,7 +262,6 @@ export default function LineChartComponent() { console.log(e)} style={{ width: '100%', height: '80%' }} useResizeHandler={true} config={{ diff --git a/src/client/app/components/MapChartComponent.tsx b/src/client/app/components/MapChartComponent.tsx index 161a31f9f..2a67368db 100644 --- a/src/client/app/components/MapChartComponent.tsx +++ b/src/client/app/components/MapChartComponent.tsx @@ -43,7 +43,6 @@ export default function MapChartComponent() { const { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } = useAppSelector(selectMapChartQueryArgs) const { data: meterReadings, isLoading: meterIsFetching } = readingsApi.useBarQuery(meterArgs, { skip: meterShouldSkip }); const { data: groupData, isLoading: groupIsFetching } = readingsApi.useBarQuery(groupArgs, { skip: groupShouldSkip }); - console.log(meterShouldSkip, groupShouldSkip, meterReadings, groupData) // converting maps to RTK has been proving troublesome, therefore using a combination of old/new stateSelectors const unitID = useAppSelector(selectSelectedUnit); @@ -384,4 +383,4 @@ export default function MapChartComponent() { layout={layout} /> ); -} \ No newline at end of file +} diff --git a/src/client/app/components/RadarChartComponent.tsx b/src/client/app/components/RadarChartComponent.tsx index c9d6b6adb..9f066a9b3 100644 --- a/src/client/app/components/RadarChartComponent.tsx +++ b/src/client/app/components/RadarChartComponent.tsx @@ -323,7 +323,6 @@ export default function RadarChartComponent() {
console.log(e)} style={{ width: '100%', height: '80%' }} useResizeHandler={true} config={{ @@ -335,4 +334,4 @@ export default function RadarChartComponent() { />
) -} \ No newline at end of file +} diff --git a/src/client/app/components/admin/AdminComponent.tsx b/src/client/app/components/admin/AdminComponent.tsx index f35ada6e4..31469997d 100644 --- a/src/client/app/components/admin/AdminComponent.tsx +++ b/src/client/app/components/admin/AdminComponent.tsx @@ -5,7 +5,6 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import TooltipHelpComponent from '../../components/TooltipHelpComponent'; -// import PreferencesContainer from '../../containers/admin/PreferencesContainer'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import PreferencesComponent from './PreferencesComponent'; import ManageUsersLinkButtonComponent from './users/ManageUsersLinkButtonComponent'; diff --git a/src/client/app/components/admin/PreferencesComponent.tsx b/src/client/app/components/admin/PreferencesComponent.tsx index f33c6b273..4a3fb5201 100644 --- a/src/client/app/components/admin/PreferencesComponent.tsx +++ b/src/client/app/components/admin/PreferencesComponent.tsx @@ -194,7 +194,6 @@ export default function PreferencesComponent() {

{`${translate('default.warning.file.size')}:`} -

- {/* Reuse same style as title. */}

{`${translate('default.meter.reading.frequency')}:`} @@ -334,10 +332,6 @@ export default function PreferencesComponent() { ); } - - - - const labelStyle: React.CSSProperties = { fontWeight: 'bold', margin: 0 @@ -350,4 +344,4 @@ const titleStyle: React.CSSProperties = { fontWeight: 'bold', margin: 0, paddingBottom: '5px' -}; \ No newline at end of file +}; diff --git a/src/client/app/components/admin/UsersDetailComponent.tsx b/src/client/app/components/admin/UsersDetailComponent.tsx index d29d97be2..e19e0fe79 100644 --- a/src/client/app/components/admin/UsersDetailComponent.tsx +++ b/src/client/app/components/admin/UsersDetailComponent.tsx @@ -27,9 +27,8 @@ export default function UserDetailComponent() { const [localUsersChanges, setLocalUsersChanges] = React.useState([]); const [hasChanges, setHasChanges] = React.useState(false); - React.useEffect(() => { setLocalUsersChanges(users) }, [users]) - React.useEffect(() => { !_.isEqual(users, localUsersChanges) ? setHasChanges(true) : setHasChanges(false) }, [localUsersChanges, users]) + React.useEffect(() => { setHasChanges(!_.isEqual(users, localUsersChanges)) }, [localUsersChanges, users]) const submitChanges = async () => { submitUserEdits(localUsersChanges) .unwrap() diff --git a/src/client/app/components/conversion/CreateConversionModalComponent.tsx b/src/client/app/components/conversion/CreateConversionModalComponent.tsx index 9b828a551..f160d33c0 100644 --- a/src/client/app/components/conversion/CreateConversionModalComponent.tsx +++ b/src/client/app/components/conversion/CreateConversionModalComponent.tsx @@ -84,11 +84,10 @@ export default function CreateConversionModalComponent() { if (validConversion) { // Close modal first to avoid repeat clicks setShowModal(false); - //5 Add the new conversion and update the store + // Add the new conversion and update the store // Omit the source options , do not need to send in request so remove here. // addConversionMutation(_.omit(conversionState, 'sourceOptions')) - // dispatch(addConversion(conversionState)); resetState(); } else { showErrorNotification(reason) @@ -243,4 +242,4 @@ export default function CreateConversionModalComponent() { ); -} \ No newline at end of file +} diff --git a/src/client/app/components/conversion/EditConversionModalComponent.tsx b/src/client/app/components/conversion/EditConversionModalComponent.tsx index cb36d3ebe..654871d6f 100644 --- a/src/client/app/components/conversion/EditConversionModalComponent.tsx +++ b/src/client/app/components/conversion/EditConversionModalComponent.tsx @@ -18,7 +18,6 @@ import translate from '../../utils/translate'; import ConfirmActionModalComponent from '../ConfirmActionModalComponent'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; - interface EditConversionModalComponentProps { show: boolean; conversion: ConversionData; @@ -124,10 +123,8 @@ export default function EditConversionModalComponent(props: EditConversionModalC const conversionHasChanges = shouldRedoCik || props.conversion.note != state.note; // Only do work if there are changes if (conversionHasChanges) { - // Save our changes by dispatching the submitEditedConversion action - // dispatch(submitEditedConversion(state, shouldRedoCik)); + // Save our changes editConversion({ conversionData: state, shouldRedoCik }) - // dispatch(unsavedWarningSlice.actions.removeUnsavedChanges()); } } diff --git a/src/client/app/components/groups/CreateGroupModalComponent.tsx b/src/client/app/components/groups/CreateGroupModalComponent.tsx index 3f4546d72..3c8b7fd6e 100644 --- a/src/client/app/components/groups/CreateGroupModalComponent.tsx +++ b/src/client/app/components/groups/CreateGroupModalComponent.tsx @@ -47,8 +47,7 @@ export default function CreateGroupModalComponent() { const groupDataById = useAppSelector(selectGroupDataById); // Units state const unitsDataById = useAppSelector(selectUnitDataById); - - // Check for admin status + // Which units are possible for graphing state const possibleGraphicUnits = useAppSelector(selectPossibleGraphicUnits) // Since creating group the initial values are effectively nothing or the desired defaults. @@ -217,8 +216,6 @@ export default function CreateGroupModalComponent() { // The input passed validation. // GPS may have been updated so create updated state to submit. const submitState = { ...state, gps: gps }; - console.log('removeMe', submitState) - createGroup(submitState) resetState(); } else { @@ -245,9 +242,7 @@ export default function CreateGroupModalComponent() { meterSelectOptions: possibleMeters, groupSelectOptions: possibleGroups })); - // pik is needed since the compatible units is not correct until pik is available. - // metersState normally does not change but can so include. - // groupState can change if another group is created/edited and this can change ones displayed in menus. + // meters and groups changes will update page due to useAppSelector above. }, [state]); // Update compatible default graphic units set. diff --git a/src/client/app/components/groups/EditGroupModalComponent.tsx b/src/client/app/components/groups/EditGroupModalComponent.tsx index aa2cd68cf..e233bbfa7 100644 --- a/src/client/app/components/groups/EditGroupModalComponent.tsx +++ b/src/client/app/components/groups/EditGroupModalComponent.tsx @@ -324,7 +324,6 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr childGroups: thisGroupState.childGroups, gps: gps, displayable: thisGroupState.displayable, note: thisGroupState.note, area: thisGroupState.area, defaultGraphicUnit: thisGroupState.defaultGraphicUnit, areaUnit: thisGroupState.areaUnit } - console.log(submitState, 'removeme') // This saves group to the DB and then refreshes the window if the last group being updated and // changes were made to the children. This avoid a reload on name change, etc. submitGroupEdits(submitState) diff --git a/src/client/app/components/groups/GroupViewComponent.tsx b/src/client/app/components/groups/GroupViewComponent.tsx index 3b68edccb..103c04668 100644 --- a/src/client/app/components/groups/GroupViewComponent.tsx +++ b/src/client/app/components/groups/GroupViewComponent.tsx @@ -28,7 +28,6 @@ interface GroupViewComponentProps { export default function GroupViewComponent(props: GroupViewComponentProps) { // Don't check if admin since only an admin is allowed to route to this page. - // Edit Modal Show const [showEditModal, setShowEditModal] = useState(false); diff --git a/src/client/app/components/groups/GroupsDetailComponent.tsx b/src/client/app/components/groups/GroupsDetailComponent.tsx index 30646e34a..63ce71469 100644 --- a/src/client/app/components/groups/GroupsDetailComponent.tsx +++ b/src/client/app/components/groups/GroupsDetailComponent.tsx @@ -17,15 +17,12 @@ import GroupViewComponent from './GroupViewComponent'; * @returns Groups page element */ export default function GroupsDetailComponent() { - // Check for admin status const isAdmin = useAppSelector(state => selectIsAdmin(state)); // We only want displayable groups if non-admins because they still have non-displayable in state. const { visibleGroups } = useAppSelector(state => selectVisibleMeterAndGroupData(state)); - - const titleStyle: React.CSSProperties = { textAlign: 'center' }; @@ -52,8 +49,7 @@ export default function GroupsDetailComponent() { {isAdmin &&

{/* The actual button for create is inside this component. */} - < CreateGroupModalComponent - /> + < CreateGroupModalComponent />
} { diff --git a/src/client/app/components/meters/CreateMeterModalComponent.tsx b/src/client/app/components/meters/CreateMeterModalComponent.tsx index 9e0cc35df..720314d8f 100644 --- a/src/client/app/components/meters/CreateMeterModalComponent.tsx +++ b/src/client/app/components/meters/CreateMeterModalComponent.tsx @@ -165,7 +165,6 @@ export default function CreateMeterModalComponent() { }) .catch(err => { // TODO Better way than popup with React but want to stay so user can read/copy. - console.log(err) window.alert(translate('meter.failed.to.create.meter') + '"' + err.data + '"'); }) } else { diff --git a/src/client/app/components/router/ErrorComponent.tsx b/src/client/app/components/router/ErrorComponent.tsx index b9b9d6ea6..b418b1064 100644 --- a/src/client/app/components/router/ErrorComponent.tsx +++ b/src/client/app/components/router/ErrorComponent.tsx @@ -6,6 +6,7 @@ import * as React from 'react'; import { useNavigate } from 'react-router-dom'; import { Button } from 'reactstrap'; import AppLayout from '../../components/AppLayout'; +import translate from '../../utils/translate'; /** * @returns A simple loading spinner used to indicate that the startup init sequence is in progress @@ -21,11 +22,11 @@ export default function ErrorComponent() { }}> {/* TODO make a good looking error page. This is a placeholder for now. */}

- Oops! An error has occurred. + {translate('error.unknown')}

diff --git a/src/client/app/components/router/GraphLinkComponent.tsx b/src/client/app/components/router/GraphLinkComponent.tsx index a5d5aae93..68ebabdc1 100644 --- a/src/client/app/components/router/GraphLinkComponent.tsx +++ b/src/client/app/components/router/GraphLinkComponent.tsx @@ -35,18 +35,11 @@ export const GraphLink = () => { // It is a best practice to reduce the number of dispatch calls, so this logic should be converted into a single reducer for the graphSlice // TODO validation could be implemented across all cases similar to compare period and sorting order switch (key) { - case 'chartType': - dispatchQueue.push(graphSlice.actions.changeChartToRender(value as ChartTypes)) - break; - case 'unitID': - dispatchQueue.push(graphSlice.actions.updateSelectedUnit(parseInt(value))) + case 'areaNormalization': + dispatchQueue.push(graphSlice.actions.setAreaNormalization(value === 'true')) break; - case 'rate': - { - const params = value.split(','); - const rate = { label: params[0], rate: parseFloat(params[1]) } as LineGraphRate; - dispatchQueue.push(graphSlice.actions.updateLineGraphRate(rate)) - } + case 'areaUnit': + dispatchQueue.push(graphSlice.actions.updateSelectedAreaUnit(value as AreaUnitType)) break; case 'barDuration': dispatchQueue.push(graphSlice.actions.updateBarDuration(moment.duration(parseInt(value), 'days'))) @@ -54,14 +47,8 @@ export const GraphLink = () => { case 'barStacking': dispatchQueue.push(graphSlice.actions.setBarStacking(Boolean(value))) break; - case 'areaNormalization': - dispatchQueue.push(graphSlice.actions.setAreaNormalization(value === 'true' ? true : false)) - break; - case 'areaUnit': - dispatchQueue.push(graphSlice.actions.updateSelectedAreaUnit(value as AreaUnitType)) - break; - case 'minMax': - dispatchQueue.push(graphSlice.actions.setShowMinMax(value === 'true' ? true : false)) + case 'chartType': + dispatchQueue.push(graphSlice.actions.changeChartToRender(value as ChartTypes)) break; case 'comparePeriod': dispatchQueue.push(graphSlice.actions.updateComparePeriod({ comparePeriod: validateComparePeriod(value), currentTime: moment() })) @@ -69,33 +56,57 @@ export const GraphLink = () => { case 'compareSortingOrder': dispatchQueue.push(graphSlice.actions.changeCompareSortingOrder(validateSortingOrder(value))) break; - case 'optionsVisibility': - dispatchQueue.push(appStateSlice.actions.setOptionsVisibility(value === 'true' ? true : false)) + case 'groupIDs': + dispatchQueue.push(graphSlice.actions.updateSelectedGroups(value.split(',').map(s => parseInt(s)))) break; case 'mapID': // 'TODO, Verify Behavior & FIXME! MapLink not working as expected dispatch(changeSelectedMap(parseInt(value))) break; - case 'serverRange': - dispatchQueue.push(graphSlice.actions.updateTimeInterval(TimeInterval.fromString(value))); + case 'meterIDs': + dispatchQueue.push(graphSlice.actions.updateSelectedMeters(value.split(',').map(s => parseInt(s)))) break; - case 'sliderRange': - dispatchQueue.push(graphSlice.actions.changeSliderRange(TimeInterval.fromString(value))); + case 'meterOrGroup': + dispatchQueue.push(graphSlice.actions.updateThreeDMeterOrGroup(value as MeterOrGroup)); break; case 'meterOrGroupID': dispatchQueue.push(graphSlice.actions.updateThreeDMeterOrGroupID(parseInt(value))); break; - case 'meterOrGroup': - dispatchQueue.push(graphSlice.actions.updateThreeDMeterOrGroup(value as MeterOrGroup)); + case 'minMax': + dispatchQueue.push(graphSlice.actions.setShowMinMax(value === 'true' ? true : false)) + break; + case 'optionsVisibility': + dispatchQueue.push(appStateSlice.actions.setOptionsVisibility(value === 'true' ? true : false)) + break; + case 'rate': + { + const params = value.split(','); + const rate = { label: params[0], rate: parseFloat(params[1]) } as LineGraphRate; + dispatchQueue.push(graphSlice.actions.updateLineGraphRate(rate)) + } break; case 'readingInterval': dispatchQueue.push(graphSlice.actions.updateThreeDReadingInterval(parseInt(value))); break; - case 'meterIDs': - dispatchQueue.push(graphSlice.actions.updateSelectedMeters(value.split(',').map(s => parseInt(s)))) + case 'serverRange': + dispatchQueue.push(graphSlice.actions.updateTimeInterval(TimeInterval.fromString(value))); + /** + * commented out since days from present feature is not currently used + */ + // const index = info.indexOf('dfp'); + // if (index === -1) { + // options.serverRange = TimeInterval.fromString(info); + // } else { + // const message = info.substring(0, index); + // const stringField = this.getNewIntervalFromMessage(message); + // options.serverRange = TimeInterval.fromString(stringField); + // } break; - case 'groupIDs': - dispatchQueue.push(graphSlice.actions.updateSelectedGroups(value.split(',').map(s => parseInt(s)))) + case 'sliderRange': + dispatchQueue.push(graphSlice.actions.changeSliderRange(TimeInterval.fromString(value))); + break; + case 'unitID': + dispatchQueue.push(graphSlice.actions.updateSelectedUnit(parseInt(value))) break; default: throw new Error('Unknown query parameter'); @@ -109,5 +120,4 @@ export const GraphLink = () => { // All appropriate state updates should've been executed // redirect to root clear the link in the search bar return - -} \ No newline at end of file +} diff --git a/src/client/app/components/router/InitializingComponent.tsx b/src/client/app/components/router/InitializingComponent.tsx index 54493cdd3..e9a1beb76 100644 --- a/src/client/app/components/router/InitializingComponent.tsx +++ b/src/client/app/components/router/InitializingComponent.tsx @@ -4,6 +4,7 @@ import * as React from 'react'; import SpinnerComponent from '../SpinnerComponent'; +import translate from '../../utils/translate'; /** * @returns A simple loading spinner used to indicate that the startup init sequence is in progress @@ -15,7 +16,9 @@ export default function InitializingComponent() { display: 'flex', flexDirection: 'column', alignContent: 'center', alignItems: 'center' }}> -

Initializing

+

+ {translate('initializing')} +

diff --git a/src/client/app/redux/api/conversionsApi.ts b/src/client/app/redux/api/conversionsApi.ts index 1a4e323d1..fd3c6e4c0 100644 --- a/src/client/app/redux/api/conversionsApi.ts +++ b/src/client/app/redux/api/conversionsApi.ts @@ -46,7 +46,6 @@ export const conversionsApi = baseApi.injectEndpoints({ // TODO Verify Behavior w/ Maintainers queryFulfilled .then(() => { - console.log('Refreshing') dispatch(conversionsApi.endpoints.refresh.initiate({ redoCik: true, refreshReadingViews: false })) }) @@ -101,4 +100,4 @@ export const selectPik = createSelector( ({ data: pik = [[]] }) => { return pik } -) \ No newline at end of file +) diff --git a/src/client/app/redux/api/preferencesApi.ts b/src/client/app/redux/api/preferencesApi.ts index d2858f308..797cdd64f 100644 --- a/src/client/app/redux/api/preferencesApi.ts +++ b/src/client/app/redux/api/preferencesApi.ts @@ -22,4 +22,3 @@ export const preferencesApi = baseApi.injectEndpoints({ }) }) }) - diff --git a/src/client/app/redux/selectors/adminSelectors.ts b/src/client/app/redux/selectors/adminSelectors.ts index 323e98ec3..0d448062d 100644 --- a/src/client/app/redux/selectors/adminSelectors.ts +++ b/src/client/app/redux/selectors/adminSelectors.ts @@ -212,7 +212,6 @@ export const selectIsValidConversion = createSelector( (_state: RootState, conversionDetails: ConversionData) => conversionDetails.destinationId, (_state: RootState, conversionDetails: ConversionData) => conversionDetails.bidirectional, (unitDataById, conversions, sourceId, destinationId, bidirectional): [boolean, string] => { - console.log('Validating Conversion Details!') /* Create Conversion Validation: Source equals destination: invalid conversion Conversion exists: invalid conversion @@ -223,7 +222,6 @@ export const selectIsValidConversion = createSelector( Cannot mix unit represent TODO Some of these can go away when we make the menus dynamic. */ - // console.log(sourceId, destinationId, bidirectional) // The destination cannot be a meter unit. if (destinationId !== -999 && unitDataById[destinationId].typeOfUnit === UnitType.meter) { @@ -234,7 +232,7 @@ export const selectIsValidConversion = createSelector( // Source or destination not set if (sourceId == -999 || destinationId == -999) { // TODO Translate Me! - return [false, 'Source or destination not set'] + return [false, translate('conversion.create.source.destination.not')]; } // Conversion already exists @@ -252,8 +250,6 @@ export const selectIsValidConversion = createSelector( return [false, translate('conversion.create.mixed.represent')]; } - - console.log('Seems to Break about here!') // If there is a non bidirectional inverse, then it is a valid conversion for (const conversion of Object.values(conversions)) { @@ -267,7 +263,6 @@ export const selectIsValidConversion = createSelector( } } - console.log('Conversion never seems to get here? ') return [true, 'Conversion is Valid'] } ) @@ -347,4 +342,4 @@ export const selectDefaultCreateConversionValues = createSelector( } return defaultValues } -) \ No newline at end of file +) diff --git a/src/client/app/redux/slices/currentUserSlice.ts b/src/client/app/redux/slices/currentUserSlice.ts index aa71e33fa..d9ef0563d 100644 --- a/src/client/app/redux/slices/currentUserSlice.ts +++ b/src/client/app/redux/slices/currentUserSlice.ts @@ -56,8 +56,8 @@ export const currentUserSlice = createSlice({ selectors: { selectCurrentUser: state => state, selectCurrentUserRole: state => state.profile?.role, - selectIsAdmin: state => Boolean(state.token && state.profile?.role === UserRole.ADMIN) // Should resolve to a boolean, Typescript doesn't agree so type assertion 'as boolean' + selectIsAdmin: state => Boolean(state.token && state.profile?.role === UserRole.ADMIN) } }) diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 804ae9fb4..482c9d34d 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -54,6 +54,7 @@ const LocaleTranslationData = { "conversion.create.exists": "This conversion already exists", "conversion.create.exists.inverse": "This conversion already exists where one is not bidirectional", "conversion.create.mixed.represent": "A conversion cannot mix units of quantity, flow and raw", + "conversion.create.source.destination.not": "Source or destination not set", "conversion.create.source.destination.same": "The source and destination cannot be the same for a conversion", "conversion.delete.conversion": "Delete Conversion", "conversion.destination": "Destination:", @@ -149,6 +150,7 @@ const LocaleTranslationData = { "error.gps": "Latitude must be between -90 and 90, and Longitude must be between -180 and 180.", "error.negative": "Cannot be negative.", "error.required": "Required field.", + "error.unknown": "Oops! An error has occurred.", "edit": "Edit", "edited": "edited", "edit.a.group": "Edit a Group", @@ -261,6 +263,7 @@ const LocaleTranslationData = { "incompatible.units": "Incompatible Units", "increasing": "increasing", "info": " for more information. ", + "initializing": "Initializing", "input.gps.coords.first": "input GPS coordinate that corresponds to the point: ", "input.gps.coords.second": "in this format -> latitude,longitude", "input.gps.range": "Invalid GPS coordinate, latitude must be an integer between -90 and 90, longitude must be an integer between -180 and 180. You input: ", @@ -381,6 +384,7 @@ const LocaleTranslationData = { "redraw": "Redraw", "remove": "Remove", "restore": "Restore", + "return.dashboard": "Return To Dashboard", "role": "Role", "save.all": "Save all", "save.map.edits": "Save map edits", @@ -524,6 +528,7 @@ const LocaleTranslationData = { "conversion.create.exists": "(Need French) This conversion already exists", "conversion.create.exists.inverse": "(Need French) This conversion already exists where one is not bidirectional", "conversion.create.mixed.represent": "(Need French) A conversion cannot mix units of quantity, flow and raw", + "conversion.create.source.destination.not": "(Need French) Source or destination not set", "conversion.create.source.destination.same": "(Need French) The source and destination cannot be the same for a conversion", "conversion.delete.conversion": "(Need French) Delete Conversion", "conversion.destination": "(Need French) Destination:", @@ -619,6 +624,7 @@ const LocaleTranslationData = { "error.gps": "(Need French) Latitude must be between -90 and 90, and Longitude must be between -180 and 180.", "error.negative": "(Need French) Cannot be negative.", "error.required": "(Need French) Required field.", + "error.unknown": "(Need French) Oops! An error has occurred.", "edit": "Modifier", "edited": "édité", "edit.a.group": "Modifier le Groupe", @@ -731,6 +737,7 @@ const LocaleTranslationData = { "incompatible.units": "(Need French) Incompatible Units", "increasing": "(Need French) increasing", "info": " pour plus d'informations. ", + "initializing": "(Need French) Initializing", "input.gps.coords.first": "(Need French) input GPS coordinate that corresponds to the point: ", "input.gps.coords.second": "(Need French) in this format -> latitude,longitude", "input.gps.range": "Coordonnée GPS invalide, la latitude doit être un nombre entier entre -90 et 90, la longitude doit être un nombre entier entre -180 et 180. (Need French)You input: ", @@ -851,6 +858,7 @@ const LocaleTranslationData = { "redraw": "Redessiner", "remove": "(need French) Remove", "restore": "Restaurer", + "return.dashboard": "(Need French) Return To Dashboard", "role": "(Need French) Role", "save.all": "(Need French) Save all", "save.map.edits": "(Need French) Save map edits", @@ -994,6 +1002,7 @@ const LocaleTranslationData = { "conversion.create.exists": "(Need Spanish) This conversion already exists", "conversion.create.exists.inverse": "(Need Spanish) This conversion already exists where one is not bidirectional", "conversion.create.mixed.represent": "(Need Spanish) A conversion cannot mix units of quantity, flow and raw", + "conversion.create.source.destination.not": "(Need Spanish) Source or destination not set", "conversion.create.source.destination.same": "(Need Spanish) The source and destination cannot be the same for a conversion", "conversion.delete.conversion": "(Need Spanish) Delete Conversion", "conversion.destination": "(Need Spanish) Destination:", @@ -1089,6 +1098,7 @@ const LocaleTranslationData = { "error.gps": "(Need Spanish) Latitude must be between -90 and 90, and Longitude must be between -180 and 180.", "error.negative": "(Need Spanish) Cannot be negative.", "error.required": "(Need Spanish) Required field.", + "error.unknown": "(Need Spanish) Oops! An error has occurred.", "edit": "Editar", "edited": "Editado", "edit.a.group": "Editar un grupo", @@ -1201,6 +1211,7 @@ const LocaleTranslationData = { "incompatible.units": "(Need Spanish) Incompatible Units", "increasing": "(need Spanish) increasing", "info": " para obtener más información. ", + "initializing": "(need Spanish) Initializing", "input.gps.coords.first": "Entrada el cordenata de GPS que corresponde al punto: ", "input.gps.coords.second": "En este forma -> latitud, longitud", "input.gps.range": "Coordenada GPS no válida, la latitud debe ser un número entero entre -90 y 90, la longitud debe ser un número entero entre -180 y 180. (need Spanish). You input: ", @@ -1321,6 +1332,7 @@ const LocaleTranslationData = { "redraw": "Redibujar", "remove": "Eliminar", "restore": "Restaurar", + "return.dashboard": "(Need Spanish) Return To Dashboard", "role": "Rol", "save.all": "Guardar todos", "save.map.edits": "Guardar las ediciones del mapa", @@ -1425,4 +1437,4 @@ export type TranslationKey = keyof typeof LocaleTranslationData export type LocaleDataKey = keyof typeof LocaleTranslationData['en'] | keyof typeof LocaleTranslationData['es'] | - keyof typeof LocaleTranslationData['fr'] \ No newline at end of file + keyof typeof LocaleTranslationData['fr'] From 3bad170c3be9ec25a56c1da879fc9c8888e138fc Mon Sep 17 00:00:00 2001 From: Steven Huss-Lederman Date: Wed, 31 Jan 2024 11:08:10 -0600 Subject: [PATCH 057/131] use Toast msgs + other - Adds warn and info Toast msgs. - Removes window.alert. - Replaces notifyUser and removes from code. - Does not address window.confirm. - Some basic edits to CreateMeterModalComponent. --- src/client/app/components/ExportComponent.tsx | 5 +- .../csv/MetersCSVUploadComponent.tsx | 5 +- .../csv/ReadingsCSVUploadComponent.tsx | 14 ++--- .../groups/CreateGroupModalComponent.tsx | 19 +++---- .../groups/EditGroupModalComponent.tsx | 23 +++++---- .../maps/MapCalibrationInitiateComponent.tsx | 17 ++++--- .../app/components/maps/MapViewComponent.tsx | 13 ++--- .../meters/CreateMeterModalComponent.tsx | 51 ++++++------------- .../meters/EditMeterModalComponent.tsx | 11 ++-- .../unit/EditUnitModalComponent.tsx | 12 ++--- src/client/app/redux/actions/groups.ts | 2 +- src/client/app/redux/actions/meters.ts | 10 ++-- .../app/redux/selectors/adminSelectors.ts | 3 -- src/client/app/utils/calibration.ts | 5 +- src/client/app/utils/input.ts | 12 ----- src/client/app/utils/notifications.ts | 34 +++++++++++++ 16 files changed, 123 insertions(+), 113 deletions(-) diff --git a/src/client/app/components/ExportComponent.tsx b/src/client/app/components/ExportComponent.tsx index 500d0df58..9227b7565 100644 --- a/src/client/app/components/ExportComponent.tsx +++ b/src/client/app/components/ExportComponent.tsx @@ -6,6 +6,7 @@ import * as _ from 'lodash'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { Button } from 'reactstrap'; +import { showErrorNotification } from '../utils/notifications'; import { selectConversionsDetails } from '../redux/api/conversionsApi'; import { selectGroupDataById } from '../redux/api/groupsApi'; import { selectMeterDataById } from '../redux/api/metersApi'; @@ -13,6 +14,7 @@ import { readingsApi } from '../redux/api/readingsApi'; import { selectUnitDataById } from '../redux/api/unitsApi'; import { useAppSelector } from '../redux/reduxHooks'; import { selectAllChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; +import { selectGraphState, selectShowMinMax } from '../redux/slices/graphSlice'; import { UserRole } from '../types/items'; import { ConversionData } from '../types/redux/conversions'; import { ChartTypes, MeterOrGroup } from '../types/redux/graph'; @@ -23,7 +25,6 @@ import { barUnitLabel, lineUnitLabel } from '../utils/graphics'; import { hasToken } from '../utils/token'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; -import { selectGraphState, selectShowMinMax } from '../redux/slices/graphSlice'; /** * Creates export buttons and does code for handling export to CSV files. @@ -217,7 +218,7 @@ export default function ExportComponent() { // User not allowed to download. const msg = translate('csv.download.size.warning.size') + ` ${fileSize.toFixed(2)}MB. ` + translate('csv.download.size.limit'); - window.alert(msg); + showErrorNotification(msg); } } else { // Anyone can download if they approve diff --git a/src/client/app/components/csv/MetersCSVUploadComponent.tsx b/src/client/app/components/csv/MetersCSVUploadComponent.tsx index 15ef2e485..9a8e0d1d5 100644 --- a/src/client/app/components/csv/MetersCSVUploadComponent.tsx +++ b/src/client/app/components/csv/MetersCSVUploadComponent.tsx @@ -7,6 +7,7 @@ import { FormattedMessage } from 'react-intl'; import { Button, Form, FormGroup, Input, Label } from 'reactstrap'; import { MODE } from '../../containers/csv/UploadCSVContainer'; import { MetersCSVUploadProps } from '../../types/csvUploadForm'; +import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; import FormFileUploaderComponent from '../FormFileUploaderComponent'; export default class MetersCSVUploadComponent extends React.Component { @@ -28,11 +29,11 @@ export default class MetersCSVUploadComponent extends React.ComponentSUCCESSThe meter upload was a success.'); + showSuccessNotification('

SUCCESS

The meter upload was a success.'); } } catch (error) { // A failed axios request should result in an error. - window.alert(error.response.data as string); + showErrorNotification(error.response.data as string); } // Refetch meters details. // Removed with rtk migration diff --git a/src/client/app/components/csv/ReadingsCSVUploadComponent.tsx b/src/client/app/components/csv/ReadingsCSVUploadComponent.tsx index 1f5fae9cf..e2e94ec03 100644 --- a/src/client/app/components/csv/ReadingsCSVUploadComponent.tsx +++ b/src/client/app/components/csv/ReadingsCSVUploadComponent.tsx @@ -3,13 +3,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { Button, Col, Input, Form, FormGroup, Label } from 'reactstrap'; -import { ReadingsCSVUploadProps, TimeSortTypes, BooleanMeterTypes } from '../../types/csvUploadForm'; -import { ReadingsCSVUploadDefaults } from '../../utils/csvUploadDefaults'; -import FormFileUploaderComponent from '../FormFileUploaderComponent'; import { FormattedMessage } from 'react-intl'; +import { Button, Col, Form, FormGroup, Input, Label } from 'reactstrap'; import { MODE } from '../../containers/csv/UploadCSVContainer'; +import { BooleanMeterTypes, ReadingsCSVUploadProps, TimeSortTypes } from '../../types/csvUploadForm'; +import { ReadingsCSVUploadDefaults } from '../../utils/csvUploadDefaults'; +import { showErrorNotification, showInfoNotification } from '../../utils/notifications'; import translate from '../../utils/translate'; +import FormFileUploaderComponent from '../FormFileUploaderComponent'; /** * Returns a range of values between the specified lower and upper bounds. @@ -63,13 +64,14 @@ export default class ReadingsCSVUploadComponent extends React.Component { private readonly fileInput: any; private notifyBadNumber() { - window.alert(`${this.props.intl.formatMessage({id: 'map.bad.number'})}`); + showErrorNotification(`${this.props.intl.formatMessage({id: 'map.bad.number'})}`); } private notifyBadDigit360() { - window.alert(`${this.props.intl.formatMessage({id: 'map.bad.digita'})}`); + showErrorNotification(`${this.props.intl.formatMessage({id: 'map.bad.digita'})}`); } private notifyBadDigit0() { - window.alert(`${this.props.intl.formatMessage({id: 'map.bad.digitb'})}`); + showErrorNotification(`${this.props.intl.formatMessage({id: 'map.bad.digitb'})}`); } private notifyBadMapLoad() { - window.alert(`${this.props.intl.formatMessage({id: 'map.bad.load'})}`); + showErrorNotification(`${this.props.intl.formatMessage({id: 'map.bad.load'})}`); } private notifyBadName() { - window.alert(`${this.props.intl.formatMessage({id: 'map.bad.name'})}`); + showErrorNotification(`${this.props.intl.formatMessage({id: 'map.bad.name'})}`); } constructor(props: MapInitiatePropsWithIntl) { @@ -180,4 +181,4 @@ class MapCalibrationInitiateComponent extends React.Component; - possibleGraphicUnits: Set; -} /** * Defines the create meter modal form @@ -39,14 +29,11 @@ export interface CreateMeterModalComponentProps { export default function CreateMeterModalComponent() { const [addMeter] = metersApi.endpoints.addMeter.useMutation() - // Admin state so can get the default reading frequency. // Memo'd memoized selector const selectGraphicUnitCompatibility = React.useMemo(makeSelectGraphicUnitCompatibility, []) const defaultValues = useAppSelector(selectDefaultCreateMeterValues) /* State */ - // To make this consistent with EditUnitModalComponent, we don't pass show and close via props - // even this one does have other props. // Modal show const [showModal, setShowModal] = useState(false); @@ -54,12 +41,13 @@ export default function CreateMeterModalComponent() { // Handlers for each type of input change const [meterDetails, setMeterDetails] = useState(defaultValues); const { - incompatibleGraphicUnits, compatibleGraphicUnits, + incompatibleGraphicUnits, compatibleUnits, incompatibleUnits // Type assertion due to conflicting GPS Property - } = useAppSelector(state => selectGraphicUnitCompatibility(state, meterDetails as unknown as MeterData)) + } = useAppSelector(state => selectGraphicUnitCompatibility(state, meterDetails as unknown as MeterData)); + const handleShow = () => setShowModal(true); const handleStringChange = (e: React.ChangeEvent) => { @@ -79,8 +67,8 @@ export default function CreateMeterModalComponent() { } // Dropdowns - const [selectedUnitId, setSelectedUnitId] = useState(false); const [selectedGraphicId, setSelectedGraphicId] = useState(false); + const [selectedUnitId, setSelectedUnitId] = useState(false); const [validMeter, setValidMeter] = useState(false); @@ -91,11 +79,10 @@ export default function CreateMeterModalComponent() { /* End State */ // Reset the state to default values - // This would also benefit from a single state changing function for all state const resetState = () => { setMeterDetails(defaultValues); - setSelectedGraphicId(false) - setSelectedUnitId(false) + setSelectedGraphicId(false); + setSelectedUnitId(false); } const handleClose = () => { @@ -126,7 +113,6 @@ export default function CreateMeterModalComponent() { // null came from the DB and it is okay to just leave it - Not a string. if (typeof gpsInput === 'string') { if (isValidGPSInput(gpsInput)) { - // Clearly gpsInput is a string but TS complains about the split so cast. const gpsValues = gpsInput.split(',').map(value => parseFloat(value)); // It is valid and needs to be in this format for routing. gps = { @@ -137,22 +123,22 @@ export default function CreateMeterModalComponent() { // GPS not okay. Only true if some input. // TODO isValidGPSInput currently pops up an alert so not doing it here, may change // so leaving code commented out. - // notifyUser(translate('input.gps.range') + state.gps + '.'); + // showErrorNotification(translate('input.gps.range') + state.gps + '.'); inputOk = false; } } if (inputOk) { // The input passed validation. - // The default value for timeZone is an empty string but that should be null for DB. - // See below for usage of timeZoneValue. - // GPS may have been updated so create updated state to submit. const submitState = { ...meterDetails, + // GPS may have been updated so create updated state to submit. gps: gps, - timeZone: (meterDetails.timeZone == '' ? null : meterDetails.timeZone), // Set default identifier as name if left blank - identifier: !meterDetails.identifier || meterDetails.identifier.length === 0 ? meterDetails.name : meterDetails.identifier + identifier: !meterDetails.identifier || meterDetails.identifier.length === 0 ? meterDetails.name : meterDetails.identifier, + // The default value for timeZone is an empty string but that should be null for DB. + // See below for usage of timeZoneValue. + timeZone: (meterDetails.timeZone == '' ? null : meterDetails.timeZone) }; // Submit new meter if checks where ok. // Attempt to add meter to database @@ -164,12 +150,11 @@ export default function CreateMeterModalComponent() { resetState(); }) .catch(err => { - // TODO Better way than popup with React but want to stay so user can read/copy. - window.alert(translate('meter.failed.to.create.meter') + '"' + err.data + '"'); + showErrorNotification(translate('meter.failed.to.create.meter') + '"' + err.data + '"'); }) } else { // Tell user that not going to update due to input issues. - notifyUser(translate('meter.input.error')); + showErrorNotification(translate('meter.input.error')); } }; @@ -751,7 +736,6 @@ export default function CreateMeterModalComponent() { ); } - /* Create Meter Validation: Name cannot be blank Area must be positive or zero @@ -791,9 +775,6 @@ const isValidCreateMeter = (meterDetails: MeterData) => { (meterDetails.maxError >= 0 && meterDetails.maxError <= MAX_ERRORS) } - - - const MIN_VAL = Number.MIN_SAFE_INTEGER; const MAX_VAL = Number.MAX_SAFE_INTEGER; const MIN_DATE_MOMENT = moment(0).utc(); diff --git a/src/client/app/components/meters/EditMeterModalComponent.tsx b/src/client/app/components/meters/EditMeterModalComponent.tsx index 47022b9ae..7453f20ad 100644 --- a/src/client/app/components/meters/EditMeterModalComponent.tsx +++ b/src/client/app/components/meters/EditMeterModalComponent.tsx @@ -8,7 +8,6 @@ import * as React from 'react'; import { useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; -import TooltipHelpComponent from '../TooltipHelpComponent'; import { metersApi, selectMeterById } from '../../redux/api/metersApi'; import { selectUnitDataById } from '../../redux/api/unitsApi'; import { useAppSelector } from '../../redux/reduxHooks'; @@ -20,9 +19,11 @@ import { MeterData, MeterTimeSortType, MeterType } from '../../types/redux/meter import { UnitRepresentType } from '../../types/redux/units'; import { GPSPoint, isValidGPSInput } from '../../utils/calibration'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; -import { getGPSString, notifyUser, nullToEmptyString } from '../../utils/input'; +import { getGPSString, nullToEmptyString } from '../../utils/input'; +import { showErrorNotification } from '../../utils/notifications'; import translate from '../../utils/translate'; import TimeZoneSelect from '../TimeZoneSelect'; +import TooltipHelpComponent from '../TooltipHelpComponent'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; interface EditMeterModalComponentProps { @@ -105,7 +106,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr // GPS not okay. // TODO isValidGPSInput currently tops up an alert so not doing it here, may change // so leaving code commented out. - // notifyUser(translate('input.gps.range') + state.gps + '.'); + // showErrorNotification(translate('input.gps.range') + state.gps + '.'); inputOk = false; } } @@ -130,7 +131,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr editMeter({ meterData: submitState, shouldRefreshViews: shouldRefreshReadingViews }) } else { // Tell user that not going to update due to input issues. - notifyUser(translate('meter.input.error')); + showErrorNotification(translate('meter.input.error')); } } }; @@ -742,4 +743,4 @@ const isValidMeter = (localMeterEdits: MeterData) => { moment(localMeterEdits.minDate).isSameOrBefore(moment(localMeterEdits.maxDate)) && moment(localMeterEdits.maxDate).isSameOrBefore(MAX_DATE_MOMENT) && (localMeterEdits.maxError >= 0 && localMeterEdits.maxError <= MAX_ERRORS) -} \ No newline at end of file +} diff --git a/src/client/app/components/unit/EditUnitModalComponent.tsx b/src/client/app/components/unit/EditUnitModalComponent.tsx index f01be808c..0aa3ce82b 100644 --- a/src/client/app/components/unit/EditUnitModalComponent.tsx +++ b/src/client/app/components/unit/EditUnitModalComponent.tsx @@ -10,16 +10,16 @@ import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, M import TooltipHelpComponent from '../../components/TooltipHelpComponent'; import { selectConversionsDetails } from '../../redux/api/conversionsApi'; import { selectMeterDataById } from '../../redux/api/metersApi'; +import { unitsApi } from '../../redux/api/unitsApi'; +import { useTranslate } from '../../redux/componentHooks'; import { useAppSelector } from '../../redux/reduxHooks'; import '../../styles/modal.css'; import { tooltipBaseStyle } from '../../styles/modalStyle'; import { TrueFalseType } from '../../types/items'; import { DisplayableType, UnitData, UnitRepresentType, UnitType } from '../../types/redux/units'; -import { notifyUser } from '../../utils/input'; +import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; -import { unitsApi } from '../../redux/api/unitsApi'; -import { showSuccessNotification, showErrorNotification } from '../../utils/notifications'; -import { useTranslate } from '../../redux/componentHooks'; + interface EditUnitModalComponentProps { show: boolean; unit: UnitData; @@ -112,7 +112,7 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp const meter = meters.find(m => m.unitId === props.unit.id); if (meter) { // There exists a meter that is still linked with this unit - notifyUser(`${translate('the.unit.of.meter')} ${meter.name} ${translate('meter.unit.change.requires')}`); + showErrorNotification(`${translate('the.unit.of.meter')} ${meter.name} ${translate('meter.unit.change.requires')}`); inputOk = false; } } @@ -130,7 +130,7 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp || props.unit.note != state.note; } else { // Tell user that not going to update due to input issues. - notifyUser(`${translate('unit.input.error')}`); + showErrorNotification(`${translate('unit.input.error')}`); return false; } } diff --git a/src/client/app/redux/actions/groups.ts b/src/client/app/redux/actions/groups.ts index b86144fad..024706eca 100644 --- a/src/client/app/redux/actions/groups.ts +++ b/src/client/app/redux/actions/groups.ts @@ -124,7 +124,7 @@ // } catch (err) { // // Failure! ): // // TODO Better way than popup with React but want to stay so user can read/copy. -// window.alert(translate('group.failed.to.edit.group') + '"' + err.response.data as string + '"'); +// showErrorNotification(translate('group.failed.to.edit.group') + '"' + err.response.data as string + '"'); // // Clear our changes from to the submitting meters state // // We must do this in case fetch failed to keep the store in sync with the database // } diff --git a/src/client/app/redux/actions/meters.ts b/src/client/app/redux/actions/meters.ts index e68f47ef5..746189e44 100644 --- a/src/client/app/redux/actions/meters.ts +++ b/src/client/app/redux/actions/meters.ts @@ -10,11 +10,11 @@ // @ts-nocheck /* eslint-disable jsdoc/require-param */ -import { Thunk, Dispatch, GetState } from '../../types/redux/actions'; -import { showSuccessNotification } from '../../utils/notifications'; -import translate from '../../utils/translate'; +import { Dispatch, GetState, Thunk } from '../../types/redux/actions'; import * as t from '../../types/redux/meters'; import { metersApi } from '../../utils/api'; +import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; +import translate from '../../utils/translate'; import { updateCikAndDBViewsIfNeeded } from './admin'; @@ -72,7 +72,7 @@ export function submitEditedMeter(editedMeter: t.MeterData, shouldRefreshReading } catch (err) { // Failure! ): // TODO Better way than popup with React but want to stay so user can read/copy. - window.alert(translate('meter.failed.to.edit.meter') + '"' + err.response.data as string + '"'); + showErrorNotification(translate('meter.failed.to.edit.meter') + '"' + err.response.data as string + '"'); // Clear our changes from to the submitting meters state // We must do this in case fetch failed to keep the store in sync with the database dispatch(metersSlice.actions.deleteSubmittedMeter(editedMeter.id)); @@ -93,7 +93,7 @@ export function addMeter(meter: t.MeterData): Thunk { showSuccessNotification(translate('meter.successfully.create.meter')); } catch (err) { // TODO Better way than popup with React but want to stay so user can read/copy. - window.alert(translate('meter.failed.to.create.meter') + '"' + err.response.data as string + '"'); + showErrorNotification(translate('meter.failed.to.create.meter') + '"' + err.response.data as string + '"'); } } } diff --git a/src/client/app/redux/selectors/adminSelectors.ts b/src/client/app/redux/selectors/adminSelectors.ts index 0d448062d..dde15e1e1 100644 --- a/src/client/app/redux/selectors/adminSelectors.ts +++ b/src/client/app/redux/selectors/adminSelectors.ts @@ -225,7 +225,6 @@ export const selectIsValidConversion = createSelector( // The destination cannot be a meter unit. if (destinationId !== -999 && unitDataById[destinationId].typeOfUnit === UnitType.meter) { - // notifyUser(translate('conversion.create.destination.meter')); return [false, translate('conversion.create.destination.meter')]; } @@ -239,14 +238,12 @@ export const selectIsValidConversion = createSelector( if ((conversions.findIndex(conversion => (( conversion.sourceId === sourceId) && conversion.destinationId === destinationId))) !== -1) { - // notifyUser(translate('conversion.create.exists')); return [false, translate('conversion.create.exists')]; } // You cannot have a conversion between units that differ in unit_represent. // This means you cannot mix quantity, flow & raw. if (unitDataById[sourceId].unitRepresent !== unitDataById[destinationId].unitRepresent) { - // notifyUser(translate('conversion.create.mixed.represent')); return [false, translate('conversion.create.mixed.represent')]; } diff --git a/src/client/app/utils/calibration.ts b/src/client/app/utils/calibration.ts index 489401525..9a0b949c3 100644 --- a/src/client/app/utils/calibration.ts +++ b/src/client/app/utils/calibration.ts @@ -2,9 +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 { MapMetadata } from '../types/redux/map'; +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'; /** @@ -121,7 +122,7 @@ export function isValidGPSInput(input: string): boolean { const result = latitudeConstraint && longitudeConstraint; if (!result) { // TODO It would be nice to return the error and then notify as desired. - window.alert(translate('input.gps.range') + input); + showErrorNotification(translate('input.gps.range') + input); } return result; } diff --git a/src/client/app/utils/input.ts b/src/client/app/utils/input.ts index bd86b3342..dd587e508 100644 --- a/src/client/app/utils/input.ts +++ b/src/client/app/utils/input.ts @@ -7,18 +7,6 @@ import { UnitData, DisplayableType, UnitRepresentType, UnitType, UnitDataById } import translate from './translate'; import * as _ from 'lodash'; -// Notifies user of msg. -// TODO isValidGPSInput uses alert so continue that. Maybe all should be changed but this impacts other parts of the code. -// Note this causes the modal to close but the state is not reset. -// Use a function so can easily change how it works. -/** - * Causes a window popup with msg - * @param msg message to display - */ -export function notifyUser(msg: string) { - window.alert(msg); -} - /** * get string value from GPSPoint or null. * @param gps GPS point to get value from and can be null diff --git a/src/client/app/utils/notifications.ts b/src/client/app/utils/notifications.ts index 7f55ae8ff..52ac86229 100644 --- a/src/client/app/utils/notifications.ts +++ b/src/client/app/utils/notifications.ts @@ -21,6 +21,40 @@ export function showSuccessNotification(message: string, position: ToastPosition }); } +/** + * Show user the information notification + * @param message translation identifier for message to display + * @param position screen position for notification where top, right is the default + * @param autoDismiss milliseconds until notification goes away with default of 7 seconds + */ +export function showInfoNotification(message: string, position: ToastPosition = toast.POSITION.TOP_RIGHT, autoDismiss = 7000) { + toast.info(message, { + position: position, + autoClose: autoDismiss, + hideProgressBar: true, + pauseOnHover: false, + draggable: true, + theme: 'colored' + }); +} + +/** + * Show user the warning notification + * @param message translation identifier for message to display + * @param position screen position for notification where top, right is the default + * @param autoDismiss milliseconds until notification goes away with default of 10 seconds + */ +export function showWarnNotification(message: string, position: ToastPosition = toast.POSITION.TOP_RIGHT, autoDismiss = 10000) { + toast.warn(message, { + position: position, + autoClose: autoDismiss, + hideProgressBar: true, + pauseOnHover: false, + draggable: true, + theme: 'colored' + }); +} + /** * Show user the error notification * @param message translation identifier for message to display From 1ab2f02ecd15242938cf68a9d7ddc077a7eeebfa Mon Sep 17 00:00:00 2001 From: Steven Huss-Lederman Date: Sun, 4 Feb 2024 16:33:04 -0600 Subject: [PATCH 058/131] basic edits Does components for meter, router & unit and componentHooks. --- .../components/meters/CreateMeterModalComponent.tsx | 8 ++++---- .../components/meters/EditMeterModalComponent.tsx | 13 ++++++------- .../app/components/meters/MeterViewComponent.tsx | 2 +- .../app/components/meters/MetersDetailComponent.tsx | 4 +--- src/client/app/components/router/ErrorComponent.tsx | 2 +- .../app/components/router/GraphLinkComponent.tsx | 2 +- src/client/app/components/router/RoleOutlet.tsx | 3 +-- .../components/unit/CreateUnitModalComponent.tsx | 1 + .../app/components/unit/EditUnitModalComponent.tsx | 2 +- .../app/components/unit/UnitsDetailComponent.tsx | 1 + src/client/app/redux/componentHooks.ts | 4 +--- 11 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/client/app/components/meters/CreateMeterModalComponent.tsx b/src/client/app/components/meters/CreateMeterModalComponent.tsx index 167c01d70..e3ddabfc0 100644 --- a/src/client/app/components/meters/CreateMeterModalComponent.tsx +++ b/src/client/app/components/meters/CreateMeterModalComponent.tsx @@ -28,10 +28,10 @@ import TooltipMarkerComponent from '../TooltipMarkerComponent'; */ export default function CreateMeterModalComponent() { - const [addMeter] = metersApi.endpoints.addMeter.useMutation() + const [addMeter] = metersApi.endpoints.addMeter.useMutation(); // Memo'd memoized selector - const selectGraphicUnitCompatibility = React.useMemo(makeSelectGraphicUnitCompatibility, []) - const defaultValues = useAppSelector(selectDefaultCreateMeterValues) + const selectGraphicUnitCompatibility = React.useMemo(makeSelectGraphicUnitCompatibility, []); + const defaultValues = useAppSelector(selectDefaultCreateMeterValues); /* State */ // Modal show @@ -772,7 +772,7 @@ const isValidCreateMeter = (meterDetails: MeterData) => { moment(meterDetails.minDate).isSameOrAfter(MIN_DATE_MOMENT) && moment(meterDetails.minDate).isSameOrBefore(moment(meterDetails.maxDate)) && moment(meterDetails.maxDate).isSameOrBefore(MAX_DATE_MOMENT) && - (meterDetails.maxError >= 0 && meterDetails.maxError <= MAX_ERRORS) + (meterDetails.maxError >= 0 && meterDetails.maxError <= MAX_ERRORS); } const MIN_VAL = Number.MIN_SAFE_INTEGER; diff --git a/src/client/app/components/meters/EditMeterModalComponent.tsx b/src/client/app/components/meters/EditMeterModalComponent.tsx index 7453f20ad..342453f7a 100644 --- a/src/client/app/components/meters/EditMeterModalComponent.tsx +++ b/src/client/app/components/meters/EditMeterModalComponent.tsx @@ -38,10 +38,10 @@ interface EditMeterModalComponentProps { * @returns Meter edit element */ export default function EditMeterModalComponent(props: EditMeterModalComponentProps) { - const [editMeter] = metersApi.useEditMeterMutation() + const [editMeter] = metersApi.useEditMeterMutation(); // since this selector is shared amongst many other modals, we must use a selector factory in order // to have a single selector per modal instance. Memo ensures that this is a stable reference - const selectGraphicUnitCompatibility = React.useMemo(makeSelectGraphicUnitCompatibility, []) + const selectGraphicUnitCompatibility = React.useMemo(makeSelectGraphicUnitCompatibility, []); // The current meter's state of meter being edited. It should always be valid. const meterState = useAppSelector(state => selectMeterById(state, props.meter.id)); const [localMeterEdits, setLocalMeterEdits] = useState(_.cloneDeep(meterState)); @@ -50,9 +50,9 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr incompatibleGraphicUnits, compatibleUnits, incompatibleUnits - } = useAppSelector(state => selectGraphicUnitCompatibility(state, localMeterEdits)) + } = useAppSelector(state => selectGraphicUnitCompatibility(state, localMeterEdits)); - useEffect(() => { setLocalMeterEdits(_.cloneDeep(meterState)) }, [meterState]) + useEffect(() => { setLocalMeterEdits(_.cloneDeep(meterState)) }, [meterState]); /* State */ // unit state const unitDataById = useAppSelector(selectUnitDataById); @@ -127,7 +127,6 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr (unitDataById[props.meter.unitId].unitRepresent != UnitRepresentType.quantity && unitDataById[localMeterEdits.unitId].unitRepresent == UnitRepresentType.quantity)); // Submit new meter if checks where ok. - // dispatch(submitEditedMeter(submitState, shouldRefreshReadingViews) as ThunkAction); editMeter({ meterData: submitState, shouldRefreshViews: shouldRefreshReadingViews }) } else { // Tell user that not going to update due to input issues. @@ -707,7 +706,7 @@ const MAX_DATE = MAX_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); const MAX_ERRORS = 75; const tooltipStyle = { ...tooltipBaseStyle, - // Only and admin can edit a meter. + // Only an admin can edit a meter. tooltipEditMeterView: 'help.admin.meteredit' }; @@ -742,5 +741,5 @@ const isValidMeter = (localMeterEdits: MeterData) => { moment(localMeterEdits.minDate).isSameOrAfter(MIN_DATE_MOMENT) && moment(localMeterEdits.minDate).isSameOrBefore(moment(localMeterEdits.maxDate)) && moment(localMeterEdits.maxDate).isSameOrBefore(MAX_DATE_MOMENT) && - (localMeterEdits.maxError >= 0 && localMeterEdits.maxError <= MAX_ERRORS) + (localMeterEdits.maxError >= 0 && localMeterEdits.maxError <= MAX_ERRORS); } diff --git a/src/client/app/components/meters/MeterViewComponent.tsx b/src/client/app/components/meters/MeterViewComponent.tsx index 7d608a799..47777cd60 100644 --- a/src/client/app/components/meters/MeterViewComponent.tsx +++ b/src/client/app/components/meters/MeterViewComponent.tsx @@ -33,7 +33,7 @@ export default function MeterViewComponent(props: MeterViewComponentProps) { // Set up to display the units associated with the meter as the unit identifier. // This is the unit associated with the meter. const unitName = useAppSelector(state => selectUnitName(state, props.meter.id)) - // This is the default graphic unit associated with the meter. See above for how code works. + // This is the default graphic unit name associated with the meter. const graphicName = useAppSelector(state => selectGraphicName(state, props.meter.id)) const handleShow = () => { setShowEditModal(true); diff --git a/src/client/app/components/meters/MetersDetailComponent.tsx b/src/client/app/components/meters/MetersDetailComponent.tsx index 2aff93444..b7cf57377 100644 --- a/src/client/app/components/meters/MetersDetailComponent.tsx +++ b/src/client/app/components/meters/MetersDetailComponent.tsx @@ -63,12 +63,10 @@ const titleStyle: React.CSSProperties = { textAlign: 'center' }; - - const tooltipStyle = { display: 'inline-block', fontSize: '50%' }; // Switch help depending if admin or not. -const getToolTipMessage = (isAdmin: boolean) => isAdmin ? 'help.admin.meterview' : 'help.meters.meterview' \ No newline at end of file +const getToolTipMessage = (isAdmin: boolean) => isAdmin ? 'help.admin.meterview' : 'help.meters.meterview' diff --git a/src/client/app/components/router/ErrorComponent.tsx b/src/client/app/components/router/ErrorComponent.tsx index b418b1064..3e5e3828b 100644 --- a/src/client/app/components/router/ErrorComponent.tsx +++ b/src/client/app/components/router/ErrorComponent.tsx @@ -9,7 +9,7 @@ import AppLayout from '../../components/AppLayout'; import translate from '../../utils/translate'; /** - * @returns A simple loading spinner used to indicate that the startup init sequence is in progress + * @returns A error page that then returns to main dashboard page. */ export default function ErrorComponent() { const nav = useNavigate(); diff --git a/src/client/app/components/router/GraphLinkComponent.tsx b/src/client/app/components/router/GraphLinkComponent.tsx index 68ebabdc1..8edefbb1b 100644 --- a/src/client/app/components/router/GraphLinkComponent.tsx +++ b/src/client/app/components/router/GraphLinkComponent.tsx @@ -118,6 +118,6 @@ export const GraphLink = () => { showErrorNotification(translate('failed.to.link.graph')); } // All appropriate state updates should've been executed - // redirect to root clear the link in the search bar + // redirect to root and clear the link in the search bar return } diff --git a/src/client/app/components/router/RoleOutlet.tsx b/src/client/app/components/router/RoleOutlet.tsx index dd8d83482..c55a1c09f 100644 --- a/src/client/app/components/router/RoleOutlet.tsx +++ b/src/client/app/components/router/RoleOutlet.tsx @@ -16,7 +16,6 @@ interface RoleOutletProps { * @returns An outlet that is responsible for Role Routes. Routes users away from certain routes if they don't have permissions. */ export default function RoleOutlet(props: RoleOutletProps) { - // Function that returns a JSX element. Either the requested route's Component, as outlet or back to root const { userRole, initComplete } = useWaitForInit(); // // If state contains token it has been validated on startup or login. @@ -29,4 +28,4 @@ export default function RoleOutlet(props: RoleOutletProps) { } return -} \ No newline at end of file +} diff --git a/src/client/app/components/unit/CreateUnitModalComponent.tsx b/src/client/app/components/unit/CreateUnitModalComponent.tsx index a2e1c4030..dc0bdbffc 100644 --- a/src/client/app/components/unit/CreateUnitModalComponent.tsx +++ b/src/client/app/components/unit/CreateUnitModalComponent.tsx @@ -1,6 +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 { useEffect, useState } from 'react'; import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, ModalBody, ModalFooter, ModalHeader, Row } from 'reactstrap'; diff --git a/src/client/app/components/unit/EditUnitModalComponent.tsx b/src/client/app/components/unit/EditUnitModalComponent.tsx index 0aa3ce82b..160e71562 100644 --- a/src/client/app/components/unit/EditUnitModalComponent.tsx +++ b/src/client/app/components/unit/EditUnitModalComponent.tsx @@ -1,6 +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 { store } from '../../store'; //Realize that * is already imported from react @@ -56,7 +57,6 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp const [state, setState] = useState(values); const conversionData = useAppSelector(selectConversionsDetails); - const handleStringChange = (e: React.ChangeEvent) => { setState({ ...state, [e.target.name]: e.target.value }); } diff --git a/src/client/app/components/unit/UnitsDetailComponent.tsx b/src/client/app/components/unit/UnitsDetailComponent.tsx index 5d76d0ea0..c8645f777 100644 --- a/src/client/app/components/unit/UnitsDetailComponent.tsx +++ b/src/client/app/components/unit/UnitsDetailComponent.tsx @@ -1,6 +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 { QueryStatus } from '@reduxjs/toolkit/query'; import * as React from 'react'; import { FormattedMessage } from 'react-intl'; diff --git a/src/client/app/redux/componentHooks.ts b/src/client/app/redux/componentHooks.ts index e2b30302e..6162cd25d 100644 --- a/src/client/app/redux/componentHooks.ts +++ b/src/client/app/redux/componentHooks.ts @@ -8,8 +8,6 @@ import { useAppSelector } from './reduxHooks'; import localeData, { LocaleDataKey } from '../translations/data'; import { createIntlCache, createIntl, defineMessages } from 'react-intl'; - - export const useWaitForInit = () => { const isAdmin = useAppSelector(selectIsAdmin); const userRole = useAppSelector(selectCurrentUserRole); @@ -37,4 +35,4 @@ export const useTranslate = () => { } return translate -}; \ No newline at end of file +}; From 27ccb651dfbc2e17cd88b5d553cad6255b64bafc Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Mon, 5 Feb 2024 02:37:22 +0000 Subject: [PATCH 059/131] Auth Midleware, address comments. --- package-lock.json | 10 +- package.json | 2 +- src/client/app/components/AppLayout.tsx | 8 +- .../components/admin/UsersDetailComponent.tsx | 1 - .../groups/GroupsDetailComponent.tsx | 4 + .../meters/MetersDetailComponent.tsx | 6 +- .../app/components/router/AdminOutlet.tsx | 3 +- .../app/components/router/ErrorComponent.tsx | 1 + .../unit/EditUnitModalComponent.tsx | 3 +- src/client/app/redux/actions/admin.ts | 192 ------------------ src/client/app/redux/actions/currentUser.ts | 57 ------ src/client/app/redux/api/authApi.ts | 17 +- src/client/app/redux/api/userApi.ts | 11 +- src/client/app/redux/listenerMiddleware.ts | 20 ++ ...phHistory.ts => graphHistoryMiddleware.ts} | 4 +- src/client/app/redux/middleware/middleware.ts | 18 -- .../middleware/unauthorizedAccesMiddleware.ts | 28 +++ .../app/redux/slices/currentUserSlice.ts | 15 +- src/client/app/store.ts | 2 +- src/client/app/types/redux/currentUser.ts | 1 - 20 files changed, 98 insertions(+), 305 deletions(-) delete mode 100644 src/client/app/redux/actions/admin.ts delete mode 100644 src/client/app/redux/actions/currentUser.ts create mode 100644 src/client/app/redux/listenerMiddleware.ts rename src/client/app/redux/middleware/{graphHistory.ts => graphHistoryMiddleware.ts} (88%) delete mode 100644 src/client/app/redux/middleware/middleware.ts create mode 100644 src/client/app/redux/middleware/unauthorizedAccesMiddleware.ts diff --git a/package-lock.json b/package-lock.json index 1e5a58acc..1ef649ff1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "MPL-2.0", "dependencies": { - "@reduxjs/toolkit": "~2.0.1", + "@reduxjs/toolkit": "~2.1.0", "@wojtekmaj/react-daterange-picker": "~5.2.0", "axios": "~1.6.2", "bcryptjs": "~2.4.3", @@ -2535,12 +2535,12 @@ } }, "node_modules/@reduxjs/toolkit": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.0.1.tgz", - "integrity": "sha512-fxIjrR9934cmS8YXIGd9e7s1XRsEU++aFc9DVNMFMRTM5Vtsg2DCRMj21eslGtDt43IUf9bJL3h5bwUlZleibA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.1.0.tgz", + "integrity": "sha512-nfJ/b4ZhzUevQ1ZPKjlDL6CMYxO4o7ZL7OSsvSOxzT/EN11LsBDgTqP7aedHtBrFSVoK7oTP1SbMWUwGb30NLg==", "dependencies": { "immer": "^10.0.3", - "redux": "^5.0.0", + "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.0.1" }, diff --git a/package.json b/package.json index 90c446c02..65d12745c 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "babel-plugin-lodash": "~3.3.4" }, "dependencies": { - "@reduxjs/toolkit": "~2.0.1", + "@reduxjs/toolkit": "~2.1.0", "@wojtekmaj/react-daterange-picker": "~5.2.0", "axios": "~1.6.2", "bcryptjs": "~2.4.3", diff --git a/src/client/app/components/AppLayout.tsx b/src/client/app/components/AppLayout.tsx index 3225caea3..65eae4421 100644 --- a/src/client/app/components/AppLayout.tsx +++ b/src/client/app/components/AppLayout.tsx @@ -21,7 +21,13 @@ export default function AppLayout(props: LayoutProps) { <> - {props.children ?? } + { + // For the vast majority of cases we utilize react-router's outlet here. + // However we can use app layout with children. + // Refer to ErrorComponent.tsx for children usage. + // Refer to RouteComponent for outlet usage + props.children ?? + } ) diff --git a/src/client/app/components/admin/UsersDetailComponent.tsx b/src/client/app/components/admin/UsersDetailComponent.tsx index e19e0fe79..53bae867f 100644 --- a/src/client/app/components/admin/UsersDetailComponent.tsx +++ b/src/client/app/components/admin/UsersDetailComponent.tsx @@ -46,7 +46,6 @@ export default function UserDetailComponent() { // make new list from existing local user state const updatedList = localUsersChanges.map(user => (user.email === targetUser.email) ? updatedUser : user) setLocalUsersChanges(updatedList) - // editUser(user.email, target.value as UserRole); } const deleteUser = (email: string) => { diff --git a/src/client/app/components/groups/GroupsDetailComponent.tsx b/src/client/app/components/groups/GroupsDetailComponent.tsx index 63ce71469..73333a2e7 100644 --- a/src/client/app/components/groups/GroupsDetailComponent.tsx +++ b/src/client/app/components/groups/GroupsDetailComponent.tsx @@ -11,6 +11,7 @@ import { selectVisibleMeterAndGroupData } from '../../redux/selectors/adminSelec import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateGroupModalComponent from './CreateGroupModalComponent'; import GroupViewComponent from './GroupViewComponent'; +import { authApi, authPollInterval } from '../../redux/api/authApi'; /** * Defines the groups page card view @@ -20,6 +21,9 @@ export default function GroupsDetailComponent() { // Check for admin status const isAdmin = useAppSelector(state => selectIsAdmin(state)); + // page may contain admin info so verify admin status while admin is authenticated. + authApi.useTokenPollQuery(undefined, { skip: !isAdmin, pollingInterval: authPollInterval }) + // We only want displayable groups if non-admins because they still have non-displayable in state. const { visibleGroups } = useAppSelector(state => selectVisibleMeterAndGroupData(state)); diff --git a/src/client/app/components/meters/MetersDetailComponent.tsx b/src/client/app/components/meters/MetersDetailComponent.tsx index b7cf57377..252c84c4c 100644 --- a/src/client/app/components/meters/MetersDetailComponent.tsx +++ b/src/client/app/components/meters/MetersDetailComponent.tsx @@ -12,6 +12,7 @@ import '../../styles/card-page.css'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import CreateMeterModalComponent from './CreateMeterModalComponent'; import MeterViewComponent from './MeterViewComponent'; +import { authApi, authPollInterval } from '../../redux/api/authApi'; /** * Defines the meters page card view @@ -21,6 +22,8 @@ export default function MetersDetailComponent() { // Check for admin status const isAdmin = useAppSelector(selectIsAdmin); + // page may contain admin info so verify admin status while admin is authenticated. + authApi.useTokenPollQuery(undefined, { skip: !isAdmin, pollingInterval: authPollInterval }) // We only want displayable meters if non-admins because they still have // non-displayable in state. const { visibleMeters } = useAppSelector(selectVisibleMeterAndGroupData); @@ -43,8 +46,7 @@ export default function MetersDetailComponent() { } {
- {/* Create a MeterViewComponent for each MeterData in Meters State after sorting by identifier */} - {/* Optional Chaining to prevent from crashing upon startup race conditions*/} + {/* Create a MeterViewComponent for each MeterData in Meters State */} {Object.values(visibleMeters) .map(MeterData => ( diff --git a/src/client/app/components/router/ErrorComponent.tsx b/src/client/app/components/router/ErrorComponent.tsx index 3e5e3828b..8defb0f84 100644 --- a/src/client/app/components/router/ErrorComponent.tsx +++ b/src/client/app/components/router/ErrorComponent.tsx @@ -15,6 +15,7 @@ export default function ErrorComponent() { const nav = useNavigate(); return ( + {/* Pass div as child prop to AppLayout */}
) => { setState({ ...state, [e.target.name]: e.target.value }); @@ -103,7 +103,6 @@ export default function EditUnitModalComponent(props: EditUnitModalComponentProp const shouldUpdateUnit = (): boolean => { // true if inputted values are okay and there are changes. let inputOk = true; - const meterDataByID = selectMeterDataById(store.getState()) // Check for case 1 if (props.unit.typeOfUnit === UnitType.meter && state.typeOfUnit !== UnitType.meter) { diff --git a/src/client/app/redux/actions/admin.ts b/src/client/app/redux/actions/admin.ts deleted file mode 100644 index 51903d63a..000000000 --- a/src/client/app/redux/actions/admin.ts +++ /dev/null @@ -1,192 +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 { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; -import { Dispatch, GetState, Thunk } from '../../types/redux/actions'; -import { State } from '../../types/redux/state'; -import { conversionArrayApi, preferencesApi } from '../../utils/api'; -import translate from '../../utils/translate'; -import * as moment from 'moment'; -// import { updateSelectedLanguage } from './options'; -import { graphSlice } from '../slices/graphSlice'; -import { adminSlice } from '../slices/adminSlice'; -// TODO Marked For Deletion after RTK migration solidified -/* 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 */ -/** - * Dispatches a fetch for admin preferences and sets the state based upon the result - */ -function fetchPreferences(): Thunk { - return async (dispatch: Dispatch, getState: GetState) => { - dispatch(adminSlice.actions.requestPreferences()); - const preferences = await preferencesApi.getPreferences(); - dispatch(adminSlice.actions.receivePreferences(preferences)); - moment.locale(getState().admin.defaultLanguage); - // TODO reference only DELETE ME - // if (!getState().graph.hotlinked) { - // hotlink removed in rtk migration - dispatch((dispatch2: Dispatch) => { - const state = getState(); - dispatch2(graphSlice.actions.changeChartToRender(state.admin.defaultChartToRender)); - if (preferences.defaultBarStacking !== state.graph.barStacking) { - dispatch2(graphSlice.actions.changeBarStacking()); - } - if (preferences.defaultAreaNormalization !== state.graph.areaNormalization) { - dispatch2(graphSlice.actions.toggleAreaNormalization()); - } - if (preferences.defaultLanguage !== state.options.selectedLanguage) { - // if the site default differs from the selected language, update the selected language and the locale - // dispatch2(updateSelectedLanguage(preferences.defaultLanguage)); - moment.locale(preferences.defaultLanguage); - } else { - // else set moment locale to site default - moment.locale(getState().admin.defaultLanguage); - } - }); - // } - }; -} -// TODO: Add warning for invalid data in admin panel src/client/app/components/admin/PreferencesComponent.tsx -/* Validates preferences - Create Preferences Validation: - Mininum Value cannot bigger than Maximum Value - Minimum Value and Maximum Value must be between valid input - Minimum Date and Maximum cannot be blank - Minimum Date cannot be after Maximum Date - Minimum Date and Maximum Value must be between valid input - Maximum No of Error must be between 0 and valid input -*/ - -function validPreferences(state: State) { - const MIN_VAL = Number.MIN_SAFE_INTEGER; - const MAX_VAL = Number.MAX_SAFE_INTEGER; - const MIN_DATE_MOMENT = moment(0).utc(); - const MAX_DATE_MOMENT = moment(0).utc().add(5000, 'years'); - const MAX_ERRORS = 75; - if (state.admin.defaultMeterReadingGap >= 0 && - state.admin.defaultMeterMinimumValue >= MIN_VAL && - state.admin.defaultMeterMinimumValue <= state.admin.defaultMeterMaximumValue && - state.admin.defaultMeterMinimumValue <= MAX_VAL && - state.admin.defaultMeterMinimumDate !== '' && - state.admin.defaultMeterMaximumDate !== '' && - moment(state.admin.defaultMeterMinimumDate).isValid() && - moment(state.admin.defaultMeterMaximumDate).isValid() && - moment(state.admin.defaultMeterMinimumDate).isSameOrAfter(MIN_DATE_MOMENT) && - moment(state.admin.defaultMeterMinimumDate).isSameOrBefore(moment(state.admin.defaultMeterMaximumDate)) && - moment(state.admin.defaultMeterMaximumDate).isSameOrBefore(MAX_DATE_MOMENT) && - (state.admin.defaultMeterMaximumErrors >= 0 && state.admin.defaultMeterMaximumErrors <= MAX_ERRORS)) { - return true; - } else { - return false; - } -} -/** - * Submits preferences stored in the state to the API to be stored in the database - */ -export function submitPreferences() { - return async (dispatch: Dispatch, getState: GetState) => { - const state = getState(); - try { - if (!validPreferences(state)) { - throw new Error('invalid input'); - } - const preferences = await preferencesApi.submitPreferences({ - displayTitle: state.admin.displayTitle, - defaultChartToRender: state.admin.defaultChartToRender, - defaultBarStacking: state.admin.defaultBarStacking, - defaultLanguage: state.admin.defaultLanguage, - defaultTimezone: state.admin.defaultTimezone, - defaultWarningFileSize: state.admin.defaultWarningFileSize, - defaultFileSizeLimit: state.admin.defaultFileSizeLimit, - defaultAreaNormalization: state.admin.defaultAreaNormalization, - defaultAreaUnit: state.admin.defaultAreaUnit, - defaultMeterReadingFrequency: state.admin.defaultMeterReadingFrequency, - defaultMeterMinimumValue: state.admin.defaultMeterMinimumValue, - defaultMeterMaximumValue: state.admin.defaultMeterMaximumValue, - defaultMeterMinimumDate: state.admin.defaultMeterMinimumDate, - defaultMeterMaximumDate: state.admin.defaultMeterMaximumDate, - defaultMeterReadingGap: state.admin.defaultMeterReadingGap, - defaultMeterMaximumErrors: state.admin.defaultMeterMaximumErrors, - defaultMeterDisableChecks: state.admin.defaultMeterDisableChecks, - defaultHelpUrl: state.admin.defaultHelpUrl - }); - // Only return the defaultMeterReadingFrequency because the value from the DB - // generally differs from what the user input so update state with DB value. - dispatch(adminSlice.actions.markPreferencesSubmitted(preferences.defaultMeterReadingFrequency)); - showSuccessNotification(translate('updated.preferences')); - } catch (e) { - dispatch(adminSlice.actions.markPreferencesNotSubmitted()); - showErrorNotification(translate('failed.to.submit.changes')); - } - }; -} - -/** - * @param state The redux state. - * @returns Whether preferences are fetching - */ -function shouldFetchPreferenceData(state: State): boolean { - return !state.admin.isFetching; -} - -/** - * @param state The redux state. - * @returns Whether preferences are submitted - */ -function shouldSubmitPreferenceData(state: State): boolean { - return !state.admin.submitted; -} - -export function fetchPreferencesIfNeeded(): Thunk { - return (dispatch: Dispatch, getState: GetState) => { - if (shouldFetchPreferenceData(getState())) { - return dispatch(fetchPreferences()); - } - return Promise.resolve(); - }; -} - -export function submitPreferencesIfNeeded(): Thunk { - return (dispatch: Dispatch, getState: GetState) => { - if (shouldSubmitPreferenceData(getState())) { - return dispatch(submitPreferences()); - } - return Promise.resolve(); - }; -} - - -/** - * @param state The redux state. - * @returns Whether or not the Cik and views are updating - */ -function shouldUpdateCikAndDBViews(state: State): boolean { - return !state.admin.isUpdatingCikAndDBViews; -} - -/** - * Redo Cik and/or refresh reading views. - * This function is called when some changes in units/conversions affect the Cik table or reading views. - * @param shouldRedoCik Whether to refresh Cik. - * @param shouldRefreshReadingViews Whether to refresh reading views. - */ -export function updateCikAndDBViewsIfNeeded(shouldRedoCik: boolean, shouldRefreshReadingViews: boolean): Thunk { - return async (dispatch: Dispatch, getState: GetState) => { - if (shouldUpdateCikAndDBViews(getState())) { - // set the page to a loading state - dispatch(adminSlice.actions.toggleWaitForCikAndDB()); - await conversionArrayApi.refresh(shouldRedoCik, shouldRefreshReadingViews); - // revert to normal state once refresh is complete - dispatch(adminSlice.actions.toggleWaitForCikAndDB()); - if (shouldRedoCik || shouldRefreshReadingViews) { - // Only reload window if redoCik and/or refresh reading views. - window.location.reload(); - } - } - return Promise.resolve(); - }; -} diff --git a/src/client/app/redux/actions/currentUser.ts b/src/client/app/redux/actions/currentUser.ts deleted file mode 100644 index bc72e4536..000000000 --- a/src/client/app/redux/actions/currentUser.ts +++ /dev/null @@ -1,57 +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 { usersApi, verificationApi } from '../../utils/api'; -import { Thunk, Dispatch, GetState } from '../../types/redux/actions'; -import { State } from '../../types/redux/state'; -import { deleteToken, hasToken } from '../../utils/token'; -import { currentUserSlice } from '../slices/currentUserSlice'; - -// TODO Marked For Deletion after RTK migration solidified -/* 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 */ -/** - * Check if we should fetch the current user's data. This function has the side effect of deleting an invalid token from local storage. - * @param state The redux state - * @returns Return true if we should fetch the current user's data. Returns false otherwise. - */ -async function shouldFetchCurrentUser(state: State): Promise { - // If we are currently fetching the current user, we should not fetch the data again. - if (!state.currentUser.isFetching) { - if (hasToken()) { - // If we have a token, we should check to see if it is valid. - if (await verificationApi.checkTokenValid()) { - // If the token is valid, we should fetch the current user's data. - return true; - } else { - deleteToken(); // We should delete the token when we know that it is invalid. This helps ensure that we do not keep an invalid token. - return false; - } - } else { - return false; - } - } else { - return false; - } -} - -export function fetchCurrentUser(): Thunk { - return async (dispatch: Dispatch) => { - dispatch(currentUserSlice.actions.requestCurrentUser()); - const user = await usersApi.getCurrentUser(); - return dispatch(currentUserSlice.actions.receiveCurrentUser(user)); - }; -} - -export function fetchCurrentUserIfNeeded(): Thunk { - return async (dispatch: Dispatch, getState: GetState) => { - if (await shouldFetchCurrentUser(getState())) { - return dispatch(fetchCurrentUser()); - } - return Promise.resolve(); - }; -} \ No newline at end of file diff --git a/src/client/app/redux/api/authApi.ts b/src/client/app/redux/api/authApi.ts index fe1b2ad59..db1d2f449 100644 --- a/src/client/app/redux/api/authApi.ts +++ b/src/client/app/redux/api/authApi.ts @@ -4,7 +4,7 @@ import { currentUserSlice } from '../slices/currentUserSlice'; import { User } from '../../types/items'; -import { deleteToken } from '../../utils/token'; +import { deleteToken, getToken, hasToken } from '../../utils/token'; import { baseApi } from './baseApi'; type LoginResponse = User & { @@ -34,6 +34,16 @@ export const authApi = baseApi.injectEndpoints({ body: { token: token } }) }), + tokenPoll: builder.query({ + // Query to be used as a polling utility for admin outlet pages. + queryFn: (_args, api) => { + if (hasToken()) { + api.dispatch(authApi.endpoints.verifyToken.initiate(getToken())) + } + // don't care about data, middleware will handle failed verifications + return { data: null } + } + }), logout: builder.mutation({ queryFn: (_, { dispatch }) => { // Opt to use a RTK mutation instead of manually writing a thunk to take advantage mutation invalidations @@ -44,4 +54,7 @@ export const authApi = baseApi.injectEndpoints({ invalidatesTags: ['MeterData', 'GroupData'] }) }) -}) \ No newline at end of file +}) + +// Poll interval in milliseconds (1 minute) +export const authPollInterval = 60000 \ No newline at end of file diff --git a/src/client/app/redux/api/userApi.ts b/src/client/app/redux/api/userApi.ts index 01cf573f4..79349fdce 100644 --- a/src/client/app/redux/api/userApi.ts +++ b/src/client/app/redux/api/userApi.ts @@ -3,7 +3,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { NewUser, User } from '../../types/items'; -// import { authApi } from './authApi'; import { baseApi } from './baseApi'; export const userApi = baseApi.injectEndpoints({ @@ -42,12 +41,4 @@ export const userApi = baseApi.injectEndpoints({ invalidatesTags: ['Users'] }) }) -}) - -// public async editUsers(users: User[]) { -// return await this.backend.doPostRequest('/api/users/edit', { users }); -// } - -// public async deleteUser(email: string) { -// return await this.backend.doPostRequest('/api/users/delete', { email }); -// } \ No newline at end of file +}) \ No newline at end of file diff --git a/src/client/app/redux/listenerMiddleware.ts b/src/client/app/redux/listenerMiddleware.ts new file mode 100644 index 000000000..8a4436228 --- /dev/null +++ b/src/client/app/redux/listenerMiddleware.ts @@ -0,0 +1,20 @@ +/* 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/. */ + +// listenerMiddleware.ts +// https://redux-toolkit.js.org/api/crea teListenerMiddleware#typescript-usage +import { addListener, createListenerMiddleware } from '@reduxjs/toolkit' +import type { AppDispatch, RootState } from '../store' +import { graphHistoryListener } from './middleware/graphHistoryMiddleware' +import { unauthorizedRequestListener } from './middleware/unauthorizedAccesMiddleware' + + +export const listenerMiddleware = createListenerMiddleware() + +export const startAppListening = listenerMiddleware.startListening.withTypes< RootState, AppDispatch>() +export const addAppListener = addListener.withTypes() +export type AppListener = typeof startAppListening + +graphHistoryListener(startAppListening) +unauthorizedRequestListener(startAppListening) diff --git a/src/client/app/redux/middleware/graphHistory.ts b/src/client/app/redux/middleware/graphHistoryMiddleware.ts similarity index 88% rename from src/client/app/redux/middleware/graphHistory.ts rename to src/client/app/redux/middleware/graphHistoryMiddleware.ts index c98e2a642..f4fc6ed4e 100644 --- a/src/client/app/redux/middleware/graphHistory.ts +++ b/src/client/app/redux/middleware/graphHistoryMiddleware.ts @@ -3,10 +3,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { isAnyOf } from '@reduxjs/toolkit'; +import { AppListener } from '../listenerMiddleware'; import { graphSlice, updateHistory } from '../slices/graphSlice'; -import { AppStartListening } from './middleware'; -export const historyMiddleware = (startListening: AppStartListening) => { +export const graphHistoryListener = (startListening: AppListener) => { startListening({ predicate: (action, currentState, previousState) => { diff --git a/src/client/app/redux/middleware/middleware.ts b/src/client/app/redux/middleware/middleware.ts deleted file mode 100644 index a5af6ad75..000000000 --- a/src/client/app/redux/middleware/middleware.ts +++ /dev/null @@ -1,18 +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/. */ - -// listenerMiddleware.ts -// https://redux-toolkit.js.org/api/createListenerMiddleware#typescript-usage -import { type TypedStartListening, type TypedAddListener, addListener, createListenerMiddleware } from '@reduxjs/toolkit' -import type { RootState, AppDispatch } from '../../store' -import { historyMiddleware } from './graphHistory' - - -export type AppStartListening = TypedStartListening -export const addAppListener = addListener as TypedAddListener -export const listenerMiddleware = createListenerMiddleware() -// Typescript usage for middleware api -export const startListening = listenerMiddleware.startListening as AppStartListening - -historyMiddleware(startListening) \ No newline at end of file diff --git a/src/client/app/redux/middleware/unauthorizedAccesMiddleware.ts b/src/client/app/redux/middleware/unauthorizedAccesMiddleware.ts new file mode 100644 index 000000000..cdf0f8c5d --- /dev/null +++ b/src/client/app/redux/middleware/unauthorizedAccesMiddleware.ts @@ -0,0 +1,28 @@ +/* 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 { isAsyncThunkAction, isRejected } from '@reduxjs/toolkit'; +import { showErrorNotification } from '../../utils/notifications'; +import translate from '../../utils/translate'; +import { AppListener } from '../listenerMiddleware'; +import { authApi } from '../api/authApi'; + +export const unauthorizedRequestListener = (startListening: AppListener) => { + + startListening({ + predicate: action => { + // Listens for rejected async thunks. if no payload then its an RTK internal call that needs to also be filtered. + return isAsyncThunkAction(action) && isRejected(action) && action.payload !== undefined + }, + effect: (action: any, {dispatch}): void => { + // Look for token failed responses from server + const unAuthorizedTokenRequest = (action.payload.status === 401 || action.payload.data.message === 'Failed to authenticate token.') + if(unAuthorizedTokenRequest){ + dispatch(authApi.endpoints.logout.initiate()) + showErrorNotification(translate('invalid.token.login')) + } + } + }) + +} \ No newline at end of file diff --git a/src/client/app/redux/slices/currentUserSlice.ts b/src/client/app/redux/slices/currentUserSlice.ts index d9ef0563d..ff4d150a8 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 { authApi } from '../api/authApi'; import { userApi } from '../api/userApi'; -import { User, UserRole } from '../../types/items'; +import { UserRole } from '../../types/items'; import { CurrentUserState } from '../../types/redux/currentUser'; import { setToken } from '../../utils/token'; @@ -14,7 +14,6 @@ import { setToken } from '../../utils/token'; * Defines store interactions when version related actions are dispatched to the store. */ const defaultState: CurrentUserState = { - isFetching: false, profile: null, token: null }; @@ -23,13 +22,6 @@ export const currentUserSlice = createSlice({ name: 'currentUser', initialState: defaultState, reducers: { - requestCurrentUser: state => { - state.isFetching = true - }, - receiveCurrentUser: (state, action: PayloadAction) => { - state.isFetching = false - state.profile = action.payload - }, clearCurrentUser: state => { state.profile = null state.token = null @@ -66,3 +58,8 @@ export const { selectCurrentUserRole, selectIsAdmin } = currentUserSlice.selectors + +export const { + setUserToken, + clearCurrentUser +} = currentUserSlice.actions \ No newline at end of file diff --git a/src/client/app/store.ts b/src/client/app/store.ts index 945357310..8544aad96 100644 --- a/src/client/app/store.ts +++ b/src/client/app/store.ts @@ -6,7 +6,7 @@ import { configureStore } from '@reduxjs/toolkit' import { rootReducer } from './redux/rootReducer'; import { baseApi } from './redux/api/baseApi'; import { Dispatch } from './types/redux/actions'; -import { listenerMiddleware } from './redux/middleware/middleware'; +import { listenerMiddleware } from './redux/listenerMiddleware'; import { setGlobalDevModeChecks } from 'reselect' export const store = configureStore({ diff --git a/src/client/app/types/redux/currentUser.ts b/src/client/app/types/redux/currentUser.ts index 78c79b26f..a69adcd7e 100644 --- a/src/client/app/types/redux/currentUser.ts +++ b/src/client/app/types/redux/currentUser.ts @@ -6,7 +6,6 @@ import { User } from '../items'; export interface CurrentUserState { - isFetching: boolean; profile: User | null; token: string | null; } From 74d52ce8699eeb9318cf0919039a9475552ad725 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Wed, 7 Feb 2024 00:40:56 +0000 Subject: [PATCH 060/131] Line ErrorBar Fix & Layout Tweaks - Layout Tweaks to allow for proper graph scaling with window. - Line utilizes query selectFromResult to derive data in a memoized fashion which should improve performace --- src/client/app/components/AppLayout.tsx | 28 +- .../app/components/BarChartComponent.tsx | 6 +- .../app/components/ChartSelectComponent.tsx | 8 +- .../app/components/DashboardComponent.tsx | 13 +- .../app/components/ErrorBarComponent.tsx | 21 +- src/client/app/components/FooterComponent.tsx | 40 +-- src/client/app/components/HeaderComponent.tsx | 6 +- src/client/app/components/HomeComponent.tsx | 4 +- .../app/components/LineChartComponent.tsx | 305 ++++-------------- .../MeterAndGroupSelectComponent.tsx | 7 +- src/client/app/components/ThreeDComponent.tsx | 12 +- .../app/components/UIOptionsComponent.tsx | 51 ++- .../app/redux/selectors/lineChartSelectors.ts | 287 ++++++++++++++++ src/client/app/redux/selectors/selectors.ts | 12 +- src/client/app/styles/index.css | 15 +- 15 files changed, 459 insertions(+), 356 deletions(-) create mode 100644 src/client/app/redux/selectors/lineChartSelectors.ts diff --git a/src/client/app/components/AppLayout.tsx b/src/client/app/components/AppLayout.tsx index 65eae4421..cb83f2813 100644 --- a/src/client/app/components/AppLayout.tsx +++ b/src/client/app/components/AppLayout.tsx @@ -20,15 +20,25 @@ export default function AppLayout(props: LayoutProps) { return ( <> - - { - // For the vast majority of cases we utilize react-router's outlet here. - // However we can use app layout with children. - // Refer to ErrorComponent.tsx for children usage. - // Refer to RouteComponent for outlet usage - props.children ?? - } - +
+ + { + // For the vast majority of cases we utilize react-router's outlet here. + // However we can use app layout with children. + // Refer to ErrorComponent.tsx for children usage. + // Refer to RouteComponent for outlet usage + props.children ?? + } + +
) } \ No newline at end of file diff --git a/src/client/app/components/BarChartComponent.tsx b/src/client/app/components/BarChartComponent.tsx index 6041d4c49..1243a7094 100644 --- a/src/client/app/components/BarChartComponent.tsx +++ b/src/client/app/components/BarChartComponent.tsx @@ -225,7 +225,7 @@ export default function BarChartComponent() { return ( + <>

: @@ -42,6 +42,7 @@ export default function ChartSelectComponent() { Object.values(ChartTypes) // filter out current chart .filter(chartType => chartType !== currentChartToRender) + .sort() // map to components .map(chartType => -

+ ); } -const divBottomPadding: React.CSSProperties = { - paddingBottom: '15px' -}; const labelStyle: React.CSSProperties = { fontWeight: 'bold', margin: 0 diff --git a/src/client/app/components/DashboardComponent.tsx b/src/client/app/components/DashboardComponent.tsx index f11f2a181..12831198a 100644 --- a/src/client/app/components/DashboardComponent.tsx +++ b/src/client/app/components/DashboardComponent.tsx @@ -23,17 +23,15 @@ import RadarChartComponent from './RadarChartComponent'; export default function DashboardComponent() { const chartToRender = useAppSelector(selectChartToRender); const optionsVisibility = useAppSelector(selectOptionsVisibility); - const optionsClassName = optionsVisibility ? 'col-2 d-none d-lg-block' : 'd-none'; - const chartClassName = optionsVisibility ? 'col-12 col-lg-10' : 'col-12'; return ( -
-
-
+
+
+
-
-
+
+
{chartToRender === ChartTypes.line && } {chartToRender === ChartTypes.bar && } @@ -42,7 +40,6 @@ export default function DashboardComponent() { {chartToRender === ChartTypes.threeD && } {chartToRender === ChartTypes.radar && }
-
diff --git a/src/client/app/components/ErrorBarComponent.tsx b/src/client/app/components/ErrorBarComponent.tsx index 3e4334608..c8116ed3e 100644 --- a/src/client/app/components/ErrorBarComponent.tsx +++ b/src/client/app/components/ErrorBarComponent.tsx @@ -3,34 +3,27 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as React from 'react'; -import { useSelector, useDispatch } from 'react-redux'; -import { State } from '../types/redux/state'; +import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; +import { graphSlice, selectShowMinMax } from '../redux/slices/graphSlice'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; -import { graphSlice } from '../redux/slices/graphSlice'; /** * React Component rendering an Error Bar checkbox for toggle operation. * @returns Error Bar checkbox with tooltip and label */ export default function ErrorBarComponent() { - const dispatch = useDispatch(); - const graphState = useSelector((state: State) => state.graph); - - /** - * Dispatches an action to toggle visibility of min/max lines on checkbox interaction - */ - const handleToggleShowMinMax = () => { - dispatch(graphSlice.actions.toggleShowMinMax()); - } + const dispatch = useAppDispatch(); + const showMinMax = useAppSelector(selectShowMinMax); return (
handleToggleShowMinMax()} - checked={graphState.showMinMax} + // Dispatches an action to toggle visibility of min/max lines on checkbox interaction + onChange={() => dispatch(graphSlice.actions.toggleShowMinMax())} + checked={showMinMax} id='errorBar' />
); +}; \ No newline at end of file diff --git a/src/client/app/components/MeterAndGroupSelectComponent.tsx b/src/client/app/components/MeterAndGroupSelectComponent.tsx index 9b79adcd2..95ea218f2 100644 --- a/src/client/app/components/MeterAndGroupSelectComponent.tsx +++ b/src/client/app/components/MeterAndGroupSelectComponent.tsx @@ -79,7 +79,7 @@ const formatGroupLabel = (data: GroupedOption) => { {data.label} {data.options.length}
- ) + ); }; interface MeterAndGroupSelectProps { @@ -101,27 +101,27 @@ const MultiValueLabel = (props: MultiValueGenericProps e.stopPropagation()} onClick={e => { - ReactTooltip.rebuild() - e.stopPropagation() - ref.current && ReactTooltip.show(ref.current) + ReactTooltip.rebuild(); + e.stopPropagation(); + ref.current && ReactTooltip.show(ref.current); }} style={{ overflow: 'hidden' }} onMouseEnter={e => { if (!isDisabled) { - const multiValueLabel = e.currentTarget.children[0] + const multiValueLabel = e.currentTarget.children[0]; if (multiValueLabel.scrollWidth > e.currentTarget.clientWidth) { - ReactTooltip.rebuild() - ref.current && ReactTooltip.show(ref.current) + ReactTooltip.rebuild(); + ref.current && ReactTooltip.show(ref.current); } } }} onMouseLeave={() => { - ref.current && ReactTooltip.hide(ref.current) + ref.current && ReactTooltip.hide(ref.current); }} >
- ) + ); }; diff --git a/src/client/app/components/MultiCompareChartComponent.tsx b/src/client/app/components/MultiCompareChartComponent.tsx index e73b8fbda..43f60deb4 100644 --- a/src/client/app/components/MultiCompareChartComponent.tsx +++ b/src/client/app/components/MultiCompareChartComponent.tsx @@ -41,7 +41,7 @@ export default function MultiCompareChartComponent() { const name = meterDataByID[Number(key)].name; const identifier = meterDataByID[Number(key)].identifier; - const areaNormValid = (!areaNormalization || (meterDataByID[Number(key)].area > 0 && meterDataByID[Number(key)].areaUnit !== AreaUnitType.none)) + const areaNormValid = (!areaNormalization || (meterDataByID[Number(key)].area > 0 && meterDataByID[Number(key)].areaUnit !== AreaUnitType.none)); if (areaNormValid && selectedMeters.includes(Number(key))) { const change = calculateChange(value.curr_use, value.prev_use); const entity: CompareEntity = { @@ -55,7 +55,7 @@ export default function MultiCompareChartComponent() { }; selectedCompareEntities.push(entity); } - }) + }); Object.entries(groupReadings).forEach(([key, value]) => { const identifier = groupDataById[Number(key)].name; const areaNormValid = (!areaNormalization || (groupDataById[Number(key)].area > 0 && groupDataById[Number(key)].areaUnit !== AreaUnitType.none)); @@ -72,9 +72,9 @@ export default function MultiCompareChartComponent() { }; selectedCompareEntities.push(entity); } - }) + }); - selectedCompareEntities = sortIDs(selectedCompareEntities, sortingOrder) + selectedCompareEntities = sortIDs(selectedCompareEntities, sortingOrder); // Compute how much space should be used in the bootstrap grid system diff --git a/src/client/app/components/PlotNavComponent.tsx b/src/client/app/components/PlotNavComponent.tsx index a8fed60be..3c4b6c652 100644 --- a/src/client/app/components/PlotNavComponent.tsx +++ b/src/client/app/components/PlotNavComponent.tsx @@ -18,7 +18,7 @@ export default function PlotNavComponent() {
- ) + ); } export const PlotNav = () => { return ( @@ -27,29 +27,29 @@ export const PlotNav = () => {
- ) -} + ); +}; export const TrashCanHistoryComponent = () => { const dispatch = useAppDispatch(); const isDirty = useAppSelector(selectIsDirty); return ( < img src={isDirty ? './full_trashcan.png' : './empty_trashcan.png'} style={{ height: '25px' }} onClick={() => { - dispatch(clearGraphHistory()) + dispatch(clearGraphHistory()); }} /> - ) -} + ); +}; export const ExpandComponent = () => { const dispatch = useAppDispatch(); return ( { dispatch(changeSliderRange(TimeInterval.unbounded())) }} + onClick={() => { dispatch(changeSliderRange(TimeInterval.unbounded())); }} /> - ) -} + ); +}; export const RefreshGraphComponent = () => { const [time, setTime] = React.useState(0); @@ -58,7 +58,7 @@ export const RefreshGraphComponent = () => { const somethingFetching = useAppSelector(selectAnythingFetching); React.useEffect(() => { - const interval = setInterval(() => { setTime(prevTime => (prevTime + 25) % 360) }, 16); + const interval = setInterval(() => { setTime(prevTime => (prevTime + 25) % 360); }, 16); if (!somethingFetching) { clearInterval(interval); } @@ -66,7 +66,7 @@ export const RefreshGraphComponent = () => { }, [somethingFetching]); return ( { dispatch(updateTimeInterval(slider)) }} + onClick={() => { dispatch(updateTimeInterval(slider)); }} /> - ) -} \ No newline at end of file + ); +}; \ No newline at end of file diff --git a/src/client/app/components/PlotOED.tsx b/src/client/app/components/PlotOED.tsx index cfa6e9984..4b51485af 100644 --- a/src/client/app/components/PlotOED.tsx +++ b/src/client/app/components/PlotOED.tsx @@ -20,16 +20,16 @@ export interface OEDPlotProps { } export const PlotOED = (props: OEDPlotProps) => { - const { data } = props + const { data } = props; const dispatch = useAppDispatch(); // Current Range Slider. Controls Zoom for graphics. - const rangeSliderMin = useAppSelector(selectPlotlySliderMin) - const rangeSliderMax = useAppSelector(selectPlotlySliderMax) + const rangeSliderMin = useAppSelector(selectPlotlySliderMin); + const rangeSliderMax = useAppSelector(selectPlotlySliderMax); const locale = useAppSelector(selectSelectedLanguage); // Local State for plotly - const figure = React.useRef>(props) + const figure = React.useRef>(props); // Debounce to limit dispatch and keep reasonable history const debouncedRelayout = _.debounce( @@ -39,16 +39,16 @@ export const PlotOED = (props: OEDPlotProps) => { if (e['xaxis.range[0]'] && e['xaxis.range[1]']) { // The event signals changes in the user's interaction with the graph. // this will automatically trigger a refetch due to updating a query arg. - const startTS = moment.utc(e['xaxis.range[0]']) - const endTS = moment.utc(e['xaxis.range[1]']) + 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)); } 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] + const range = figure.current.layout?.xaxis?.range; + const startTS = range && range[0]; + const endTS = range && range[1]; dispatch(changeSliderRange(new TimeInterval(startTS, endTS))); } @@ -59,27 +59,27 @@ export const PlotOED = (props: OEDPlotProps) => { figure.current = { ...figure.current, ...e - } as PlotParams + } as PlotParams; }; // Iterating through datasets may be expensive, so useMemo() // Get dataset wth min /max date const minRange = React.useMemo(() => { - const minDataset = _.minBy(data, obj => obj.x![0]) - const min = minDataset?.x?.[0] - return min as Datum - }, [props.data]) + const minDataset = _.minBy(data, obj => obj.x![0]); + const min = minDataset?.x?.[0]; + return min as Datum; + }, [props.data]); // Get min/ max value from dataset const maxRange = React.useMemo(() => { - const maxDataset = _.maxBy(data, obj => obj.x![obj.x!.length - 1]) - const max = maxDataset?.x?.[maxDataset?.x?.length - 1] as Datum - return max as Datum - }, [props.data]) + const maxDataset = _.maxBy(data, obj => obj.x![obj.x!.length - 1]); + const max = maxDataset?.x?.[maxDataset?.x?.length - 1] as Datum; + return max as Datum; + }, [props.data]); // Use rangeSlider when bounded, else use min/maxRange - const start = rangeSliderMin ?? minRange - const end = rangeSliderMax ?? maxRange + const start = rangeSliderMin ?? minRange; + const end = rangeSliderMax ?? maxRange; return ( { } }} /> - ) -} \ No newline at end of file + ); +}; \ No newline at end of file diff --git a/src/client/app/components/RadarChartComponent.tsx b/src/client/app/components/RadarChartComponent.tsx index d16559292..e874b79b2 100644 --- a/src/client/app/components/RadarChartComponent.tsx +++ b/src/client/app/components/RadarChartComponent.tsx @@ -4,7 +4,7 @@ import * as _ from 'lodash'; import * as moment from 'moment'; -import * as React from 'react' +import * as React from 'react'; import getGraphColor from '../utils/getGraphColor'; import translate from '../utils/translate'; import Plot from 'react-plotly.js'; @@ -29,7 +29,7 @@ import LogoSpinner from './LogoSpinner'; * @returns radar plotly component */ export default function RadarChartComponent() { - const { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } = useAppSelector(selectRadarChartQueryArgs) + const { meterArgs, groupArgs, meterShouldSkip, groupShouldSkip } = useAppSelector(selectRadarChartQueryArgs); const { data: meterReadings, isLoading: meterIsLoading } = readingsApi.useLineQuery(meterArgs, { skip: meterShouldSkip }); const { data: groupData, isLoading: groupIsLoading } = readingsApi.useLineQuery(groupArgs, { skip: groupShouldSkip }); const datasets: any[] = []; @@ -47,7 +47,7 @@ export default function RadarChartComponent() { const groupDataById = useAppSelector(selectGroupDataById); if (meterIsLoading || groupIsLoading) { - return + return ; // return } @@ -60,7 +60,7 @@ export default function RadarChartComponent() { if (selectUnitState !== undefined) { // Determine the r-axis label and if the rate needs to be scaled. const returned = lineUnitLabel(selectUnitState, currentSelectedRate, areaNormalization, selectedAreaUnit); - unitLabel = returned.unitLabel + unitLabel = returned.unitLabel; needsRateScaling = returned.needsRateScaling; } } @@ -78,7 +78,7 @@ export default function RadarChartComponent() { meterArea * getAreaUnitConversion(meterDataById[meterID].areaUnit, selectedAreaUnit) : 1; // Divide areaScaling into the rate so have complete scaling factor for readings. const scaling = rateScaling / areaScaling; - const readingsData = meterReadings[meterID] + const readingsData = meterReadings[meterID]; if (readingsData) { const label = meterDataById[meterID].identifier; const colorID = meterID; @@ -139,7 +139,7 @@ export default function RadarChartComponent() { groupArea * getAreaUnitConversion(groupDataById[groupID].areaUnit, selectedAreaUnit) : 1; // Divide areaScaling into the rate so have complete scaling factor for readings. const scaling = rateScaling / areaScaling; - const readingsData = groupData[groupID] + const readingsData = groupData[groupID]; if (readingsData) { const label = groupDataById[groupID].name; const colorID = groupID; @@ -210,7 +210,7 @@ export default function RadarChartComponent() { } } ] - } + }; } else { // Plotly scatterpolar plots have the unfortunate attribute that if a smaller number of plotting // points is done first then that impacts the labeling of the polar coordinate where you can get @@ -245,7 +245,7 @@ export default function RadarChartComponent() { } } ] - } + }; } else { // Check if all the values for the dates are compatible. Plotly does not like having different dates in different // scatterpolar lines. Lots of attempts to get this to work failed so not going to allow since not that common. @@ -280,7 +280,7 @@ export default function RadarChartComponent() { } } ] - } + }; } else { // Data available and okay so plot. // Maximum number of ticks, represents 12 months. Too many is cluttered so this seems good value. @@ -334,5 +334,5 @@ export default function RadarChartComponent() { layout={layout} />
- ) + ); } diff --git a/src/client/app/components/ReadingsPerDaySelectComponent.tsx b/src/client/app/components/ReadingsPerDaySelectComponent.tsx index 6ed93e305..2b4bea1bc 100644 --- a/src/client/app/components/ReadingsPerDaySelectComponent.tsx +++ b/src/client/app/components/ReadingsPerDaySelectComponent.tsx @@ -54,7 +54,7 @@ export default function ReadingsPerDaySelect() { // (24 hours a day) / intervalLength, e.g, 1 hour intervals give 24 readings per day label: String((24 / intervalLength)), value: intervalLength - } as ReadingsPerDayOption + } as ReadingsPerDayOption; }); // Use the selectedOption enum value to update threeD State @@ -74,7 +74,7 @@ export default function ReadingsPerDaySelect() { const value = { label: displayValue, value: readingInterval - } + }; if (graphState.chartToRender === ChartTypes.threeD) { return ( @@ -85,7 +85,7 @@ export default function ReadingsPerDaySelect() {

+ {translate('identifier')} + + handleStringChange(e)} - value={meterDetails.identifier} /> + /> {/* Name input */} - - + {translate('name')} + + handleStringChange(e)} required value={meterDetails.name} - invalid={meterDetails.name === ''} /> + invalid={meterDetails.name === ''} + /> @@ -218,60 +210,62 @@ export default function CreateMeterModalComponent() { {/* meter unit input */} - - + {translate('meter.unitName')} + + { handleNumberChange(e); - setSelectedUnitId(true); }} - invalid={!selectedUnitId}> - {} - {Array.from(compatibleUnits).map(unit => { - return (); - })} - {Array.from(incompatibleUnits).map(unit => { - return (); - })} + invalid={!unitIsSelected}> + { + + } + { + Array.from(compatibleUnits).map(unit => + + ) + } {/* default graphic unit input */} - - + {translate('defaultGraphicUnit')} + + { handleNumberChange(e); - setSelectedGraphicId(true); }} > - {} - {Array.from(compatibleGraphicUnits).map(unit => { - return (); - })} - {Array.from(incompatibleGraphicUnits).map(unit => { - return (); - })} + + { + Array.from(compatibleGraphicUnits).map(unit => + + ) + } + { + Array.from(incompatibleGraphicUnits).map(unit => + + + ) + } @@ -280,10 +274,7 @@ export default function CreateMeterModalComponent() { {/* Enabled input */} - handleBooleanChange(e)}> {Object.keys(TrueFalseType).map(key => { @@ -294,10 +285,7 @@ export default function CreateMeterModalComponent() { {/* Displayable input */} - handleBooleanChange(e)} invalid={meterDetails.displayable && meterDetails.unitId === -99}> @@ -314,10 +302,7 @@ export default function CreateMeterModalComponent() { {/* Meter type input */} - handleStringChange(e)} invalid={meterDetails.meterType === ''}> @@ -339,10 +324,7 @@ export default function CreateMeterModalComponent() { {/* Meter reading frequency */} - handleStringChange(e)} value={meterDetails.readingFrequency} @@ -356,10 +338,7 @@ export default function CreateMeterModalComponent() { {/* URL input */} - handleStringChange(e)} value={meterDetails.url} /> @@ -367,10 +346,7 @@ export default function CreateMeterModalComponent() { {/* GPS input */} - handleStringChange(e)} value={meterDetails.gps} /> @@ -379,10 +355,7 @@ export default function CreateMeterModalComponent() { {/* Area input */} - handleNumberChange(e)} @@ -394,10 +367,7 @@ export default function CreateMeterModalComponent() { {/* meter area unit input */} - handleStringChange(e)} invalid={meterDetails.area > 0 && meterDetails.areaUnit === AreaUnitType.none}> @@ -413,10 +383,7 @@ export default function CreateMeterModalComponent() { {/* note input */} - handleStringChange(e)} value={meterDetails.note} placeholder='Note' /> @@ -425,10 +392,7 @@ export default function CreateMeterModalComponent() { {/* cumulative input */} - handleBooleanChange(e)}> {Object.keys(TrueFalseType).map(key => { @@ -439,10 +403,7 @@ export default function CreateMeterModalComponent() { {/* cumulativeReset input */} - handleBooleanChange(e)}> {Object.keys(TrueFalseType).map(key => { @@ -455,11 +416,7 @@ export default function CreateMeterModalComponent() { {/* cumulativeResetStart input */} - handleStringChange(e)} value={meterDetails.cumulativeResetStart} placeholder='HH:MM:SS' /> @@ -467,10 +424,7 @@ export default function CreateMeterModalComponent() { {/* cumulativeResetEnd input */} - handleStringChange(e)} value={meterDetails.cumulativeResetEnd} @@ -481,11 +435,7 @@ export default function CreateMeterModalComponent() { {/* endOnlyTime input */} - handleBooleanChange(e)}> {Object.keys(TrueFalseType).map(key => { return (); @@ -495,10 +445,7 @@ export default function CreateMeterModalComponent() { {/* readingGap input */} - handleNumberChange(e)} min='0' defaultValue={meterDetails.readingGap} @@ -512,10 +459,7 @@ export default function CreateMeterModalComponent() { {/* readingVariation input */} - handleNumberChange(e)} min='0' defaultValue={meterDetails.readingVariation} @@ -527,10 +471,7 @@ export default function CreateMeterModalComponent() { {/* readingDuplication input */} - handleNumberChange(e)} step='1' min='1' @@ -546,10 +487,7 @@ export default function CreateMeterModalComponent() { {/* timeSort input */} - handleStringChange(e)}> {Object.keys(MeterTimeSortType).map(key => { @@ -569,10 +507,7 @@ export default function CreateMeterModalComponent() { {/* minVal input */} - handleNumberChange(e)} min={MIN_VAL} max={meterDetails.maxVal} @@ -585,10 +520,7 @@ export default function CreateMeterModalComponent() { {/* maxVal input */} - handleNumberChange(e)} min={meterDetails.minVal} max={MAX_VAL} @@ -603,10 +535,7 @@ export default function CreateMeterModalComponent() { {/* minDate input */} - handleStringChange(e)} placeholder='YYYY-MM-DD HH:MM:SS' @@ -621,10 +550,7 @@ export default function CreateMeterModalComponent() { {/* maxDate input */} - handleStringChange(e)} @@ -642,10 +568,7 @@ export default function CreateMeterModalComponent() { {/* maxError input */} - handleNumberChange(e)} min='0' max={MAX_ERRORS} @@ -657,10 +580,7 @@ export default function CreateMeterModalComponent() { - handleBooleanChange(e)}> {Object.keys(TrueFalseType).map(key => { @@ -673,20 +593,14 @@ export default function CreateMeterModalComponent() { {/* reading input */} - handleNumberChange(e)} defaultValue={meterDetails.reading} /> {/* startTimestamp input */} - handleStringChange(e)} placeholder='YYYY-MM-DD HH:MM:SS' @@ -697,10 +611,7 @@ export default function CreateMeterModalComponent() { {/* endTimestamp input */} - handleStringChange(e)} placeholder='YYYY-MM-DD HH:MM:SS' @@ -709,10 +620,7 @@ export default function CreateMeterModalComponent() { {/* previousEnd input */} - handleStringChange(e)} placeholder='YYYY-MM-DD HH:MM:SS' @@ -726,7 +634,7 @@ export default function CreateMeterModalComponent() { {/* On click calls the function handleSaveChanges in this component */} - @@ -735,49 +643,5 @@ export default function CreateMeterModalComponent() { ); } -/* Create Meter Validation: - Name cannot be blank - Area must be positive or zero - If area is nonzero, area unit must be set - Reading Gap must be greater than zero - Reading Variation must be greater than zero - Reading Duplication must be between 1 and 9 - Reading frequency cannot be blank - Unit and Default Graphic Unit must be set (can be to no unit) - Meter type must be set - If displayable is true and unitId is set to -99, warn admin - Minimum Value cannot bigger than Maximum Value - Minimum Value and Maximum Value must be between valid input - Minimum Date and Maximum cannot be blank - Minimum Date cannot be after Maximum Date - Minimum Date and Maximum Value must be between valid input - Maximum No of Error must be between 0 and valid input -*/ -const isValidCreateMeter = (meterDetails: MeterData) => { - return meterDetails.name !== '' && - (meterDetails.area === 0 || (meterDetails.area > 0 && meterDetails.areaUnit !== AreaUnitType.none)) && - meterDetails.readingGap >= 0 && - meterDetails.readingVariation >= 0 && - (meterDetails.readingDuplication >= 1 && meterDetails.readingDuplication <= 9) && - meterDetails.readingFrequency !== '' && - meterDetails.unitId !== -99 && - meterDetails.defaultGraphicUnit !== -999 && - meterDetails.meterType !== '' && - meterDetails.minVal >= MIN_VAL && - meterDetails.minVal <= meterDetails.maxVal && - meterDetails.maxVal <= MAX_VAL && - moment(meterDetails.minDate).isValid() && - moment(meterDetails.maxDate).isValid() && - moment(meterDetails.minDate).isSameOrAfter(MIN_DATE_MOMENT) && - moment(meterDetails.minDate).isSameOrBefore(moment(meterDetails.maxDate)) && - moment(meterDetails.maxDate).isSameOrBefore(MAX_DATE_MOMENT) && - (meterDetails.maxError >= 0 && meterDetails.maxError <= MAX_ERRORS); -}; -const MIN_VAL = Number.MIN_SAFE_INTEGER; -const MAX_VAL = Number.MAX_SAFE_INTEGER; -const MIN_DATE_MOMENT = moment(0).utc(); -const MAX_DATE_MOMENT = moment(0).utc().add(5000, 'years'); -const MIN_DATE = MIN_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); -const MAX_DATE = MAX_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); -const MAX_ERRORS = 75; + diff --git a/src/client/app/components/meters/EditMeterModalComponent.tsx b/src/client/app/components/meters/EditMeterModalComponent.tsx index 09806e7f5..40ddcf8e3 100644 --- a/src/client/app/components/meters/EditMeterModalComponent.tsx +++ b/src/client/app/components/meters/EditMeterModalComponent.tsx @@ -11,7 +11,11 @@ import { Button, Col, Container, FormFeedback, FormGroup, Input, Label, Modal, M import { metersApi, selectMeterById } from '../../redux/api/metersApi'; import { selectUnitDataById } from '../../redux/api/unitsApi'; import { useAppSelector } from '../../redux/reduxHooks'; -import { selectGraphicUnitCompatibility } from '../../redux/selectors/adminSelectors'; +import { + MAX_DATE, MAX_DATE_MOMENT, MAX_ERRORS, + MAX_VAL, MIN_DATE, MIN_DATE_MOMENT, MIN_VAL, + selectGraphicUnitCompatibility +} from '../../redux/selectors/adminSelectors'; import '../../styles/modal.css'; import { tooltipBaseStyle } from '../../styles/modalStyle'; import { TrueFalseType } from '../../types/items'; @@ -696,13 +700,7 @@ export default function EditMeterModalComponent(props: EditMeterModalComponentPr } -const MIN_VAL = Number.MIN_SAFE_INTEGER; -const MAX_VAL = Number.MAX_SAFE_INTEGER; -const MIN_DATE_MOMENT = moment(0).utc(); -const MAX_DATE_MOMENT = moment(0).utc().add(5000, 'years'); -const MIN_DATE = MIN_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); -const MAX_DATE = MAX_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); -const MAX_ERRORS = 75; + const tooltipStyle = { ...tooltipBaseStyle, // Only an admin can edit a meter. diff --git a/src/client/app/redux/selectors/adminSelectors.ts b/src/client/app/redux/selectors/adminSelectors.ts index 761744821..b806bbfbd 100644 --- a/src/client/app/redux/selectors/adminSelectors.ts +++ b/src/client/app/redux/selectors/adminSelectors.ts @@ -3,10 +3,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import * as _ from 'lodash'; +import * as moment from 'moment'; import { selectConversionsDetails } from '../../redux/api/conversionsApi'; import { selectAllGroups } from '../../redux/api/groupsApi'; import { selectAllMeters, selectMeterById } from '../../redux/api/metersApi'; -import { PreferenceRequestItem } from '../../types/items'; +import { selectAdminPreferences } from '../../redux/slices/adminSlice'; import { ConversionData } from '../../types/redux/conversions'; import { MeterData, MeterTimeSortType } from '../../types/redux/meters'; import { UnitData, UnitType } from '../../types/redux/units'; @@ -15,33 +16,16 @@ import { AreaUnitType } from '../../utils/getAreaUnitConversion'; import { noUnitTranslated, potentialGraphicUnits } from '../../utils/input'; import translate from '../../utils/translate'; import { selectAllUnits, selectUnitDataById } from '../api/unitsApi'; -import { selectAdminState } from '../slices/adminSlice'; import { selectVisibleMetersAndGroups } from './authVisibilitySelectors'; import { createAppSelector } from './selectors'; -export const selectAdminPreferences = createAppSelector( - [selectAdminState], - (adminState): PreferenceRequestItem => ({ - displayTitle: adminState.displayTitle, - defaultChartToRender: adminState.defaultChartToRender, - defaultBarStacking: adminState.defaultBarStacking, - defaultLanguage: adminState.defaultLanguage, - defaultTimezone: adminState.defaultTimezone, - defaultWarningFileSize: adminState.defaultWarningFileSize, - defaultFileSizeLimit: adminState.defaultFileSizeLimit, - defaultAreaNormalization: adminState.defaultAreaNormalization, - defaultAreaUnit: adminState.defaultAreaUnit, - defaultMeterReadingFrequency: adminState.defaultMeterReadingFrequency, - defaultMeterMinimumValue: adminState.defaultMeterMinimumValue, - defaultMeterMaximumValue: adminState.defaultMeterMaximumValue, - defaultMeterMinimumDate: adminState.defaultMeterMinimumDate, - defaultMeterMaximumDate: adminState.defaultMeterMaximumDate, - defaultMeterReadingGap: adminState.defaultMeterReadingGap, - defaultMeterMaximumErrors: adminState.defaultMeterMaximumErrors, - defaultMeterDisableChecks: adminState.defaultMeterDisableChecks, - defaultHelpUrl: adminState.defaultHelpUrl - }) -); +export const MIN_VAL = Number.MIN_SAFE_INTEGER; +export const MAX_VAL = Number.MAX_SAFE_INTEGER; +export const MIN_DATE_MOMENT = moment(0).utc(); +export const MAX_DATE_MOMENT = moment(0).utc().add(5000, 'years'); +export const MIN_DATE = MIN_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); +export const MAX_DATE = MAX_DATE_MOMENT.format('YYYY-MM-DD HH:mm:ssZ'); +export const MAX_ERRORS = 75; export const selectPossibleGraphicUnits = createAppSelector( selectUnitDataById, @@ -164,6 +148,58 @@ export const selectGraphicUnitCompatibility = createAppSelector( return { compatibleGraphicUnits, incompatibleGraphicUnits, compatibleUnits, incompatibleUnits }; } ); +// Test Selector to preserve old. +export const selectCreateMeterUnitCompatibility = createAppSelector( + [ + selectPossibleGraphicUnits, + selectPossibleMeterUnits, + (_state, meterDetails: MeterData) => meterDetails.unitId + ], + (possibleGraphicUnits, possibleMeterUnits, unitId) => { + console.log( + 'possibleGraphicUnits', possibleGraphicUnits, + '\npossibleMeterUnits', possibleGraphicUnits + ); + // Units always Editable, and default Grahpic changes based on this. + const compatibleUnits = new Set(possibleMeterUnits); + // Units incompatible with currently selected graphic unit + + const compatibleGraphicUnits = new Set(); + // Graphic units incompatible with currently selected unit + const incompatibleGraphicUnits = new Set(); + // No unit selected All compatible now. + // possibleMeterUnits.forEach(unit => compatibleUnits.add(unit)); + if (unitId === -999) { + // Initial condition. When no unit selected + // No graphic units will be available for selection + possibleGraphicUnits.forEach(unit => incompatibleGraphicUnits.add(unit)); + } + else if (unitId != -99) { + // If unit is not 'no unit' + // Find all units compatible with the selected unit + const unitsCompatibleWithSelectedUnit = unitsCompatibleWithUnit(unitId); + possibleGraphicUnits.forEach(unit => { + // If current graphic unit exists in the set of compatible graphic units OR if the current graphic unit is 'no unit' + if (unitsCompatibleWithSelectedUnit.has(unit.id) || unit.id === -99) { + compatibleGraphicUnits.add(unit); + } else { + incompatibleGraphicUnits.add(unit); + } + }); + } else { + possibleGraphicUnits.forEach(unit => { + // OED does not allow a default graphic unit if there is no unit so it must be -99. + // No unit is selected + // Only -99 is allowed. + unit.id === -99 + ? compatibleGraphicUnits.add(unit) + : incompatibleGraphicUnits.add(unit); + }); + } + + return { compatibleUnits, compatibleGraphicUnits, incompatibleGraphicUnits }; + } +); export const selectIsValidConversion = createAppSelector( @@ -255,8 +291,8 @@ export const selectDefaultCreateMeterValues = createAppSelector( gps: '', // Defaults of -999 (not to be confused with -99 which is no unit) // Purely for allowing the default select to be "select a ..." - unitId: -99, - defaultGraphicUnit: -99, + unitId: -999, + defaultGraphicUnit: -999, note: '', cumulative: false, cumulativeReset: false, @@ -302,3 +338,54 @@ export const selectDefaultCreateConversionValues = createAppSelector( return defaultValues; } ); + +/* Create Meter Validation: + Name cannot be blank + Area must be positive or zero + If area is nonzero, area unit must be set + Reading Gap must be greater than zero + Reading Variation must be greater than zero + Reading Duplication must be between 1 and 9 + Reading frequency cannot be blank + Unit and Default Graphic Unit must be set (can be to no unit) + Meter type must be set + If displayable is true and unitId is set to -99, warn admin + Minimum Value cannot bigger than Maximum Value + Minimum Value and Maximum Value must be between valid input + Minimum Date and Maximum cannot be blank + Minimum Date cannot be after Maximum Date + Minimum Date and Maximum Value must be between valid input + Maximum No of Error must be between 0 and valid input +*/ +export const isValidCreateMeter = createAppSelector( + [ + selectCreateMeterUnitCompatibility, + (_state, meterDetails: MeterData) => meterDetails + ], + (graphicUnitCompatibility, meterDetails) => { + const { compatibleGraphicUnits } = graphicUnitCompatibility; + const defaultGraphicUnitIsValid = Boolean(Array.from(compatibleGraphicUnits).find(unit => unit.id === meterDetails.defaultGraphicUnit)); + const meterIsValid = defaultGraphicUnitIsValid && + meterDetails.name !== '' && + (meterDetails.area === 0 || (meterDetails.area > 0 && meterDetails.areaUnit !== AreaUnitType.none)) && + meterDetails.readingGap >= 0 && + meterDetails.readingVariation >= 0 && + (meterDetails.readingDuplication >= 1 && meterDetails.readingDuplication <= 9) && + meterDetails.readingFrequency !== '' && + meterDetails.unitId !== -999 && + meterDetails.defaultGraphicUnit !== -999 && + meterDetails.meterType !== '' && + meterDetails.minVal >= MIN_VAL && + meterDetails.minVal <= meterDetails.maxVal && + meterDetails.maxVal <= MAX_VAL && + moment(meterDetails.minDate).isValid() && + moment(meterDetails.maxDate).isValid() && + moment(meterDetails.minDate).isSameOrAfter(MIN_DATE_MOMENT) && + moment(meterDetails.minDate).isSameOrBefore(moment(meterDetails.maxDate)) && + moment(meterDetails.maxDate).isSameOrBefore(MAX_DATE_MOMENT) && + (meterDetails.maxError >= 0 && meterDetails.maxError <= MAX_ERRORS); + return { meterIsValid, defaultGraphicUnitIsValid }; + + } +); + diff --git a/src/client/app/redux/slices/adminSlice.ts b/src/client/app/redux/slices/adminSlice.ts index b260f6bdf..23fc73795 100644 --- a/src/client/app/redux/slices/adminSlice.ts +++ b/src/client/app/redux/slices/adminSlice.ts @@ -11,6 +11,7 @@ import { ChartTypes } from '../../types/redux/graph'; import { LanguageTypes } from '../../types/redux/i18n'; import { durationFormat } from '../../utils/durationFormat'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; +import { createAppSelector } from '../../redux/selectors/selectors'; export const defaultAdminState: AdminState = { displayTitle: '', @@ -171,3 +172,28 @@ export const { selectDisplayTitle, selectBaseHelpUrl } = adminSlice.selectors; + + +export const selectAdminPreferences = createAppSelector( + [selectAdminState], + (adminState): PreferenceRequestItem => ({ + displayTitle: adminState.displayTitle, + defaultChartToRender: adminState.defaultChartToRender, + defaultBarStacking: adminState.defaultBarStacking, + defaultLanguage: adminState.defaultLanguage, + defaultTimezone: adminState.defaultTimezone, + defaultWarningFileSize: adminState.defaultWarningFileSize, + defaultFileSizeLimit: adminState.defaultFileSizeLimit, + defaultAreaNormalization: adminState.defaultAreaNormalization, + defaultAreaUnit: adminState.defaultAreaUnit, + defaultMeterReadingFrequency: adminState.defaultMeterReadingFrequency, + defaultMeterMinimumValue: adminState.defaultMeterMinimumValue, + defaultMeterMaximumValue: adminState.defaultMeterMaximumValue, + defaultMeterMinimumDate: adminState.defaultMeterMinimumDate, + defaultMeterMaximumDate: adminState.defaultMeterMaximumDate, + defaultMeterReadingGap: adminState.defaultMeterReadingGap, + defaultMeterMaximumErrors: adminState.defaultMeterMaximumErrors, + defaultMeterDisableChecks: adminState.defaultMeterDisableChecks, + defaultHelpUrl: adminState.defaultHelpUrl + }) +); \ No newline at end of file From 8f9601d99ccbfca3801a285627c27fc281107df3 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Thu, 29 Feb 2024 16:45:13 -0800 Subject: [PATCH 115/131] cik refresh restored. --- .../components/meters/CreateMeterModalComponent.tsx | 10 +++++++--- src/client/app/redux/api/baseApi.ts | 3 ++- src/client/app/redux/api/conversionsApi.ts | 6 +++--- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/client/app/components/meters/CreateMeterModalComponent.tsx b/src/client/app/components/meters/CreateMeterModalComponent.tsx index f90adb93a..4c8eec2f3 100644 --- a/src/client/app/components/meters/CreateMeterModalComponent.tsx +++ b/src/client/app/components/meters/CreateMeterModalComponent.tsx @@ -583,9 +583,13 @@ export default function CreateMeterModalComponent() { handleBooleanChange(e)}> - {Object.keys(TrueFalseType).map(key => { - return (); - })} + { + Object.keys(TrueFalseType).map(key => + + ) + } diff --git a/src/client/app/redux/api/baseApi.ts b/src/client/app/redux/api/baseApi.ts index d210f5414..2df30a073 100644 --- a/src/client/app/redux/api/baseApi.ts +++ b/src/client/app/redux/api/baseApi.ts @@ -33,7 +33,8 @@ export const baseApi = createApi({ 'Preferences', 'Users', 'ConversionDetails', - 'Units' + 'Units', + 'Cik' ], // Initially no defined endpoints, Use rtk query's injectEndpoints endpoints: () => ({}) diff --git a/src/client/app/redux/api/conversionsApi.ts b/src/client/app/redux/api/conversionsApi.ts index 98a20885d..eb595e6c8 100644 --- a/src/client/app/redux/api/conversionsApi.ts +++ b/src/client/app/redux/api/conversionsApi.ts @@ -14,7 +14,8 @@ export const conversionsApi = baseApi.injectEndpoints({ providesTags: ['ConversionDetails'] }), getCikDetails: builder.query({ - query: () => 'api/ciks' + query: () => 'api/ciks', + providesTags: ['Cik'] }), addConversion: builder.mutation({ query: conversion => ({ @@ -83,8 +84,7 @@ export const conversionsApi = baseApi.injectEndpoints({ method: 'POST', body: { redoCik, refreshReadingViews } }), - // TODO check behavior with maintainers, always invalidates, should be conditional? - invalidatesTags: ['ConversionDetails'] + invalidatesTags: ['ConversionDetails', 'Cik'] }) }) }); From 6edcbf6bb5f78c3e91482b271183d283e26ed583 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Thu, 29 Feb 2024 17:20:08 -0800 Subject: [PATCH 116/131] Radar Include Rate Menu --- src/client/app/components/GraphicRateMenuComponent.tsx | 8 +++++++- src/client/app/redux/selectors/adminSelectors.ts | 4 ---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/client/app/components/GraphicRateMenuComponent.tsx b/src/client/app/components/GraphicRateMenuComponent.tsx index dee7161a2..163d51965 100644 --- a/src/client/app/components/GraphicRateMenuComponent.tsx +++ b/src/client/app/components/GraphicRateMenuComponent.tsx @@ -37,7 +37,13 @@ export default function GraphicRateMenuComponent() { } } // Also don't show if not the line graphic, or three-d. - if (graphState.chartToRender !== ChartTypes.line && graphState.chartToRender !== ChartTypes.threeD) { + const displayOnChartType: ChartTypes[] = [ + ChartTypes.line, + ChartTypes.threeD, + ChartTypes.radar + ]; + + if (!displayOnChartType.includes(graphState.chartToRender)) { shouldRender = false; } // Array of select options created from the rates diff --git a/src/client/app/redux/selectors/adminSelectors.ts b/src/client/app/redux/selectors/adminSelectors.ts index b806bbfbd..509fc16d9 100644 --- a/src/client/app/redux/selectors/adminSelectors.ts +++ b/src/client/app/redux/selectors/adminSelectors.ts @@ -156,10 +156,6 @@ export const selectCreateMeterUnitCompatibility = createAppSelector( (_state, meterDetails: MeterData) => meterDetails.unitId ], (possibleGraphicUnits, possibleMeterUnits, unitId) => { - console.log( - 'possibleGraphicUnits', possibleGraphicUnits, - '\npossibleMeterUnits', possibleGraphicUnits - ); // Units always Editable, and default Grahpic changes based on this. const compatibleUnits = new Set(possibleMeterUnits); // Units incompatible with currently selected graphic unit From f31b2440a40cef8634dfe44b6da115acbedb94a4 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Sun, 3 Mar 2024 11:13:42 -0800 Subject: [PATCH 117/131] Address PR Comments. --- .../app/components/HeaderButtonsComponent.tsx | 82 ++++++------------- src/client/app/components/HeaderComponent.tsx | 3 +- .../ReadingsPerDaySelectComponent.tsx | 33 ++++---- src/client/app/redux/api/authApi.ts | 2 - src/client/app/redux/api/baseApi.ts | 2 - src/client/app/redux/api/versionApi.ts | 5 +- .../app/redux/selectors/threeDSelectors.ts | 2 +- src/client/app/redux/slices/adminSlice.ts | 10 ++- .../app/redux/slices/currentUserSlice.ts | 21 +++-- src/client/app/redux/slices/graphSlice.ts | 7 +- src/client/app/utils/hasPermissions.ts | 27 ------ 11 files changed, 70 insertions(+), 124 deletions(-) delete mode 100644 src/client/app/utils/hasPermissions.ts diff --git a/src/client/app/components/HeaderButtonsComponent.tsx b/src/client/app/components/HeaderButtonsComponent.tsx index f0e707b56..b1e487d47 100644 --- a/src/client/app/components/HeaderButtonsComponent.tsx +++ b/src/client/app/components/HeaderButtonsComponent.tsx @@ -8,18 +8,17 @@ 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 { selectOptionsVisibility, toggleOptionsVisibility } from '../redux/slices/appStateSlice'; +import { clearGraphHistory } from '../redux/actions/extraActions'; import { authApi } from '../redux/api/authApi'; import { selectOEDVersion } from '../redux/api/versionApi'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; +import { selectHelpUrl } from '../redux/slices/adminSlice'; +import { selectOptionsVisibility, toggleOptionsVisibility } from '../redux/slices/appStateSlice'; +import { selectHasRolePermissions, selectIsAdmin, selectIsLoggedIn } from '../redux/slices/currentUserSlice'; import { UserRole } from '../types/items'; -import { hasPermissions, isRoleAdmin } from '../utils/hasPermissions'; import translate from '../utils/translate'; import LanguageSelectorComponent from './LanguageSelectorComponent'; import TooltipMarkerComponent from './TooltipMarkerComponent'; -import { selectCurrentUser } from '../redux/slices/currentUserSlice'; -import { selectBaseHelpUrl } from '../redux/slices/adminSlice'; -import { clearGraphHistory } from '../redux/actions/extraActions'; /** * React Component that defines the header buttons at the top of a page @@ -33,32 +32,26 @@ export default function HeaderButtonsComponent() { // OED version is needed for help redirect const version = useAppSelector(selectOEDVersion); - const baseHelpUrl = useAppSelector(selectBaseHelpUrl); - // Help URL location - const helpUrl = baseHelpUrl + version; + const helpUrl = useAppSelector(selectHelpUrl); // options help const optionsHelp = helpUrl + '/optionsMenu.html'; + const loggedInAsAdmin = useAppSelector(selectIsAdmin); + const loggedIn = useAppSelector(selectIsLoggedIn); + const csvPermission = useAppSelector(state => selectHasRolePermissions(state, UserRole.CSV)); + // whether to collapse options when on graphs page + const optionsVisibility = useAppSelector(selectOptionsVisibility); + console.log('loggedInAsAdmin', loggedInAsAdmin, 'loggedIn', loggedIn, 'csvPermission', csvPermission); // This is the state model for rendering this page. const defaultState = { // All these values should update before user interacts with them so hide everything until the useEffects // set to what is desired. // The styles control if an item is seen at all. - adminViewableLinkStyle: { - display: 'none' - } as React.CSSProperties, - csvViewableLinkStyle: { - display: 'none' - } as React.CSSProperties, - loginLinkStyle: { - display: 'none' - } as React.CSSProperties, - logoutLinkStyle: { - display: 'none' - } as React.CSSProperties, - showOptionsStyle: { - display: 'none' - } as React.CSSProperties, + adminViewableLinkStyle: { display: 'none' } as React.CSSProperties, + csvViewableLinkStyle: { display: 'none' } as React.CSSProperties, + loginLinkStyle: { display: 'none' } as React.CSSProperties, + logoutLinkStyle: { display: 'none' } as React.CSSProperties, + showOptionsStyle: { display: 'none' } as React.CSSProperties, // The should ones tell if see but not selectable. shouldHomeButtonDisabled: true, shouldAdminButtonDisabled: true, @@ -76,14 +69,11 @@ export default function HeaderButtonsComponent() { // Local state for rendering. const [state, setState] = useState(defaultState); - // Information on the current user. - const { profile: currentUser } = useAppSelector(selectCurrentUser); // Tracks unsaved changes. // TODO Re-implement AFTER RTK Migration - // const unsavedChangesState = useAppSelector(state => state.unsavedWarning.hasUnsavedChanges); + // hard-coded for the time being. Rework w/admin pages const unsavedChangesState = false; - // whether to collapse options when on graphs page - const optionsVisibility = useAppSelector(selectOptionsVisibility); + // Must update in case the version was not set when the page was loaded. useEffect(() => { @@ -110,43 +100,23 @@ export default function HeaderButtonsComponent() { // This updates which items are hidden based on the login status. useEffect(() => { - // True if you are an admin. - let loggedInAsAdmin: boolean; - // What role you have or null if not logged in. // We can get the admin state from the role but separate the two. - let role: UserRole | null; - let currentMenuTitle: string; - if (currentUser !== null) { - // There is a current user so gets its information - loggedInAsAdmin = isRoleAdmin(currentUser.role); - role = currentUser.role; - // The menu title has logout. - currentMenuTitle = translate('page.choice.logout'); - } else { - // You are not logged in. - loggedInAsAdmin = false; - role = null; - // The menu title has login. - currentMenuTitle = translate('page.choice.login'); - } - // If you have a role then check if it is CSV. - const renderCSVButton = Boolean(role && hasPermissions(role, UserRole.CSV)); - // If no role then not logged in so show link to log in. - const renderLoginButton = role === null; - // If an admin then show these items, otherwise hide them. + const currentMenuTitle = loggedIn ? translate('page.choice.logout') : translate('page.choice.login'); const currentAdminViewableLinkStyle = { + // If an admin then show these items, otherwise hide them. + // If no role then not logged in so show link to log in. display: loggedInAsAdmin ? 'block' : 'none' }; // Similar but need to have CSV permissions. const currentCsvViewableLinkStyle: React.CSSProperties = { - display: renderCSVButton ? 'block' : 'none' + display: csvPermission ? 'block' : 'none' }; // Show login if not and logout if you are. const currentLoginLinkStyle = { - display: renderLoginButton ? 'block' : 'none' + display: !loggedIn ? 'block' : 'none' }; const currentLogoutLinkStyle = { - display: !renderLoginButton ? 'block' : 'none' + display: loggedIn ? 'block' : 'none' }; const currentShowOptionsStyle = { display: pathname === '/' ? 'block' : 'none' @@ -165,7 +135,7 @@ export default function HeaderButtonsComponent() { pageChoicesHelp: currentPageChoicesHelp, showOptionsStyle: currentShowOptionsStyle })); - }, [pathname, currentUser, helpUrl]); + }, [pathname, helpUrl, loggedIn, csvPermission, loggedInAsAdmin]); // Handle actions on logout. const handleLogOut = () => { @@ -177,7 +147,7 @@ export default function HeaderButtonsComponent() { logout(); } }; - + console.log(state); return (
diff --git a/src/client/app/components/HeaderComponent.tsx b/src/client/app/components/HeaderComponent.tsx index 8d34db7a7..875770e32 100644 --- a/src/client/app/components/HeaderComponent.tsx +++ b/src/client/app/components/HeaderComponent.tsx @@ -9,13 +9,14 @@ import { useAppSelector } from '../redux/reduxHooks'; import HeaderButtonsComponent from './HeaderButtonsComponent'; import LogoComponent from './LogoComponent'; import MenuModalComponent from './MenuModalComponent'; +import { selectDisplayTitle } from '../redux/slices/adminSlice'; /** * React component that controls the header strip at the top of all pages * @returns header element */ export default function HeaderComponent() { - const siteTitle = useAppSelector(state => state.admin.displayTitle); + const siteTitle = useAppSelector(selectDisplayTitle); const showOptions = useAppSelector(selectOptionsVisibility); const { pathname } = useLocation(); diff --git a/src/client/app/components/ReadingsPerDaySelectComponent.tsx b/src/client/app/components/ReadingsPerDaySelectComponent.tsx index 2b4bea1bc..3fb74c305 100644 --- a/src/client/app/components/ReadingsPerDaySelectComponent.tsx +++ b/src/client/app/components/ReadingsPerDaySelectComponent.tsx @@ -39,24 +39,6 @@ export default function ReadingsPerDaySelect() { } - // Return normal interval - // return readingInterval; - - // Iterate over readingInterval enum to create select option - const options = Object.values(ReadingInterval) - // Filter strings as to only get integer values from typescript's reverse mapping of enums - .filter(value => !isNaN(Number(value)) && value !== ReadingInterval.Incompatible) - .map(value => { - // Length of interval readings in hours - const intervalLength = Number(value); - return { - // readingInterval Enum inversely corresponds to the hour interval for readings. - // (24 hours a day) / intervalLength, e.g, 1 hour intervals give 24 readings per day - label: String((24 / intervalLength)), - value: intervalLength - } as ReadingsPerDayOption; - }); - // Use the selectedOption enum value to update threeD State const onSelectChange = (selectedOption: ReadingsPerDayOption) => dispatch(updateThreeDReadingInterval(selectedOption.value)); @@ -95,3 +77,18 @@ interface ReadingsPerDayOption { label: string; value: ReadingInterval; } + +// Iterate over readingInterval enum to create select option +const options = Object.values(ReadingInterval) + // Filter strings as to only get integer values from typescript's reverse mapping of enums + .filter(value => !isNaN(Number(value)) && value !== ReadingInterval.Incompatible) + .map(value => { + // Length of interval readings in hours + const intervalLength = Number(value); + return { + // readingInterval Enum inversely corresponds to the hour interval for readings. + // (24 hours a day) / intervalLength, e.g, 1 hour intervals give 24 readings per day + label: String((24 / intervalLength)), + value: intervalLength + } as ReadingsPerDayOption; + }); diff --git a/src/client/app/redux/api/authApi.ts b/src/client/app/redux/api/authApi.ts index 43b8fc60e..edac20912 100644 --- a/src/client/app/redux/api/authApi.ts +++ b/src/client/app/redux/api/authApi.ts @@ -24,8 +24,6 @@ export const authApi = baseApi.injectEndpoints({ // 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'] - // Listeners for this query (ExtraReducers): - // currentUserSlice->MatchFulfilled }), verifyToken: builder.mutation<{ success: boolean }, string>({ query: token => ({ diff --git a/src/client/app/redux/api/baseApi.ts b/src/client/app/redux/api/baseApi.ts index 2df30a073..863b80ad4 100644 --- a/src/client/app/redux/api/baseApi.ts +++ b/src/client/app/redux/api/baseApi.ts @@ -38,6 +38,4 @@ export const baseApi = createApi({ ], // Initially no defined endpoints, Use rtk query's injectEndpoints endpoints: () => ({}) - // Defaults to 60 seconds or 1 minute - // keepUnusedDataFor: 60 }); diff --git a/src/client/app/redux/api/versionApi.ts b/src/client/app/redux/api/versionApi.ts index 39fa7be7c..114c81a06 100644 --- a/src/client/app/redux/api/versionApi.ts +++ b/src/client/app/redux/api/versionApi.ts @@ -16,7 +16,8 @@ export const versionApi = baseApi.injectEndpoints({ export const selectVersion = versionApi.endpoints.getVersion.select(); export const selectOEDVersion = createSelector( selectVersion, - ({ data: version }) => { - return version ?? ''; + ({ data: version = 'v1.0.0' }) => { + // default to v1.0.0 when data in flight, or error + return version; } ); diff --git a/src/client/app/redux/selectors/threeDSelectors.ts b/src/client/app/redux/selectors/threeDSelectors.ts index 00391f128..0f4cc025a 100644 --- a/src/client/app/redux/selectors/threeDSelectors.ts +++ b/src/client/app/redux/selectors/threeDSelectors.ts @@ -21,7 +21,7 @@ export const selectThreeDComponentInfo = createSelector( selectGroupDataById, (id, meterOrGroup, meterDataById, groupDataById) => { // Default Values - let meterOrGroupName = 'Unselected Meter or Group'; + let meterOrGroupName = ''; let isAreaCompatible = true; if (id && meterDataById[id]) { diff --git a/src/client/app/redux/slices/adminSlice.ts b/src/client/app/redux/slices/adminSlice.ts index 23fc73795..2dd88d4c8 100644 --- a/src/client/app/redux/slices/adminSlice.ts +++ b/src/client/app/redux/slices/adminSlice.ts @@ -4,14 +4,15 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import * as moment from 'moment'; -import { preferencesApi } from '../api/preferencesApi'; +import { createAppSelector } from '../../redux/selectors/selectors'; import { PreferenceRequestItem } from '../../types/items'; import { AdminState } from '../../types/redux/admin'; import { ChartTypes } from '../../types/redux/graph'; import { LanguageTypes } from '../../types/redux/i18n'; import { durationFormat } from '../../utils/durationFormat'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; -import { createAppSelector } from '../../redux/selectors/selectors'; +import { preferencesApi } from '../api/preferencesApi'; +import { selectOEDVersion } from '../../redux/api/versionApi'; export const defaultAdminState: AdminState = { displayTitle: '', @@ -196,4 +197,9 @@ export const selectAdminPreferences = createAppSelector( defaultMeterDisableChecks: adminState.defaultMeterDisableChecks, defaultHelpUrl: adminState.defaultHelpUrl }) +); + +export const selectHelpUrl = createAppSelector( + [selectBaseHelpUrl, selectOEDVersion], + (baseUrl, version) => baseUrl + version ); \ No newline at end of file diff --git a/src/client/app/redux/slices/currentUserSlice.ts b/src/client/app/redux/slices/currentUserSlice.ts index 388441f5b..b617aa83b 100644 --- a/src/client/app/redux/slices/currentUserSlice.ts +++ b/src/client/app/redux/slices/currentUserSlice.ts @@ -4,11 +4,11 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; -import { authApi } from '../api/authApi'; -import { userApi } from '../api/userApi'; import { UserRole } from '../../types/items'; import { CurrentUserState } from '../../types/redux/currentUser'; import { setToken } from '../../utils/token'; +import { authApi } from '../api/authApi'; +import { userApi } from '../api/userApi'; /* * Defines store interactions when version related actions are dispatched to the store. @@ -46,27 +46,32 @@ export const currentUserSlice = createSlice({ }); }, selectors: { - selectCurrentUser: state => state, + selectCurrentUserState: state => state, + selectIsLoggedIn: state => Boolean(state.profile), + selectCurrentUserProfile: state => state.profile, selectCurrentUserRole: state => state.profile?.role, // Should resolve to a boolean, Typescript doesn't agree so type assertion 'as boolean' selectIsAdmin: state => Boolean(state.token && state.profile?.role === UserRole.ADMIN), - selectHasRolePermissions: (state, role: UserRole): boolean => { + selectHasRolePermissions: (state, desiredRole: UserRole): boolean => { const isAdmin = currentUserSlice.getSelectors().selectIsAdmin(state); const userRole = currentUserSlice.getSelectors().selectCurrentUserRole(state); - return Boolean(isAdmin || (userRole && userRole === role)); + return Boolean(isAdmin || (userRole && userRole === desiredRole)); } } }); export const { - selectCurrentUser, + selectCurrentUserState, selectCurrentUserRole, selectIsAdmin, - selectHasRolePermissions + selectHasRolePermissions, + selectCurrentUserProfile, + selectIsLoggedIn } = currentUserSlice.selectors; export const { setUserToken, clearCurrentUser -} = currentUserSlice.actions; \ No newline at end of file +} = currentUserSlice.actions; + diff --git a/src/client/app/redux/slices/graphSlice.ts b/src/client/app/redux/slices/graphSlice.ts index c40c1aa5a..eab9e7e12 100644 --- a/src/client/app/redux/slices/graphSlice.ts +++ b/src/client/app/redux/slices/graphSlice.ts @@ -164,8 +164,7 @@ export const graphSlice = createSlice({ // if a meter clear meters, else clear groups isAMeter ? current.selectedMeters = [] : current.selectedGroups = []; - } - if (valueRemoved) { + } else if (valueRemoved) { isAMeter = meta.removedValue.meterOrGroup === MeterOrGroup.meters; // An entry was deleted. // Update either selected meters or groups @@ -173,9 +172,7 @@ export const graphSlice = createSlice({ isAMeter ? current.selectedMeters = newMetersOrGroups : current.selectedGroups = newMetersOrGroups; - } - - if (valueAdded) { + } else if (valueAdded) { isAMeter = meta.option?.meterOrGroup === MeterOrGroup.meters; const addedMeterOrGroupUnit = meta.option?.defaultGraphicUnit; // An entry was added, diff --git a/src/client/app/utils/hasPermissions.ts b/src/client/app/utils/hasPermissions.ts deleted file mode 100644 index f3133caa9..000000000 --- a/src/client/app/utils/hasPermissions.ts +++ /dev/null @@ -1,27 +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 { UserRole } from '../types/items'; - -/** - * Checks if the user has the permissions of a given role. - * @param user User role to evaluate - * @param compareTo User role to compare to - * @returns Whether or not the user has the compareTo role - */ -export function hasPermissions(user: UserRole, compareTo: UserRole): boolean { - // Admins always have any other role. - return user === UserRole.ADMIN || user === compareTo; -} - -/** - * Checks if user is an Admin. - * @param user User role to evaluate - * @returns Whether or not user is an admin - */ -export function isRoleAdmin(user: UserRole): boolean { - // TODO Already Converted to a selector - // migrate all references to this method to use selectIsLoggedInAsAdmin from authSelectors.ts - return user === UserRole.ADMIN; -} From f33e63f6befb03c7f8460eaabcfec62a3b6fc5d3 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Sun, 3 Mar 2024 11:15:33 -0800 Subject: [PATCH 118/131] Re-home stable emtpy readings --- src/client/app/components/BarChartComponent.tsx | 8 +++----- src/client/app/components/HeaderButtonsComponent.tsx | 2 -- src/client/app/components/LineChartComponent.tsx | 12 +++++------- src/client/app/components/ThreeDComponent.tsx | 2 +- .../app/components/admin/UsersDetailComponent.tsx | 5 ++--- .../conversion/ConversionsDetailComponent.tsx | 7 ++----- src/client/app/redux/api/conversionsApi.ts | 2 ++ src/client/app/redux/api/readingsApi.ts | 9 +++++++++ src/client/app/redux/api/unitsApi.ts | 3 ++- src/client/app/redux/api/userApi.ts | 5 ++++- 10 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/client/app/components/BarChartComponent.tsx b/src/client/app/components/BarChartComponent.tsx index 50e37220b..5509bb097 100644 --- a/src/client/app/components/BarChartComponent.tsx +++ b/src/client/app/components/BarChartComponent.tsx @@ -9,7 +9,7 @@ import * as React from 'react'; import Plot from 'react-plotly.js'; import { TimeInterval } from '../../../common/TimeInterval'; import { updateSliderRange } from '../redux/actions/extraActions'; -import { readingsApi } from '../redux/api/readingsApi'; +import { readingsApi, stableEmptyBarReadings } from '../redux/api/readingsApi'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { selectPlotlyBarDataFromResult, selectPlotlyBarDeps } from '../redux/selectors/barChartSelectors'; import { selectBarChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; @@ -17,11 +17,9 @@ import { selectBarUnitLabel, selectIsRaw } from '../redux/selectors/plotlyDataSe import { selectSelectedLanguage } from '../redux/slices/appStateSlice'; import { selectBarStacking } from '../redux/slices/graphSlice'; import Locales from '../types/locales'; -import { BarReadings } from '../types/readings'; import translate from '../utils/translate'; import SpinnerComponent from './SpinnerComponent'; -const stableEmptyData: BarReadings = {}; /** * Passes the current redux state of the barchart, and turns it into props for the React * component, which is what will be visible on the page. Makes it possible to access @@ -37,7 +35,7 @@ export default function BarChartComponent() { skip: meterShouldSkip, selectFromResult: ({ data, ...rest }) => ({ ...rest, - data: selectPlotlyBarDataFromResult(data ?? stableEmptyData, barMeterDeps) + data: selectPlotlyBarDataFromResult(data ?? stableEmptyBarReadings, barMeterDeps) }) }); @@ -45,7 +43,7 @@ export default function BarChartComponent() { skip: groupShouldSkip, selectFromResult: ({ data, ...rest }) => ({ ...rest, - data: selectPlotlyBarDataFromResult(data ?? stableEmptyData, barGroupDeps) + data: selectPlotlyBarDataFromResult(data ?? stableEmptyBarReadings, barGroupDeps) }) }); diff --git a/src/client/app/components/HeaderButtonsComponent.tsx b/src/client/app/components/HeaderButtonsComponent.tsx index b1e487d47..5e289288e 100644 --- a/src/client/app/components/HeaderButtonsComponent.tsx +++ b/src/client/app/components/HeaderButtonsComponent.tsx @@ -41,7 +41,6 @@ export default function HeaderButtonsComponent() { const csvPermission = useAppSelector(state => selectHasRolePermissions(state, UserRole.CSV)); // whether to collapse options when on graphs page const optionsVisibility = useAppSelector(selectOptionsVisibility); - console.log('loggedInAsAdmin', loggedInAsAdmin, 'loggedIn', loggedIn, 'csvPermission', csvPermission); // This is the state model for rendering this page. const defaultState = { // All these values should update before user interacts with them so hide everything until the useEffects @@ -147,7 +146,6 @@ export default function HeaderButtonsComponent() { logout(); } }; - console.log(state); return (
diff --git a/src/client/app/components/LineChartComponent.tsx b/src/client/app/components/LineChartComponent.tsx index 4237c358e..01ef4be4b 100644 --- a/src/client/app/components/LineChartComponent.tsx +++ b/src/client/app/components/LineChartComponent.tsx @@ -9,19 +9,17 @@ import * as React from 'react'; import Plot from 'react-plotly.js'; import { TimeInterval } from '../../../common/TimeInterval'; import { updateSliderRange } from '../redux/actions/extraActions'; -import { readingsApi } from '../redux/api/readingsApi'; +import { readingsApi, stableEmptyLineReadings } from '../redux/api/readingsApi'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { selectLineChartQueryArgs } from '../redux/selectors/chartQuerySelectors'; import { selectLineChartDeps, selectPlotlyGroupData, selectPlotlyMeterData } from '../redux/selectors/lineChartSelectors'; import { selectLineUnitLabel } from '../redux/selectors/plotlyDataSelectors'; import { selectSelectedLanguage } from '../redux/slices/appStateSlice'; import Locales from '../types/locales'; -import { LineReadings } from '../types/readings'; import translate from '../utils/translate'; import SpinnerComponent from './SpinnerComponent'; -// Stable reference for when there is not data. Avoids rerenders. -const stableEmptyReadings: LineReadings = {}; + /** * @returns plotlyLine graphic */ @@ -42,16 +40,16 @@ export default function LineChartComponent() { ...rest, // use query data as selector parameter, pass in data dependencies. // Data may still be in transit, so pass a stable empty reference if needed for memoization. - data: selectPlotlyMeterData(data ?? stableEmptyReadings, meterDeps) + data: selectPlotlyMeterData(data ?? stableEmptyLineReadings, meterDeps) }) }); - const { data: groupPlotlyData = stableEmptyReadings, isFetching: groupIsFetching } = readingsApi.useLineQuery(groupArgs, + const { data: groupPlotlyData = stableEmptyLineReadings, isFetching: groupIsFetching } = readingsApi.useLineQuery(groupArgs, { skip: groupShouldSkip, selectFromResult: ({ data, ...rest }) => ({ ...rest, - data: selectPlotlyGroupData(data ?? stableEmptyReadings, groupDeps) + data: selectPlotlyGroupData(data ?? stableEmptyLineReadings, groupDeps) }) }); diff --git a/src/client/app/components/ThreeDComponent.tsx b/src/client/app/components/ThreeDComponent.tsx index 0740094b4..b1e8324c9 100644 --- a/src/client/app/components/ThreeDComponent.tsx +++ b/src/client/app/components/ThreeDComponent.tsx @@ -33,7 +33,7 @@ import Locales from '../types/locales'; */ export default function ThreeDComponent() { const { args, shouldSkipQuery } = useAppSelector(selectThreeDQueryArgs); - const { data, isLoading: isFetching } = readingsApi.endpoints.threeD.useQuery(args, { skip: shouldSkipQuery }); + const { data, isFetching } = readingsApi.endpoints.threeD.useQuery(args, { skip: shouldSkipQuery }); const meterDataById = useAppSelector(selectMeterDataById); const groupDataById = useAppSelector(selectGroupDataById); const unitDataById = useAppSelector(selectUnitDataById); diff --git a/src/client/app/components/admin/UsersDetailComponent.tsx b/src/client/app/components/admin/UsersDetailComponent.tsx index f9386fbf8..e326fb807 100644 --- a/src/client/app/components/admin/UsersDetailComponent.tsx +++ b/src/client/app/components/admin/UsersDetailComponent.tsx @@ -7,7 +7,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import { Button, Input, Table } from 'reactstrap'; import TooltipHelpComponent from '../TooltipHelpComponent'; -import { userApi } from '../../redux/api/userApi'; +import { stableEmptyUsers, userApi } from '../../redux/api/userApi'; import { User, UserRole } from '../../types/items'; import { showErrorNotification, showSuccessNotification } from '../../utils/notifications'; import translate from '../../utils/translate'; @@ -15,8 +15,7 @@ import TooltipMarkerComponent from '../TooltipMarkerComponent'; import { UnsavedWarningComponent } from '../UnsavedWarningComponent'; import CreateUserLinkButtonComponent from './users/CreateUserLinkButtonComponent'; -// Provide a stable empty reference for when data is in flight -const stableEmptyUsers: User[] = []; + /** * Component which shows user details * @returns User Detail element diff --git a/src/client/app/components/conversion/ConversionsDetailComponent.tsx b/src/client/app/components/conversion/ConversionsDetailComponent.tsx index e6802bcaf..41654ed53 100644 --- a/src/client/app/components/conversion/ConversionsDetailComponent.tsx +++ b/src/client/app/components/conversion/ConversionsDetailComponent.tsx @@ -6,16 +6,13 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import SpinnerComponent from '../SpinnerComponent'; import TooltipHelpComponent from '../TooltipHelpComponent'; -import { conversionsApi } from '../../redux/api/conversionsApi'; -import { unitsAdapter, unitsApi } from '../../redux/api/unitsApi'; +import { conversionsApi, stableEmptyConversions } from '../../redux/api/conversionsApi'; +import { stableEmptyUnitDataById, unitsAdapter, unitsApi } from '../../redux/api/unitsApi'; import { ConversionData } from '../../types/redux/conversions'; import TooltipMarkerComponent from '../TooltipMarkerComponent'; import ConversionViewComponent from './ConversionViewComponent'; import CreateConversionModalComponent from './CreateConversionModalComponent'; -import { UnitDataById } from 'types/redux/units'; -const stableEmptyConversions: ConversionData[] = []; -const stableEmptyUnitDataById: UnitDataById = {}; /** * Defines the conversions page card view * @returns Conversion page element diff --git a/src/client/app/redux/api/conversionsApi.ts b/src/client/app/redux/api/conversionsApi.ts index eb595e6c8..d52c42dd9 100644 --- a/src/client/app/redux/api/conversionsApi.ts +++ b/src/client/app/redux/api/conversionsApi.ts @@ -105,3 +105,5 @@ export const selectCik = createSelector( return cik; } ); + +export const stableEmptyConversions: ConversionData[] = []; diff --git a/src/client/app/redux/api/readingsApi.ts b/src/client/app/redux/api/readingsApi.ts index d7a349a68..3a2021dc4 100644 --- a/src/client/app/redux/api/readingsApi.ts +++ b/src/client/app/redux/api/readingsApi.ts @@ -149,3 +149,12 @@ export const readingsApi = baseApi.injectEndpoints({ }) }); + +// Stable reference for when there is not data. Avoids rerenders. +export const stableEmptyLineReadings: LineReadings = {}; +export const stableEmptyBarReadings: BarReadings = {}; +export const stableEmptyThreeDReadings: ThreeDReading = { + xData: [], + yData: [], + zData: [] +}; diff --git a/src/client/app/redux/api/unitsApi.ts b/src/client/app/redux/api/unitsApi.ts index a148cd1f5..f16bff0f8 100644 --- a/src/client/app/redux/api/unitsApi.ts +++ b/src/client/app/redux/api/unitsApi.ts @@ -4,7 +4,7 @@ import { EntityState, createEntityAdapter } from '@reduxjs/toolkit'; import { RootState } from 'store'; -import { UnitData } from '../../types/redux/units'; +import { UnitData, UnitDataById } from '../../types/redux/units'; import { baseApi } from './baseApi'; import { conversionsApi } from './conversionsApi'; export const unitsAdapter = createEntityAdapter({ @@ -70,3 +70,4 @@ export const { selectEntities: selectUnitDataById } = unitsAdapter.getSelectors((state: RootState) => selectUnitDataResult(state).data ?? unitsInitialState); +export const stableEmptyUnitDataById: UnitDataById = {}; diff --git a/src/client/app/redux/api/userApi.ts b/src/client/app/redux/api/userApi.ts index b23690e75..d32b11d85 100644 --- a/src/client/app/redux/api/userApi.ts +++ b/src/client/app/redux/api/userApi.ts @@ -41,4 +41,7 @@ export const userApi = baseApi.injectEndpoints({ invalidatesTags: ['Users'] }) }) -}); \ No newline at end of file +}); + +// Provide a stable empty reference for when data is in flight +export const stableEmptyUsers: User[] = []; \ No newline at end of file From a5cb49033243fbb280e377d7c90370f9998e771c Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Sun, 3 Mar 2024 11:30:49 -0800 Subject: [PATCH 119/131] 3d Revisions - rework/tweak Readings Per Day Component modern RTK. --- .../ReadingsPerDaySelectComponent.tsx | 79 +++++++------------ .../app/components/UIOptionsComponent.tsx | 4 +- .../app/redux/selectors/threeDSelectors.ts | 52 +++++++++++- 3 files changed, 77 insertions(+), 58 deletions(-) diff --git a/src/client/app/components/ReadingsPerDaySelectComponent.tsx b/src/client/app/components/ReadingsPerDaySelectComponent.tsx index 3fb74c305..a75d6a513 100644 --- a/src/client/app/components/ReadingsPerDaySelectComponent.tsx +++ b/src/client/app/components/ReadingsPerDaySelectComponent.tsx @@ -2,14 +2,14 @@ * 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 Select from 'react-select'; -import { selectGraphState, selectThreeDReadingInterval, updateThreeDReadingInterval } from '../redux/slices/graphSlice'; -import { readingsApi } from '../redux/api/readingsApi'; +import { readingsApi, stableEmptyThreeDReadings } from '../redux/api/readingsApi'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { selectThreeDQueryArgs } from '../redux/selectors/chartQuerySelectors'; -import { ChartTypes, ReadingInterval } from '../types/redux/graph'; +import { selectReadingsPerDaySelectData } from '../redux/selectors/threeDSelectors'; +import { selectThreeDReadingInterval, updateThreeDReadingInterval } from '../redux/slices/graphSlice'; +import { ReadingInterval } from '../types/redux/graph'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; @@ -19,58 +19,32 @@ import TooltipMarkerComponent from './TooltipMarkerComponent'; */ export default function ReadingsPerDaySelect() { const dispatch = useAppDispatch(); - const graphState = useAppSelector(selectGraphState); const readingInterval = useAppSelector(selectThreeDReadingInterval); const { args, shouldSkipQuery } = useAppSelector(selectThreeDQueryArgs); - const { data, isFetching } = readingsApi.endpoints.threeD.useQuery(args, { skip: shouldSkipQuery }); - - let actualReadingInterval = ReadingInterval.Hourly; - if (data && data.zData.length) { - // Special Case: When no compatible data available, data returned is from api is -999 - if (data.zData[0][0] && data.zData[0][0] < 0) { - actualReadingInterval = ReadingInterval.Incompatible; - } else { - const startTS = moment.utc(data.xData[0].startTimestamp); - const endTS = moment.utc(data.xData[0].endTimestamp); - // This should be the number of hours between readings. - actualReadingInterval = endTS.diff(startTS) / 3600000; - } - - } - - // Use the selectedOption enum value to update threeD State - const onSelectChange = (selectedOption: ReadingsPerDayOption) => dispatch(updateThreeDReadingInterval(selectedOption.value)); - - // Default Display Value && Disabled Status - let displayValue = `${24 / readingInterval}`; - let isDisabled = false; - - // Modify Display Value if needed. - if (actualReadingInterval === ReadingInterval.Incompatible) { - isDisabled = true; - } else if (actualReadingInterval !== readingInterval) { - displayValue += ` -> ${24 / actualReadingInterval}`; - } - - const value = { - label: displayValue, - value: readingInterval - }; + const { currentValue, isDisabled, isFetching } = readingsApi.endpoints.threeD.useQuery(args, { + skip: shouldSkipQuery, + selectFromResult: ({ currentData, ...result }) => ({ + ...result, + ...selectReadingsPerDaySelectData(currentData ?? stableEmptyThreeDReadings, readingInterval) + }) + }); - if (graphState.chartToRender === ChartTypes.threeD) { - return ( -
-

- {`${translate('readings.per.day')}:`} - -

- dispatch(updateThreeDReadingInterval(e!.value))} + /> +
+ ); } interface ReadingsPerDayOption { @@ -92,3 +66,4 @@ const options = Object.values(ReadingInterval) value: intervalLength } as ReadingsPerDayOption; }); + diff --git a/src/client/app/components/UIOptionsComponent.tsx b/src/client/app/components/UIOptionsComponent.tsx index fa9736ce9..13c6bb572 100644 --- a/src/client/app/components/UIOptionsComponent.tsx +++ b/src/client/app/components/UIOptionsComponent.tsx @@ -18,7 +18,7 @@ import DateRangeComponent from './DateRangeComponent'; import ErrorBarComponent from './ErrorBarComponent'; import GraphicRateMenuComponent from './GraphicRateMenuComponent'; import MapControlsComponent from './MapControlsComponent'; -import ThreeDSelectComponent from './ReadingsPerDaySelectComponent'; +import ReadingsPerDaySelectComponent from './ReadingsPerDaySelectComponent'; /** * @returns the UI Control panel @@ -65,7 +65,7 @@ export default function UIOptionsComponent() { - + {chartToRender === ChartTypes.threeD && } { /* Controls error bar, specifically for the line chart. */ diff --git a/src/client/app/redux/selectors/threeDSelectors.ts b/src/client/app/redux/selectors/threeDSelectors.ts index 0f4cc025a..370097ca8 100644 --- a/src/client/app/redux/selectors/threeDSelectors.ts +++ b/src/client/app/redux/selectors/threeDSelectors.ts @@ -3,13 +3,15 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { createSelector } from '@reduxjs/toolkit'; +import { utc } from 'moment'; +import { ThreeDReading } from 'types/readings'; +import { selectGroupDataById } from '../../redux/api/groupsApi'; +import { selectMeterDataById } from '../../redux/api/metersApi'; +import { MeterOrGroup, ReadingInterval } from '../../types/redux/graph'; +import { AreaUnitType } from '../../utils/getAreaUnitConversion'; import { selectThreeDMeterOrGroup, selectThreeDMeterOrGroupID } from '../slices/graphSlice'; -import { selectGroupDataById } from '../../redux/api/groupsApi'; -import { MeterOrGroup } from '../../types/redux/graph'; -import { AreaUnitType } from '../../utils/getAreaUnitConversion'; -import { selectMeterDataById } from '../../redux/api/metersApi'; import { selectNameFromEntity } from './entitySelectors'; @@ -43,3 +45,45 @@ export const selectThreeDComponentInfo = createSelector( }; } ); + + +export const selectReadingsPerDaySelectData = createSelector( + [ + (readings: ThreeDReading) => readings, + (_readings: ThreeDReading, readingInterval: ReadingInterval) => readingInterval + ], + (data, readingInterval) => { + // initially honor the selected reading interval. + let actualReadingInterval = readingInterval; + if (data && data.zData.length) { + // Special Case: When no compatible data available, data returned is from api is -999 + if (data.zData[0][0] && data.zData[0][0] < 0) { + actualReadingInterval = ReadingInterval.Incompatible; + } else { + const startTS = utc(data.xData[0].startTimestamp); + const endTS = utc(data.xData[0].endTimestamp); + // This should be the number of hours between readings. + actualReadingInterval = endTS.diff(startTS) / 3600000; + } + } + // Default Display Value && Disabled Status + let displayValue = `${24 / readingInterval}`; + let isDisabled = false; + + // Modify Display Value if needed. + if (actualReadingInterval === ReadingInterval.Incompatible) { + // Disable select when api returns -999 incompatible + isDisabled = true; + } else if (actualReadingInterval !== readingInterval) { + // notify user with converted 'syntax' + displayValue += ` -> ${24 / actualReadingInterval}`; + } + + // Current Value to be displayed on ReadingsPerDay component. + const currentValue = { + label: displayValue, + value: readingInterval + }; + return { actualReadingInterval, isDisabled, displayValue, currentValue }; + } +); From e97bdcfe874397179f0956a63c1853939a49f55e Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Sun, 3 Mar 2024 11:53:13 -0800 Subject: [PATCH 120/131] More 3d Revisions - default to 6 months back when selecting entity to graph on 3d. --- .../app/components/ThreeDPillComponent.tsx | 8 +++---- src/client/app/redux/slices/graphSlice.ts | 23 +++++++++++++------ 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/client/app/components/ThreeDPillComponent.tsx b/src/client/app/components/ThreeDPillComponent.tsx index 86a6d6cc6..a5c7b8ae2 100644 --- a/src/client/app/components/ThreeDPillComponent.tsx +++ b/src/client/app/components/ThreeDPillComponent.tsx @@ -41,12 +41,12 @@ export default function ThreeDPillComponent() { }); // When a Pill Badge is clicked update threeD state to indicate new meter or group to render. - const handlePillClick = (pillData: MeterOrGroupPill) => dispatch(updateThreeDMeterOrGroupInfo( - { + const handlePillClick = (pillData: MeterOrGroupPill) => dispatch( + updateThreeDMeterOrGroupInfo({ meterOrGroupID: pillData.meterOrGroupID, meterOrGroup: pillData.meterOrGroup - } - )); + }) + ); // Method Generates Reactstrap Pill Badges for selected meters or groups const populatePills = (meterOrGroupPillData: MeterOrGroupPill[]) => { diff --git a/src/client/app/redux/slices/graphSlice.ts b/src/client/app/redux/slices/graphSlice.ts index eab9e7e12..c808b152c 100644 --- a/src/client/app/redux/slices/graphSlice.ts +++ b/src/client/app/redux/slices/graphSlice.ts @@ -134,15 +134,24 @@ export const graphSlice = createSlice({ updateThreeDReadingInterval: (state, action: PayloadAction) => { state.current.threeD.readingInterval = action.payload; }, - updateThreeDMeterOrGroupInfo: (state, action: PayloadAction<{ meterOrGroupID: number | undefined, meterOrGroup: MeterOrGroup }>) => { - state.current.threeD.meterOrGroupID = action.payload.meterOrGroupID; - state.current.threeD.meterOrGroup = action.payload.meterOrGroup; - }, - updateThreeDMeterOrGroupID: (state, action: PayloadAction) => { - state.current.threeD.meterOrGroupID = action.payload; + updateThreeDMeterOrGroupID: (state, action: PayloadAction) => { + if (state.current.threeD.meterOrGroupID !== action.payload) { + state.current.threeD.meterOrGroupID = action.payload; + } }, updateThreeDMeterOrGroup: (state, action: PayloadAction) => { - state.current.threeD.meterOrGroup = action.payload; + if (state.current.threeD.meterOrGroup !== action.payload) { + state.current.threeD.meterOrGroup = action.payload; + } + }, + updateThreeDMeterOrGroupInfo: (state, action: PayloadAction<{ meterOrGroupID: number | undefined, meterOrGroup: MeterOrGroup }>) => { + const { updateThreeDMeterOrGroupID, updateThreeDMeterOrGroup } = graphSlice.caseReducers; + updateThreeDMeterOrGroupID(state, graphSlice.actions.updateThreeDMeterOrGroupID(action.payload.meterOrGroupID)); + updateThreeDMeterOrGroup(state, graphSlice.actions.updateThreeDMeterOrGroup(action.payload.meterOrGroup)); + if (!state.current.queryTimeInterval.getIsBounded()) { + // Set the query time interval to 6 moths back when not bounded for 3D + state.current.queryTimeInterval = new TimeInterval(moment.utc().subtract(6, 'months'), moment.utc()); + } }, updateSelectedMetersOrGroups: ({ current }, action: PayloadAction<{ newMetersOrGroups: number[], meta: ActionMeta }>) => { // This reducer handles the addition and subtraction values for both the meter and group select components. From 98705868d942e966580a2752c62089bc1fda37d4 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Sun, 3 Mar 2024 13:27:53 -0800 Subject: [PATCH 121/131] 3d Bugs --- .../MeterAndGroupSelectComponent.tsx | 9 ++++---- src/client/app/redux/slices/graphSlice.ts | 21 +++++++++++++------ 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/client/app/components/MeterAndGroupSelectComponent.tsx b/src/client/app/components/MeterAndGroupSelectComponent.tsx index f069d7144..22029be6e 100644 --- a/src/client/app/components/MeterAndGroupSelectComponent.tsx +++ b/src/client/app/components/MeterAndGroupSelectComponent.tsx @@ -93,21 +93,20 @@ const MultiValueLabel = (props: MultiValueGenericProps e.stopPropagation()} - onClick={e => { - ReactTooltip.rebuild(); + onMouseDown={e => { e.stopPropagation(); + ReactTooltip.rebuild(); ref.current && ReactTooltip.show(ref.current); }} + onClick={e => e.stopPropagation()} style={{ overflow: 'hidden' }} onMouseEnter={e => { if (!isDisabled) { const multiValueLabel = e.currentTarget.children[0]; if (multiValueLabel.scrollWidth > e.currentTarget.clientWidth) { - ReactTooltip.rebuild(); ref.current && ReactTooltip.show(ref.current); } } diff --git a/src/client/app/redux/slices/graphSlice.ts b/src/client/app/redux/slices/graphSlice.ts index c808b152c..15ba78e30 100644 --- a/src/client/app/redux/slices/graphSlice.ts +++ b/src/client/app/redux/slices/graphSlice.ts @@ -153,7 +153,8 @@ export const graphSlice = createSlice({ state.current.queryTimeInterval = new TimeInterval(moment.utc().subtract(6, 'months'), moment.utc()); } }, - updateSelectedMetersOrGroups: ({ current }, action: PayloadAction<{ newMetersOrGroups: number[], meta: ActionMeta }>) => { + updateSelectedMetersOrGroups: (state, action: PayloadAction<{ newMetersOrGroups: number[], meta: ActionMeta }>) => { + const { current } = state; // This reducer handles the addition and subtraction values for both the meter and group select components. // The 'MeterOrGroup' type is heavily utilized in the reducer and other parts of the code. // Note that this option is binary, if it's not a meter, then it's a group. @@ -162,8 +163,10 @@ export const graphSlice = createSlice({ const { newMetersOrGroups, meta } = action.payload; const cleared = meta.action === 'clear'; const valueRemoved = (meta.action === 'pop-value' || meta.action === 'remove-value') && meta.removedValue !== undefined; - const valueAdded = meta.action === 'select-option' && meta.option; + const valueAdded = meta.action === 'select-option' && meta.option !== undefined; let isAMeter = true; + console.log('UpSelMetOrGroupies.'); + console.log(valueRemoved, meta, valueRemoved && meta.option); if (cleared) { const clearedMeterOrGroups = meta.removedValues; @@ -209,12 +212,18 @@ export const graphSlice = createSlice({ current.threeD.meterOrGroup = undefined; } - } else if (valueAdded && meta.option && current.chartToRender === ChartTypes.threeD) { + } else if (valueAdded && current.chartToRender === ChartTypes.threeD) { // When a meter or group is selected/added, make it the currently active in 3D current. // TODO Currently only tracks when on 3d, Verify that this is the desired behavior - current.threeD.meterOrGroupID = meta.option.value; - current.threeD.meterOrGroup = meta.option.meterOrGroup; - } else if (valueRemoved && meta.option) { + // re-use existing reducers, action creators + graphSlice.caseReducers.updateThreeDMeterOrGroupInfo(state, + graphSlice.actions.updateThreeDMeterOrGroupInfo({ + meterOrGroupID: meta.option!.value, + meterOrGroup: meta.option!.meterOrGroup! + }) + ); + } else if (valueRemoved) { + console.log('Should be removing the current threedstate'); const idMatches = meta.removedValue.value === current.threeD.meterOrGroupID; const typeMatches = meta.removedValue.meterOrGroup === current.threeD.meterOrGroup; if (idMatches && typeMatches) { From d38ea6279c78003ac54ca5d245b614ed8ec74696 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Sun, 3 Mar 2024 14:05:59 -0800 Subject: [PATCH 122/131] update dispatch 3d from selects. -- remove logs. --- src/client/app/components/BarChartComponent.tsx | 1 - src/client/app/components/LineChartComponent.tsx | 1 - .../components/MeterAndGroupSelectComponent.tsx | 15 +++++++++++++-- src/client/app/components/PlotOED.tsx | 1 - src/client/app/redux/selectors/uiSelectors.ts | 9 +++++---- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/client/app/components/BarChartComponent.tsx b/src/client/app/components/BarChartComponent.tsx index 5509bb097..313b82c52 100644 --- a/src/client/app/components/BarChartComponent.tsx +++ b/src/client/app/components/BarChartComponent.tsx @@ -108,7 +108,6 @@ export default function BarChartComponent() { }} onRelayout={debounce( (e: PlotRelayoutEvent) => { - // console.log(e) // This event emits an object that contains values indicating changes in the user's graph, such as zooming. if (e['xaxis.range[0]'] && e['xaxis.range[1]']) { // The event signals changes in the user's interaction with the graph. diff --git a/src/client/app/components/LineChartComponent.tsx b/src/client/app/components/LineChartComponent.tsx index 01ef4be4b..f71058629 100644 --- a/src/client/app/components/LineChartComponent.tsx +++ b/src/client/app/components/LineChartComponent.tsx @@ -94,7 +94,6 @@ export default function LineChartComponent() { }} onRelayout={debounce( (e: PlotRelayoutEvent) => { - // console.log(e) // This event emits an object that contains values indicating changes in the user's graph, such as zooming. if (e['xaxis.range[0]'] && e['xaxis.range[1]']) { // The event signals changes in the user's interaction with the graph. diff --git a/src/client/app/components/MeterAndGroupSelectComponent.tsx b/src/client/app/components/MeterAndGroupSelectComponent.tsx index 22029be6e..26a40dd51 100644 --- a/src/client/app/components/MeterAndGroupSelectComponent.tsx +++ b/src/client/app/components/MeterAndGroupSelectComponent.tsx @@ -13,9 +13,9 @@ import ReactTooltip from 'react-tooltip'; import { Badge } from 'reactstrap'; import { useAppDispatch, useAppSelector } from '../redux/reduxHooks'; import { selectMeterGroupSelectData } from '../redux/selectors/uiSelectors'; -import { updateSelectedMetersOrGroups } from '../redux/slices/graphSlice'; +import { selectChartToRender, updateSelectedMetersOrGroups, updateThreeDMeterOrGroupInfo } from '../redux/slices/graphSlice'; import { GroupedOption, SelectOption } from '../types/items'; -import { MeterOrGroup } from '../types/redux/graph'; +import { ChartTypes, MeterOrGroup } from '../types/redux/graph'; import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; import { selectAnythingFetching } from '../redux/selectors/apiSelectors'; @@ -90,6 +90,8 @@ const MultiValueLabel = (props: MultiValueGenericProps selectChartToRender(state) === ChartTypes.threeD); // TODO Verify behavior, and set proper message/ translate return ( < div ref={ref} @@ -100,12 +102,21 @@ const MultiValueLabel = (props: MultiValueGenericProps e.stopPropagation()} style={{ overflow: 'hidden' }} onMouseEnter={e => { if (!isDisabled) { const multiValueLabel = e.currentTarget.children[0]; + // display a reacto tooltip for options that have overflowing/cutoff labels. if (multiValueLabel.scrollWidth > e.currentTarget.clientWidth) { ref.current && ReactTooltip.show(ref.current); } diff --git a/src/client/app/components/PlotOED.tsx b/src/client/app/components/PlotOED.tsx index c938439e2..f2f245300 100644 --- a/src/client/app/components/PlotOED.tsx +++ b/src/client/app/components/PlotOED.tsx @@ -37,7 +37,6 @@ export const PlotOED = (props: OEDPlotProps) => { // Debounce to limit dispatch and keep reasonable history const debouncedRelayout = _.debounce( (e: PlotRelayoutEvent) => { - // console.log(e) // This event emits an object that contains values indicating changes in the user's graph, such as zooming. if (e['xaxis.range[0]'] && e['xaxis.range[1]']) { // The event signals changes in the user's interaction with the graph. diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts index e485a27aa..e26104e06 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -362,15 +362,16 @@ export const selectUnitSelectData = createAppSelector( * Visibility is determined by which set the items are contained in. * @param compatibleItems - compatible items to make select options for * @param incompatibleItems - incompatible items to make select options for - * @param dataById - current redux state, must be one of UnitsState, MetersState, or GroupsState + * @param entityDataById - current redux state, must be one of UnitsState, MetersState, or GroupsState * @returns Two Lists: Compatible, and Incompatible selectOptions for use as grouped React-Select options */ export function getSelectOptionsByEntity( compatibleItems: Set, - incompatibleItems: Set, dataById: MeterDataByID | GroupDataByID | UnitDataById + incompatibleItems: Set, + entityDataById: MeterDataByID | GroupDataByID | UnitDataById ) { //The final list of select options to be displayed - const compatibleItemOptions = Object.entries(dataById) + const compatibleItemOptions = Object.entries(entityDataById) .filter(([id]) => compatibleItems.has(Number(id))) .map(([id, entity]) => { // Groups unit and meters have identifier, groups doesn't @@ -389,7 +390,7 @@ export function getSelectOptionsByEntity( }); //Loop over each itemId and create an activated select option - const incompatibleItemOptions = Object.entries(dataById) + const incompatibleItemOptions = Object.entries(entityDataById) .filter(([id]) => incompatibleItems.has(Number(id))) .map(([id, entity]) => { const label = selectNameFromEntity(entity); From 4db53cf6557b9e93c95054ecd5ed39934eae8ed3 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Sun, 3 Mar 2024 21:05:22 -0800 Subject: [PATCH 123/131] logs --- src/client/app/redux/slices/graphSlice.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/client/app/redux/slices/graphSlice.ts b/src/client/app/redux/slices/graphSlice.ts index 15ba78e30..b94fe365a 100644 --- a/src/client/app/redux/slices/graphSlice.ts +++ b/src/client/app/redux/slices/graphSlice.ts @@ -165,8 +165,6 @@ export const graphSlice = createSlice({ const valueRemoved = (meta.action === 'pop-value' || meta.action === 'remove-value') && meta.removedValue !== undefined; const valueAdded = meta.action === 'select-option' && meta.option !== undefined; let isAMeter = true; - console.log('UpSelMetOrGroupies.'); - console.log(valueRemoved, meta, valueRemoved && meta.option); if (cleared) { const clearedMeterOrGroups = meta.removedValues; @@ -223,7 +221,6 @@ export const graphSlice = createSlice({ }) ); } else if (valueRemoved) { - console.log('Should be removing the current threedstate'); const idMatches = meta.removedValue.value === current.threeD.meterOrGroupID; const typeMatches = meta.removedValue.meterOrGroup === current.threeD.meterOrGroup; if (idMatches && typeMatches) { From 4a35baca2da4306a82210579b8a23280acde5832 Mon Sep 17 00:00:00 2001 From: Steven Huss-Lederman Date: Mon, 4 Mar 2024 09:40:21 -0600 Subject: [PATCH 124/131] minor format/edit --- src/client/app/components/MeterAndGroupSelectComponent.tsx | 2 +- src/client/app/components/PlotNavComponent.tsx | 3 +-- src/client/app/redux/slices/currentUserSlice.ts | 1 - src/client/app/translations/data.ts | 4 ++-- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/client/app/components/MeterAndGroupSelectComponent.tsx b/src/client/app/components/MeterAndGroupSelectComponent.tsx index 26a40dd51..52d1e9f7d 100644 --- a/src/client/app/components/MeterAndGroupSelectComponent.tsx +++ b/src/client/app/components/MeterAndGroupSelectComponent.tsx @@ -116,7 +116,7 @@ const MultiValueLabel = (props: MultiValueGenericProps { if (!isDisabled) { const multiValueLabel = e.currentTarget.children[0]; - // display a reacto tooltip for options that have overflowing/cutoff labels. + // display a react tooltip for options that have overflowing/cutoff labels. if (multiValueLabel.scrollWidth > e.currentTarget.clientWidth) { ref.current && ReactTooltip.show(ref.current); } diff --git a/src/client/app/components/PlotNavComponent.tsx b/src/client/app/components/PlotNavComponent.tsx index 6d0cca0f7..69c76321b 100644 --- a/src/client/app/components/PlotNavComponent.tsx +++ b/src/client/app/components/PlotNavComponent.tsx @@ -23,7 +23,6 @@ export default function PlotNavComponent() {
-
); } @@ -73,4 +72,4 @@ export const RefreshGraphComponent = () => { onClick={() => { !somethingFetching && dispatch(updateTimeInterval(sliderInterval)); }} /> ); -}; \ No newline at end of file +}; diff --git a/src/client/app/redux/slices/currentUserSlice.ts b/src/client/app/redux/slices/currentUserSlice.ts index b617aa83b..e6ec54092 100644 --- a/src/client/app/redux/slices/currentUserSlice.ts +++ b/src/client/app/redux/slices/currentUserSlice.ts @@ -74,4 +74,3 @@ export const { setUserToken, clearCurrentUser } = currentUserSlice.actions; - diff --git a/src/client/app/translations/data.ts b/src/client/app/translations/data.ts index 0e602e77d..6ecee08ae 100644 --- a/src/client/app/translations/data.ts +++ b/src/client/app/translations/data.ts @@ -1001,8 +1001,8 @@ const LocaleTranslationData = { "child.groups": "Los grupos secundarios", "child.meters": "Medidores infantiles", "clear.graph.history": "Borrar historia", - "clipboard.copied": "Copied To Clipboard", - "clipboard.not.copied": "Failed to Copy To Clipboard", + "clipboard.copied": "(Need Spanish) Copied To Clipboard", + "clipboard.not.copied": "(Need Spanish) Failed to Copy To Clipboard", "close": "Cerrar", "compare": "Comparar", "compare.raw": "No se puede crear gráfico de comparación con unidades crudas como temperatura", From 870b634e3199c9f1f991a971a9c01c843e7d9eb9 Mon Sep 17 00:00:00 2001 From: Steven Huss-Lederman Date: Mon, 4 Mar 2024 15:43:55 -0600 Subject: [PATCH 125/131] remove actions now in RTK radar and 3D files removed since radar always used line and 3D seems similar. Thus, that state definations are also gone. --- src/client/app/types/redux/actions.ts | 124 ------------------ src/client/app/types/redux/barReadings.ts | 45 +------ src/client/app/types/redux/compareReadings.ts | 44 ------- src/client/app/types/redux/lineReadings.ts | 41 +----- src/client/app/types/redux/radarReadings.ts | 70 ---------- src/client/app/types/redux/threeDReadings.ts | 79 ----------- 6 files changed, 2 insertions(+), 401 deletions(-) delete mode 100644 src/client/app/types/redux/radarReadings.ts delete mode 100644 src/client/app/types/redux/threeDReadings.ts diff --git a/src/client/app/types/redux/actions.ts b/src/client/app/types/redux/actions.ts index 60c80731c..5ab356b7d 100644 --- a/src/client/app/types/redux/actions.ts +++ b/src/client/app/types/redux/actions.ts @@ -8,101 +8,10 @@ import { State } from './state'; export enum ActionType { - RequestCurrentUser = 'REQUEST_CURRENT_USER', - ReceiveCurrentUser = 'RECEIVE_CURRENT_USER', - ClearCurrentUser = 'CLEAR_CURRENT_USER', - - RequestVersion = 'REQUEST_VERSION', - ReceiveVersion = 'RECEIVE_VERSION', - UpdateUnsavedChanges = 'UPDATE_UNSAVED_CHANGES', RemoveUnsavedChanges = 'REMOVE_UNSAVED_CHANGES', FlipLogOutState = 'FLIP_LOG_OUT_STATE', - RequestGroupBarReadings = 'REQUEST_GROUP_BAR_READINGS', - ReceiveGroupBarReadings = 'RECEIVE_GROUP_BAR_READINGS', - RequestMeterBarReadings = 'REQUEST_METER_BAR_READINGS', - ReceiveMeterBarReadings = 'RECEIVE_METER_BAR_READINGS', - - RequestGroupLineReadings = 'REQUEST_GROUP_LINE_READINGS', - ReceiveGroupLineReadings = 'RECEIVE_GROUP_LINE_READINGS', - RequestMeterLineReadings = 'REQUEST_METER_LINE_READINGS', - ReceiveMeterLineReadings = 'RECEIVE_METER_LINE_READINGS', - - RequestGroupCompareReadings = 'REQUEST_GROUP_COMPARE_READINGS', - ReceiveGroupCompareReadings = 'RECEIVE_GROUP_COMPARE_READINGS', - RequestMeterCompareReadings = 'REQUEST_METER_COMPARE_READINGS', - ReceiveMeterCompareReadings = 'RECEIVE_METER_COMPARE_READINGS', - - RequestGroupRadarReadings = 'REQUEST_GROUP_RADAR_READINGS', - ReceiveGroupRadarReadings = 'RECEIVE_GROUP_RADAR_READINGS', - RequestMeterRadarReadings = 'REQUEST_METER_RADAR_READINGS', - ReceiveMeterRadarReadings = 'RECEIVE_METER_RADAR_READINGS', - - RequestMeterThreeDReadings = 'REQUEST_METER_THREED_READINGS', - ReceiveMeterThreeDReadings = 'RECEIVE_METER_THREED_READINGS', - RequestGroupThreeDReadings = 'REQUEST_GROUP`_THREED_READINGS', - ReceiveGroupThreeDReadings = 'RECEIVE_GROUP_THREED_READINGS', - UpdateThreeDReadingInterval = 'UPDATE_THREED_READINGS_INTERVAL', - UpdateThreeDMeterOrGroupInfo = 'UPDATE_TREED_METER_OR_GROUP_INFO', - - UpdateSelectedMeters = 'UPDATE_SELECTED_METERS', - UpdateSelectedGroups = 'UPDATE_SELECTED_GROUPS', - UpdateSelectedUnit = 'UPDATE_SELECTED_UNIT', - UpdateSelectedAreaUnit = 'UPDATE_SELECTED_AREA_UNIT', - UpdateSelectedConversion = 'UPDATE_SELECTED_CONVERSION', - UpdateBarDuration = 'UPDATE_BAR_DURATION', - ChangeChartToRender = 'CHANGE_CHART_TO_RENDER', - ChangeBarStacking = 'CHANGE_BAR_STACKING', - ToggleAreaNormalization = 'TOGGLE_AREA_NORMALIZATION', - ToggleShowMinMax = 'TOGGLE_SHOW_MIN_MAX', - ChangeGraphZoom = 'CHANGE_GRAPH_ZOOM', - ChangeSliderRange = 'CHANGE_SLIDER_RANGE', - ResetRangeSliderStack = 'RESET_RANGE_SLIDER_STACK', - ToggleOptionsVisibility = 'TOGGLE_OPTIONS_VISIBILITY', - UpdateComparePeriod = 'UPDATE_COMPARE_PERIOD', - ChangeCompareSortingOrder = 'CHANGE_COMPARE_SORTING_ORDER', - SetHotlinked = 'SET_HOTLINKED', - UpdateLineGraphRate = 'UPDATE_LINE_GRAPH_RATE', - ConfirmGraphRenderOnce = 'CONFIRM_GRAPH_RENDER_ONCE', - - RequestGroupsDetails = 'REQUEST_GROUPS_DETAILS', - ReceiveGroupsDetails = 'RECEIVE_GROUPS_DETAILS', - RequestGroupChildren = 'REQUEST_GROUP_CHILDREN', - ReceiveGroupChildren = 'RECEIVE_GROUP_CHILDREN', - RequestAllGroupsChildren = 'REQUEST_ALL_GROUPS_CHILDREN', - ReceiveAllGroupsChildren = 'RECEIVE_ALL_GROUPS_CHILDREN', - ChangeDisplayedGroups = 'CHANGE_DISPLAYED_GROUPS', - ConfirmEditedGroup = 'CONFIRM_EDITED_GROUP', - ConfirmGroupsFetchedOnce = 'CONFIRM_GROUPS_FETCHED_ONCE', - ConfirmAllGroupsChildrenFetchedOnce = 'CONFIRM_ALL_GROUPS_CHILDREN_FETCHED_ONCE', - - UpdateDisplayTitle = 'UPDATE_DISPLAY_TITLE', - UpdateDefaultChartToRender = 'UPDATE_DEFAULT_CHART_TO_RENDER', - ToggleDefaultBarStacking = 'TOGGLE_DEFAULT_BAR_STACKING', - ToggleDefaultAreaNormalization = 'TOGGLE_DEFAULT_AREA_NORMALIZATION', - UpdateDefaultAreaUnit = 'UPDATE_DEFAULT_AREA_UNIT', - UpdateDefaultTimezone = 'UPDATE_DEFAULT_TIMEZONE', - UpdateDefaultLanguage = 'UPDATE_DEFAULT_LANGUAGE', - RequestPreferences = 'REQUEST_PREFERENCES', - ReceivePreferences = 'RECEIVE_PREFERENCES', - MarkPreferencesNotSubmitted = 'MARK_PREFERENCES_NOT_SUBMITTED', - MarkPreferencesSubmitted = 'MARK_PREFERENCES_SUBMITTED', - UpdateDefaultWarningFileSize = 'UPDATE_DEFAULT_WARNING_FILE_SIZE', - UpdateDefaultFileSizeLimit = 'UPDATE_DEFAULT_FILE_SIZE_LIMIT', - ToggleWaitForCikAndDB = 'TOGGLE_WAIT_FOR_CIK_AND_DB', - UpdateDefaultMeterReadingFrequency = 'UPDATE_DEFAULT_METER_READING_FREQUENCY', - UpdateDefaultMeterMinimumValue = 'UPDATE_DEFAULT_METER_MINIMUM_VALUE', - UpdateDefaultMeterMaximumValue = 'UPDATE_DEFAULT_METER_MAXIMUM_VALUE', - UpdateDefaultMeterMinimumDate = 'UPDATE_DEFAULT_METER_MINIMUM_DATE', - UpdateDefaultMeterMaximumDate = 'UPDATE_DEFAULT_METER_MAXIMUM_DATE', - UpdateDefaultMeterReadingGap = 'UPDATE_DEFAULT_METER_READING_GAP', - UpdateDefaultMeterMaximumErrors = 'UPDATE_DEFAULT_METER_MAXIMUM_ERRORS', - UpdateDefaultMeterDisableChecks = 'UPDATE_DEFAULT_METER_DISABLE_CHECKS', - UpdateDefaultHelpUrl = 'UPDATE_DEFAULT_HELP_URL', - - UpdateSelectedLanguage = 'UPDATE_SELECTED_LANGUAGE', - UpdateCalibrationMode = 'UPDATE_MAP_MODE', UpdateSelectedMap = 'UPDATE_SELECTED_MAPS', UpdateMapSource = 'UPDATE_MAP_IMAGE', @@ -120,39 +29,6 @@ export enum ActionType { SetCalibration = 'SET_CALIBRATION', ResetCalibration = 'RESET_CALIBRATION', IncrementCounter = 'INCREMENT_COUNTER', - - ReceiveUnitsDetails = 'RECEIVE_UNITS_DETAILS', - RequestUnitsDetails = 'REQUEST_UNITS_DETAILS', - ChangeDisplayedUnits = 'CHANGE_DISPLAYED_UNITS', - SubmitEditedUnit = 'SUBMIT_EDITED_UNIT', - ConfirmEditedUnit = 'CONFIRM_EDITED_UNIT', - DeleteSubmittedUnit = 'DELETE_SUBMITTED_UNIT', - ConfirmUnitsFetchedOnce = 'CONFIRM_UNITS_FETCHED_ONCE', - - ReceiveMetersDetails = 'RECEIVE_METERS_DETAILS', - RequestMetersDetails = 'REQUEST_METERS_DETAILS', - ChangeDisplayedMeters = 'CHANGE_DISPLAYED_METERS', - EditMeterDetails = 'EDIT_METER_DETAILS', - SubmitEditedMeter = 'SUBMIT_EDITED_METER', - ConfirmEditedMeter = 'CONFIRM_EDITED_METER', - ConfirmAddMeter = 'CONFIRM_ADD_METER', - DeleteSubmittedMeter = 'DELETE_SUBMITTED_METER', - ConfirmMetersFetchedOnce = 'CONFIRM_METERS_FETCHED_ONCE', - - ReceiveConversionsDetails = 'RECEIVE_CONVERSIONS_DETAILS', - RequestConversionsDetails = 'REQUEST_CONVERSIONS_DETAILS', - ChangeDisplayedConversions = 'CHANGE_DISPLAYED_CONVERSIONS', - EditConversionDetails = 'EDIT_CONVERSION_DETAILS', - SubmitEditedConversion = 'SUBMIT_EDITED_CONVERSION', - ConfirmEditedConversion = 'CONFIRM_EDITED_CONVERSION', - DeleteEditedConversion = 'DELETE_EDITED_CONVERSION', - DeleteSubmittedConversion = 'DELETE_SUBMITTED_CONVERSION', - DeleteConversion = 'DELETE_CONVERSION', - ConfirmConversionsFetchedOnce = 'CONFIRM_CONVERSIONS_FETCHED_ONCE', - - RequestCiksDetails = 'REQUEST_CIKS_DETAILS', - ReceiveCiksDetails = 'RECEIVE_CIKS_DETAILS', - ConfirmCiksFetchedOne = 'CONFIRM_CIKS_FETCHED_ONE', } /** diff --git a/src/client/app/types/redux/barReadings.ts b/src/client/app/types/redux/barReadings.ts index 737fbab8d..08c8beff1 100644 --- a/src/client/app/types/redux/barReadings.ts +++ b/src/client/app/types/redux/barReadings.ts @@ -2,50 +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 * as moment from 'moment'; -import { TimeInterval } from '../../../../common/TimeInterval'; -import { ActionType } from './actions'; -import { BarReading, BarReadings } from '../readings'; - -export interface RequestMeterBarReadingsAction { - type: ActionType.RequestMeterBarReadings; - meterIDs: number[]; - unitID: number; - timeInterval: TimeInterval; - barDuration: moment.Duration; -} - -export interface RequestGroupBarReadingsAction { - type: ActionType.RequestGroupBarReadings; - groupIDs: number[]; - unitID: number; - timeInterval: TimeInterval; - barDuration: moment.Duration; -} - -export interface ReceiveMeterBarReadingsAction { - type: ActionType.ReceiveMeterBarReadings; - meterIDs: number[]; - unitID: number; - timeInterval: TimeInterval; - barDuration: moment.Duration; - readings: BarReadings; -} - -export interface ReceiveGroupBarReadingsAction { - type: ActionType.ReceiveGroupBarReadings; - groupIDs: number[]; - unitID: number; - timeInterval: TimeInterval; - barDuration: moment.Duration; - readings: BarReadings; -} - -export type BarReadingsAction = - ReceiveMeterBarReadingsAction | - ReceiveGroupBarReadingsAction | - RequestMeterBarReadingsAction | - RequestGroupBarReadingsAction; +import { BarReading } from '../readings'; export interface BarReadingsState { byMeterID: { diff --git a/src/client/app/types/redux/compareReadings.ts b/src/client/app/types/redux/compareReadings.ts index 5c1c63b6f..ab9f67305 100644 --- a/src/client/app/types/redux/compareReadings.ts +++ b/src/client/app/types/redux/compareReadings.ts @@ -2,50 +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 { ActionType } from './actions'; -import { CompareReadings } from '../readings'; - -export interface RequestMeterCompareReadingsAction { - type: ActionType.RequestMeterCompareReadings; - meterIDs: number[]; - unitID: number; - timeInterval: TimeInterval; - compareShift: moment.Duration; -} - -export interface RequestGroupCompareReadingsAction { - type: ActionType.RequestGroupCompareReadings; - groupIDs: number[]; - unitID: number; - timeInterval: TimeInterval; - compareShift: moment.Duration; -} - -export interface ReceiveMeterCompareReadingsAction { - type: ActionType.ReceiveMeterCompareReadings; - meterIDs: number[]; - unitID: number; - timeInterval: TimeInterval; - compareShift: moment.Duration; - readings: CompareReadings; -} - -export interface ReceiveGroupCompareReadingsAction { - type: ActionType.ReceiveGroupCompareReadings; - groupIDs: number[]; - unitID: number; - timeInterval: TimeInterval; - compareShift: moment.Duration; - readings: CompareReadings; -} - -export type CompareReadingsAction = - ReceiveMeterCompareReadingsAction | - ReceiveGroupCompareReadingsAction | - RequestMeterCompareReadingsAction | - RequestGroupCompareReadingsAction; export interface CompareReadingsData { isFetching: boolean; diff --git a/src/client/app/types/redux/lineReadings.ts b/src/client/app/types/redux/lineReadings.ts index 3d94a102b..ff71a6b12 100644 --- a/src/client/app/types/redux/lineReadings.ts +++ b/src/client/app/types/redux/lineReadings.ts @@ -2,46 +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 { TimeInterval } from '../../../../common/TimeInterval'; -import { ActionType } from './actions'; -import { LineReading, LineReadings } from '../readings'; - -export interface RequestMeterLineReadingsAction { - type: ActionType.RequestMeterLineReadings; - meterIDs: number[]; - unitID: number; - timeInterval: TimeInterval; -} - -export interface RequestGroupLineReadingsAction { - type: ActionType.RequestGroupLineReadings; - groupIDs: number[]; - unitID: number; - timeInterval: TimeInterval; -} - -export interface ReceiveMeterLineReadingsAction { - type: ActionType.ReceiveMeterLineReadings; - meterIDs: number[]; - unitID: number; - timeInterval: TimeInterval; - readings: LineReadings; - -} - -export interface ReceiveGroupLineReadingsAction { - type: ActionType.ReceiveGroupLineReadings; - groupIDs: number[]; - unitID: number; - timeInterval: TimeInterval; - readings: LineReadings; -} - -export type LineReadingsAction = - ReceiveMeterLineReadingsAction | - ReceiveGroupLineReadingsAction | - RequestMeterLineReadingsAction | - RequestGroupLineReadingsAction; +import { LineReading } from '../readings'; export interface LineReadingsState { byMeterID: { diff --git a/src/client/app/types/redux/radarReadings.ts b/src/client/app/types/redux/radarReadings.ts deleted file mode 100644 index c6c0083e9..000000000 --- a/src/client/app/types/redux/radarReadings.ts +++ /dev/null @@ -1,70 +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 { TimeInterval } from '../../../../common/TimeInterval'; -import { ActionType } from './actions'; -import { LineReading, LineReadings } from '../readings'; - -export interface RequestMeterRadarReadingAction { - type: ActionType.RequestMeterLineReadings; - meterIDs: number[]; - unitID: number; - timeInterval: TimeInterval; -} - -export interface RequestGroupRadarReadingAction { - type: ActionType.RequestGroupLineReadings; - groupIDs: number[]; - unitID: number; - timeInterval: TimeInterval; -} - -export interface ReceiveMeterRadarReadingAction { - type: ActionType.ReceiveMeterLineReadings; - meterIDs: number[]; - unitID: number; - timeInterval: TimeInterval; - readings: LineReadings; -} - -export interface ReceiveGroupRadarReadingAction { - type: ActionType.ReceiveGroupLineReadings; - groupIDs: number[]; - unitID: number; - timeInterval: TimeInterval; - readings: LineReadings; -} - -export type RadarReadingsAction = - RequestMeterRadarReadingAction | - RequestGroupRadarReadingAction | - ReceiveMeterRadarReadingAction | - ReceiveGroupRadarReadingAction; - - -export interface RadarReadingsState { - byMeterID: { - [meterID: number]: { - [timeInterval: string]: { - [unitID: number]: { - isFetching: boolean; - readings?: LineReading[]; - } - } - } - }; - byGroupID: { - [groupID: number]: { - [timeInterval: string]: { - [unitID: number]: { - isFetching: boolean; - readings?: LineReading[]; - } - } - } - }; - isFetching: boolean; - metersFetching: boolean; - groupsFetching: boolean; -} diff --git a/src/client/app/types/redux/threeDReadings.ts b/src/client/app/types/redux/threeDReadings.ts deleted file mode 100644 index db8fd5249..000000000 --- a/src/client/app/types/redux/threeDReadings.ts +++ /dev/null @@ -1,79 +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 { TimeInterval } from '../../../../common/TimeInterval'; -import { ActionType } from './actions'; -import { ThreeDReading } from '../readings'; -import { ReadingInterval } from './graph'; - -export interface RequestMeterThreeDReadingsAction { - type: ActionType.RequestMeterThreeDReadings; - meterID: number; - unitID: number; - timeInterval: TimeInterval; - readingInterval: ReadingInterval; -} - -export interface ReceiveMeterThreeDReadingsAction { - type: ActionType.ReceiveMeterThreeDReadings; - meterID: number; - unitID: number; - timeInterval: TimeInterval; - readingInterval: ReadingInterval; - readings: ThreeDReading; -} -export interface RequestGroupThreeDReadingsAction { - type: ActionType.RequestGroupThreeDReadings; - groupID: number; - unitID: number; - timeInterval: TimeInterval; - readingInterval: ReadingInterval; -} - -export interface ReceiveGroupThreeDReadingsAction { - type: ActionType.ReceiveGroupThreeDReadings; - groupID: number; - unitID: number; - timeInterval: TimeInterval; - readingInterval: ReadingInterval; - readings: ThreeDReading; -} - - -export type ThreeDReadingsAction = - ReceiveMeterThreeDReadingsAction | - RequestMeterThreeDReadingsAction | - RequestGroupThreeDReadingsAction | - ReceiveGroupThreeDReadingsAction; - -export interface ThreeDReadingsState { - byMeterID: { - [meterID: number]: { - [timeInterval: string]: { - [unitID: number]: { - [readingInterval: string]: { - isFetching: boolean; - readings?: ThreeDReading; - } - } - } - } - }; - - byGroupID: { - [groupID: number]: { - [timeInterval: string]: { - [unitID: number]: { - [readingInterval: string]: { - isFetching: boolean; - readings?: ThreeDReading; - } - } - } - } - }; - - isFetching: boolean; - metersFetching: boolean; -} From 9754faf78d69453e720e4efdca6b42533d05de1b Mon Sep 17 00:00:00 2001 From: Steven Huss-Lederman Date: Tue, 5 Mar 2024 08:45:22 -0600 Subject: [PATCH 126/131] defaultGraphicUnit -> unitId bug fix The selectUnitNmae used the wrong unit id. --- src/client/app/redux/selectors/adminSelectors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/app/redux/selectors/adminSelectors.ts b/src/client/app/redux/selectors/adminSelectors.ts index 509fc16d9..cf1028682 100644 --- a/src/client/app/redux/selectors/adminSelectors.ts +++ b/src/client/app/redux/selectors/adminSelectors.ts @@ -64,7 +64,7 @@ export const selectUnitName = createAppSelector( selectMeterById, (unitDataById, meterData) => { const unitName = (Object.keys(unitDataById).length === 0 || !meterData || meterData.unitId === -99) ? - noUnitTranslated().identifier : unitDataById[meterData.defaultGraphicUnit]?.identifier; + noUnitTranslated().identifier : unitDataById[meterData.unitId]?.identifier; return unitName; } ); From ca21cd31cb7d8ceded5f4c1a4117d0f88c4b808d Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Tue, 5 Mar 2024 07:40:56 -0800 Subject: [PATCH 127/131] createMeterModalTweak --- .../app/components/meters/CreateMeterModalComponent.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/client/app/components/meters/CreateMeterModalComponent.tsx b/src/client/app/components/meters/CreateMeterModalComponent.tsx index 4c8eec2f3..0b99ccb58 100644 --- a/src/client/app/components/meters/CreateMeterModalComponent.tsx +++ b/src/client/app/components/meters/CreateMeterModalComponent.tsx @@ -53,6 +53,15 @@ export default function CreateMeterModalComponent() { selectCreateMeterUnitCompatibility(state, meterDetails as unknown as MeterData) ); const { meterIsValid, defaultGraphicUnitIsValid } = useAppSelector(state => isValidCreateMeter(state, meterDetails as unknown as MeterData)); + + // Reset default graphingUnit when selected is invalid with updated unitId. + React.useEffect(() => { + if (!defaultGraphicUnitIsValid) { + setMeterDetails(details => ({ ...details, defaultGraphicUnit: -999 })); + } + }, [meterDetails.unitId]); + + const handleShow = () => setShowModal(true); const handleStringChange = (e: React.ChangeEvent) => { From e3775f614c3474e708e74ca1b184e4dc04ee3c17 Mon Sep 17 00:00:00 2001 From: Steven Huss-Lederman Date: Tue, 5 Mar 2024 10:18:24 -0600 Subject: [PATCH 128/131] fix bug where meter/group dropdown wrong for maps --- src/client/app/redux/selectors/uiSelectors.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts index e26104e06..499cc070a 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -205,6 +205,9 @@ export const selectChartTypeCompatibility = createAppSelector( } }); } + // Filter out any new incompatible meters/groups from the compatibility list. + incompatibleMeters.forEach(meterID => compatibleMeters.delete(meterID)); + incompatibleGroups.forEach(groupID => compatibleGroups.delete(groupID)); return { compatibleMeters, From 584cff9dbbb8fe135f3e39afac440e6e955ad7d3 Mon Sep 17 00:00:00 2001 From: Steven Huss-Lederman Date: Tue, 5 Mar 2024 10:30:42 -0600 Subject: [PATCH 129/131] update group edit state so correct after create --- src/client/app/components/groups/EditGroupModalComponent.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/client/app/components/groups/EditGroupModalComponent.tsx b/src/client/app/components/groups/EditGroupModalComponent.tsx index 9fd887c51..658f1da33 100644 --- a/src/client/app/components/groups/EditGroupModalComponent.tsx +++ b/src/client/app/components/groups/EditGroupModalComponent.tsx @@ -69,6 +69,11 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr const [editGroupsState, setEditGroupsState] = useState(_.cloneDeep(groupDataById)); const possibleGraphicUnits = useAppSelector(selectPossibleGraphicUnits); + // Update group state in case changed from create/edit + useEffect(() => { + setEditGroupsState(_.cloneDeep(groupDataById)); + }, [groupDataById]); + // The current groups state of group being edited of the local copy. It should always be valid. const groupState = editGroupsState[props.groupId]; From 5b0199ff17335ddcb250780964796e0a82b8ee3b Mon Sep 17 00:00:00 2001 From: Steven Huss-Lederman Date: Tue, 5 Mar 2024 12:26:29 -0600 Subject: [PATCH 130/131] invalidated readings on appropriate conversion change --- src/client/app/redux/api/baseApi.ts | 3 ++- src/client/app/redux/api/conversionsApi.ts | 2 +- src/client/app/redux/api/readingsApi.ts | 15 ++++++++++----- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/client/app/redux/api/baseApi.ts b/src/client/app/redux/api/baseApi.ts index 863b80ad4..58d58b5e1 100644 --- a/src/client/app/redux/api/baseApi.ts +++ b/src/client/app/redux/api/baseApi.ts @@ -34,7 +34,8 @@ export const baseApi = createApi({ 'Users', 'ConversionDetails', 'Units', - 'Cik' + 'Cik', + 'Readings' ], // Initially no defined endpoints, Use rtk query's injectEndpoints endpoints: () => ({}) diff --git a/src/client/app/redux/api/conversionsApi.ts b/src/client/app/redux/api/conversionsApi.ts index d52c42dd9..1c0c2ce7b 100644 --- a/src/client/app/redux/api/conversionsApi.ts +++ b/src/client/app/redux/api/conversionsApi.ts @@ -84,7 +84,7 @@ export const conversionsApi = baseApi.injectEndpoints({ method: 'POST', body: { redoCik, refreshReadingViews } }), - invalidatesTags: ['ConversionDetails', 'Cik'] + invalidatesTags: ['ConversionDetails', 'Cik', 'Readings'] }) }) }); diff --git a/src/client/app/redux/api/readingsApi.ts b/src/client/app/redux/api/readingsApi.ts index 3a2021dc4..7e3f7a4b9 100644 --- a/src/client/app/redux/api/readingsApi.ts +++ b/src/client/app/redux/api/readingsApi.ts @@ -24,7 +24,8 @@ export const readingsApi = baseApi.injectEndpoints({ // destructure args that are passed into the callback, and generate/ implicitly return the API url for the request. url: `api/unitReadings/threeD/${meterOrGroup}/${id}`, params: { timeInterval, graphicUnitId, readingInterval } - }) + }), + providesTags: ['Readings'] }), line: builder.query({ // Customize Cache Behavior by utilizing (serializeQueryArgs, merge, forceRefetch) @@ -88,7 +89,8 @@ export const readingsApi = baseApi.injectEndpoints({ // https://redux-toolkit.js.org/rtk-query/usage/customizing-queries#implementing-a-queryfn // queryFn requires either a data, or error object to be returned return error ? { error } : { data: data as LineReadings }; - } + }, + providesTags: ['Readings'] }), bar: builder.query({ // Refer to line endpoint for detailed explanation as the logic is identical @@ -107,7 +109,8 @@ export const readingsApi = baseApi.injectEndpoints({ const idsToFetch = _.difference(ids, cachedIDs).join(','); const { data, error } = await baseQuery({ url: `api/unitReadings/bar/${meterOrGroup}/${idsToFetch}`, params }); return error ? { error } : { data: data as BarReadings }; - } + }, + providesTags: ['Readings'] }), compare: builder.query({ serializeQueryArgs: ({ queryArgs }) => _.omit(queryArgs, 'ids'), @@ -125,7 +128,8 @@ export const readingsApi = baseApi.injectEndpoints({ const idsToFetch = _.difference(ids, cachedIDs).join(','); const { data, error } = await baseQuery({ url: `/api/compareReadings/${meterOrGroup}/${idsToFetch}`, params }); return error ? { error } : { data: data as CompareReadings }; - } + }, + providesTags: ['Readings'] }), radar: builder.query({ // Refer to line endpoint for detailed explanation as the logic is identical @@ -144,7 +148,8 @@ export const readingsApi = baseApi.injectEndpoints({ const idsToFetch = _.difference(ids, cachedIDs).join(','); const { data, error } = await baseQuery({ url: `api/unitReadings/radar/${meterOrGroup}/${idsToFetch}`, params }); return error ? { error } : { data: data as LineReadings }; - } + }, + providesTags: ['Readings'] }) }) From dda8170f2be0dc878a55507a4fd646823c9282a6 Mon Sep 17 00:00:00 2001 From: Steven Huss-Lederman Date: Tue, 5 Mar 2024 12:26:58 -0600 Subject: [PATCH 131/131] comment out/TODO for 3D when > 1 yr --- src/client/app/redux/slices/graphSlice.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/client/app/redux/slices/graphSlice.ts b/src/client/app/redux/slices/graphSlice.ts index b94fe365a..67ca4bda5 100644 --- a/src/client/app/redux/slices/graphSlice.ts +++ b/src/client/app/redux/slices/graphSlice.ts @@ -148,10 +148,11 @@ export const graphSlice = createSlice({ const { updateThreeDMeterOrGroupID, updateThreeDMeterOrGroup } = graphSlice.caseReducers; updateThreeDMeterOrGroupID(state, graphSlice.actions.updateThreeDMeterOrGroupID(action.payload.meterOrGroupID)); updateThreeDMeterOrGroup(state, graphSlice.actions.updateThreeDMeterOrGroup(action.payload.meterOrGroup)); - if (!state.current.queryTimeInterval.getIsBounded()) { - // Set the query time interval to 6 moths back when not bounded for 3D - state.current.queryTimeInterval = new TimeInterval(moment.utc().subtract(6, 'months'), moment.utc()); - } + // TODO Under development so in future + // if (!state.current.queryTimeInterval.getIsBounded()) { + // // Set the query time interval to 6 moths back when not bounded for 3D + // state.current.queryTimeInterval = new TimeInterval(moment.utc().subtract(6, 'months'), moment.utc()); + // } }, updateSelectedMetersOrGroups: (state, action: PayloadAction<{ newMetersOrGroups: number[], meta: ActionMeta }>) => { const { current } = state;