From 6059a824b322e500fc07684ae9c192648cfcf6c0 Mon Sep 17 00:00:00 2001 From: ChrisMart21 Date: Tue, 13 Feb 2024 00:43:42 +0000 Subject: [PATCH] ChartLink bug fix + single dispatch - 3d Chart Alignment Fix --- .../app/components/DateRangeComponent.tsx | 3 +- .../MeterAndGroupSelectComponent.tsx | 3 + src/client/app/components/ThreeDComponent.tsx | 12 +- .../app/components/UIOptionsComponent.tsx | 2 +- .../conversion/ConversionViewComponent.tsx | 7 +- .../conversion/ConversionsDetailComponent.tsx | 7 +- .../EditConversionModalComponent.tsx | 4 +- .../groups/EditGroupModalComponent.tsx | 2 - .../components/router/GraphLinkComponent.tsx | 118 +++--------------- src/client/app/redux/api/groupsApi.ts | 3 +- .../app/redux/selectors/lineChartSelectors.ts | 3 +- .../redux/selectors/plotlyDataSelectors.ts | 20 ++- .../app/redux/selectors/threeDSelectors.ts | 16 +-- src/client/app/redux/selectors/uiSelectors.ts | 25 ++-- src/client/app/redux/slices/appStateSlice.ts | 23 ++-- src/client/app/redux/slices/graphSlice.ts | 89 +++++++++++-- src/client/app/styles/index.css | 9 ++ src/client/app/types/redux/graph.ts | 1 + 18 files changed, 182 insertions(+), 165 deletions(-) diff --git a/src/client/app/components/DateRangeComponent.tsx b/src/client/app/components/DateRangeComponent.tsx index 76a3de2c8..dd7c79039 100644 --- a/src/client/app/components/DateRangeComponent.tsx +++ b/src/client/app/components/DateRangeComponent.tsx @@ -16,8 +16,7 @@ import translate from '../utils/translate'; import TooltipMarkerComponent from './TooltipMarkerComponent'; import { selectSelectedLanguage } from '../redux/slices/appStateSlice'; -// Potential Fixes, for now omitted -// import '../styles/DateRangeCustom.css' +import '../styles/DateRangeCustom.css' /** * A component which allows users to select date ranges in lieu of a slider (line graphic) diff --git a/src/client/app/components/MeterAndGroupSelectComponent.tsx b/src/client/app/components/MeterAndGroupSelectComponent.tsx index 6fc323f22..c6b02d9cd 100644 --- a/src/client/app/components/MeterAndGroupSelectComponent.tsx +++ b/src/client/app/components/MeterAndGroupSelectComponent.tsx @@ -60,6 +60,9 @@ export default function MeterAndGroupSelectComponent(props: MeterAndGroupSelectP // Included React-Select Animations components={animatedComponents} styles={customStyles} + classNames={{ + valueContainer: () => 'no_scrollbar' + }} isLoading={somethingIsFetching} /> diff --git a/src/client/app/components/ThreeDComponent.tsx b/src/client/app/components/ThreeDComponent.tsx index e7c6e7d0f..2df742937 100644 --- a/src/client/app/components/ThreeDComponent.tsx +++ b/src/client/app/components/ThreeDComponent.tsx @@ -67,7 +67,7 @@ export default function ThreeDComponent() { } return ( -
+ <> {isFetching ? @@ -75,11 +75,11 @@ export default function ThreeDComponent() { data={dataToRender as Plotly.Data[]} layout={layout as Plotly.Layout} config={config} - style={{ width: '100%', height: '100%' }} + style={{ width: '100%', flexGrow: '1' }} useResizeHandler={true} /> } -
+ ) } @@ -221,7 +221,9 @@ function setHelpLayout(helpText: string = 'Help Text Goes Here', fontSize: numbe function setThreeDLayout(zLabelText: string = 'Resource Usage') { // responsible for setting Labels return { - //Leaves holes in graph for missing, undefined, NaN, or null values + // Eliminate margin + margin: { t: 0, b: 0, l: 0, r: 0, pad: 0 }, + // Leaves gaps / voids in graph for missing, undefined, NaN, or null values connectgaps: false, scene: { xaxis: { @@ -248,7 +250,7 @@ function setThreeDLayout(zLabelText: string = 'Resource Usage') { } } } - } + } as Partial } const config = { diff --git a/src/client/app/components/UIOptionsComponent.tsx b/src/client/app/components/UIOptionsComponent.tsx index 017ecbf36..e275e5676 100644 --- a/src/client/app/components/UIOptionsComponent.tsx +++ b/src/client/app/components/UIOptionsComponent.tsx @@ -56,7 +56,7 @@ export default function UIOptionsComponent() { ReactTooltip.rebuild(); return ( -
+
diff --git a/src/client/app/components/conversion/ConversionViewComponent.tsx b/src/client/app/components/conversion/ConversionViewComponent.tsx index 418931908..a762ac50a 100644 --- a/src/client/app/components/conversion/ConversionViewComponent.tsx +++ b/src/client/app/components/conversion/ConversionViewComponent.tsx @@ -37,7 +37,6 @@ 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; @@ -46,7 +45,7 @@ export default function ConversionViewComponent(props: ConversionViewComponentPr } else { arrowShown = ' → '; } - const header = String(unitDataById[props.conversion.sourceId].identifier + arrowShown + unitDataById[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. @@ -56,10 +55,10 @@ export default function ConversionViewComponent(props: ConversionViewComponentPr {header}
- {unitDataById[props.conversion.sourceId].identifier} + {unitDataById[props.conversion.sourceId]?.identifier}
- {unitDataById[props.conversion.destinationId].identifier} + {unitDataById[props.conversion.destinationId]?.identifier}
{translate(`TrueFalseType.${props.conversion.bidirectional.toString()}`)} diff --git a/src/client/app/components/conversion/ConversionsDetailComponent.tsx b/src/client/app/components/conversion/ConversionsDetailComponent.tsx index 8fb6a4274..d96d5ee51 100644 --- a/src/client/app/components/conversion/ConversionsDetailComponent.tsx +++ b/src/client/app/components/conversion/ConversionsDetailComponent.tsx @@ -12,7 +12,10 @@ 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 @@ -21,9 +24,9 @@ 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(); + const { data: conversionsState = stableEmptyConversions, isFetching: conversionsFetching } = conversionsApi.useGetConversionsDetailsQuery(); // Units DataById - const { unitDataById = {}, isFetching: unitsFetching } = unitsApi.useGetUnitsDetailsQuery(undefined, { + const { unitDataById = stableEmptyUnitDataById, isFetching: unitsFetching } = unitsApi.useGetUnitsDetailsQuery(undefined, { selectFromResult: ({ data, ...result }) => ({ ...result, unitDataById: data && unitsAdapter.getSelectors().selectEntities(data) diff --git a/src/client/app/components/conversion/EditConversionModalComponent.tsx b/src/client/app/components/conversion/EditConversionModalComponent.tsx index 654871d6f..0a2d97902 100644 --- a/src/client/app/components/conversion/EditConversionModalComponent.tsx +++ b/src/client/app/components/conversion/EditConversionModalComponent.tsx @@ -162,7 +162,7 @@ export default function EditConversionModalComponent(props: EditConversionModalC id='sourceId' name='sourceId' type='text' - defaultValue={unitDataById[state.sourceId].identifier} + defaultValue={unitDataById[state.sourceId]?.identifier} // Disable input to prevent changing ID value disabled> @@ -176,7 +176,7 @@ export default function EditConversionModalComponent(props: EditConversionModalC id='destinationId' name='destinationId' type='text' - defaultValue={unitDataById[state.destinationId].identifier} + defaultValue={unitDataById[state.destinationId]?.identifier} // Disable input to prevent changing ID value disabled> diff --git a/src/client/app/components/groups/EditGroupModalComponent.tsx b/src/client/app/components/groups/EditGroupModalComponent.tsx index 2c97f5b10..f2bad368e 100644 --- a/src/client/app/components/groups/EditGroupModalComponent.tsx +++ b/src/client/app/components/groups/EditGroupModalComponent.tsx @@ -329,8 +329,6 @@ export default function EditGroupModalComponent(props: EditGroupModalComponentPr // 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 { showErrorNotification(translate('group.input.error')); } diff --git a/src/client/app/components/router/GraphLinkComponent.tsx b/src/client/app/components/router/GraphLinkComponent.tsx index 8edefbb1b..dc0c32a60 100644 --- a/src/client/app/components/router/GraphLinkComponent.tsx +++ b/src/client/app/components/router/GraphLinkComponent.tsx @@ -2,122 +2,32 @@ * License, v. 2.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 '../router/InitializingComponent'; -import moment from 'moment'; import * as React from 'react'; import { Navigate, useSearchParams } from 'react-router-dom'; -import { graphSlice } from '../../redux/slices/graphSlice'; import { useWaitForInit } from '../../redux/componentHooks'; 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 '../../redux/actions/map'; -import { appStateSlice } from '../../redux/slices/appStateSlice'; +import { processGraphLink } from '../../redux/slices/graphSlice'; +import InitializingComponent from '../router/InitializingComponent'; export const GraphLink = () => { const dispatch = useAppDispatch(); const [URLSearchParams] = useSearchParams(); const { initComplete } = useWaitForInit(); - const dispatchQueue: PayloadAction[] = []; + React.useEffect(() => { + const linkIsValid = validateHotlink(URLSearchParams) + if (linkIsValid) { + dispatch(processGraphLink(URLSearchParams)) + } + }, []) 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 'areaNormalization': - dispatchQueue.push(graphSlice.actions.setAreaNormalization(value === 'true')) - break; - case 'areaUnit': - dispatchQueue.push(graphSlice.actions.updateSelectedAreaUnit(value as AreaUnitType)) - 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 'chartType': - dispatchQueue.push(graphSlice.actions.changeChartToRender(value as ChartTypes)) - 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 '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 'meterIDs': - dispatchQueue.push(graphSlice.actions.updateSelectedMeters(value.split(',').map(s => parseInt(s)))) - break; - case 'meterOrGroup': - dispatchQueue.push(graphSlice.actions.updateThreeDMeterOrGroup(value as MeterOrGroup)); - break; - case 'meterOrGroupID': - dispatchQueue.push(graphSlice.actions.updateThreeDMeterOrGroupID(parseInt(value))); - break; - 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 '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 'unitID': - dispatchQueue.push(graphSlice.actions.updateSelectedUnit(parseInt(value))) - 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 and clear the link in the search bar return } + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const validateHotlink = (params: URLSearchParams) => { + // TODO VALIDATE HOTLINK + return true +} \ 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 12573be07..e021694f7 100644 --- a/src/client/app/redux/api/groupsApi.ts +++ b/src/client/app/redux/api/groupsApi.ts @@ -69,7 +69,8 @@ export const groupsApi = baseApi.injectEndpoints({ url: 'api/groups/edit', method: 'PUT', body: group - }) + }), + invalidatesTags: ['GroupData', 'GroupChildrenData'] }), deleteGroup: builder.mutation({ query: groupId => ({ diff --git a/src/client/app/redux/selectors/lineChartSelectors.ts b/src/client/app/redux/selectors/lineChartSelectors.ts index 9d6540cbc..fec0f3dfd 100644 --- a/src/client/app/redux/selectors/lineChartSelectors.ts +++ b/src/client/app/redux/selectors/lineChartSelectors.ts @@ -30,7 +30,8 @@ export const selectPlotlyMeterData = selectFromLineReadingsResult( [data => data, (_data, dependencies: PlotlyLineDeps) => dependencies], (data, { areaUnit, areaNormalization, meterDataById, compatibleEntities, showMinMax, lineUnitLabel, rateScaling }) => { const plotlyLineData = Object.entries(data) - // filter entries for requested groups + // filter entries for requested compatible groups + // compatible entities is using the same data deriving selectors as the select option for group, & meter. .filter(([groupID]) => compatibleEntities.includes((Number(groupID)))) .map(([id, readings]) => { const meterInfo = meterDataById[Number(id)] diff --git a/src/client/app/redux/selectors/plotlyDataSelectors.ts b/src/client/app/redux/selectors/plotlyDataSelectors.ts index c1d6f33ab..f32452b7e 100644 --- a/src/client/app/redux/selectors/plotlyDataSelectors.ts +++ b/src/client/app/redux/selectors/plotlyDataSelectors.ts @@ -85,9 +85,25 @@ export const selectBarUnitLabel = (state: RootState) => { } // fallback to name if no identifier -export const selectNameFromEntity = (entity: MeterData | GroupData | UnitData) => 'identifier' in entity ? entity.identifier : entity.name +export const selectNameFromEntity = (entity: MeterData | GroupData | UnitData) => { + if (entity && 'identifier' in entity && entity.identifier) { + return entity.identifier + } else if (entity && 'name' in entity && entity.name) { + return entity.name + } else { + // Users May Possibly receive data for meters with neither identifier, or name so empty. + return '' + } +} + // Determines if meter or group base on objet distinct properties of each -export const selectMeterOrGroupFromEntity = (entity: MeterData | GroupData) => 'meterType' in entity ? MeterOrGroup.meters : MeterOrGroup.groups +export const selectMeterOrGroupFromEntity = (entity: MeterData | GroupData) => { + return 'meterType' in entity ? MeterOrGroup.meters : 'childMeters' in entity ? MeterOrGroup.groups : undefined +} + +export const selectDefaultGraphicUnitFromEntity = (entity: MeterData | GroupData) => { + return 'defaultGraphicUnit' in entity ? entity.defaultGraphicUnit : undefined +} // Line and Groups use these values to derive plotly data, so make selector for them to 'extend' export const selectCommonPlotlyDependencies = createStructuredSelector( diff --git a/src/client/app/redux/selectors/threeDSelectors.ts b/src/client/app/redux/selectors/threeDSelectors.ts index 55df030c4..bf190bf79 100644 --- a/src/client/app/redux/selectors/threeDSelectors.ts +++ b/src/client/app/redux/selectors/threeDSelectors.ts @@ -10,6 +10,7 @@ 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 './plotlyDataSelectors'; // Memoized Selectors @@ -24,15 +25,14 @@ export const selectThreeDComponentInfo = createSelector( let isAreaCompatible = true; if (id && meterDataById[id]) { + const entity = meterOrGroup === MeterOrGroup.meters ? meterDataById[id] : groupDataById[id] + meterOrGroupName = selectNameFromEntity(entity) // Get Meter or Group's info - 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 && groupDataById) { - const groupInfo = groupDataById[id]; - meterOrGroupName = groupInfo.name; - isAreaCompatible = groupInfo.area !== 0 && groupInfo.areaUnit !== AreaUnitType.none; + if (meterOrGroup === MeterOrGroup.meters && entity) { + isAreaCompatible = entity.area !== 0 && entity.areaUnit !== AreaUnitType.none; + } else if (meterOrGroup === MeterOrGroup.groups && entity) { + meterOrGroupName = entity.name; + isAreaCompatible = entity.area !== 0 && entity.areaUnit !== AreaUnitType.none; } } diff --git a/src/client/app/redux/selectors/uiSelectors.ts b/src/client/app/redux/selectors/uiSelectors.ts index 1f46e4409..da8b82ef3 100644 --- a/src/client/app/redux/selectors/uiSelectors.ts +++ b/src/client/app/redux/selectors/uiSelectors.ts @@ -12,7 +12,7 @@ import { selectUnitDataById } from '../../redux/api/unitsApi'; import { RootState } from '../../store'; import { DataType } from '../../types/Datasources'; import { GroupedOption, SelectOption } from '../../types/items'; -import { ChartTypes, MeterOrGroup } from '../../types/redux/graph'; +import { ChartTypes } from '../../types/redux/graph'; import { UnitDataById, UnitRepresentType } from '../../types/redux/units'; import { CartesianPoint, Dimensions, calculateScaleFromEndpoints, gpsToUserGrid, @@ -27,12 +27,9 @@ import { selectSelectedUnit } from '../slices/graphSlice'; import { selectVisibleMetersAndGroups, selectVisibleUnitOrSuffixState } from './authVisibilitySelectors'; -import { selectNameFromEntity } from './plotlyDataSelectors'; +import { selectDefaultGraphicUnitFromEntity, selectMeterOrGroupFromEntity, selectNameFromEntity } from './plotlyDataSelectors'; import { createAppSelector } from './selectors'; - - - export const selectCurrentUnitCompatibility = createAppSelector( [ selectVisibleMetersAndGroups, @@ -148,8 +145,6 @@ export const selectChartTypeCompatibility = createAppSelector( 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]; @@ -350,6 +345,7 @@ export const selectUnitSelectData = createAppSelector( } }); } + // Ready to display unit. Put selectable ones before non-selectable ones. const unitOptions = getSelectOptionsByEntity(compatibleUnits, incompatibleUnits, unitDataById); const unitsGroupedOptions: GroupedOption[] = [ @@ -386,8 +382,8 @@ export function getSelectOptionsByEntity( // Groups unit and meters have identifier, groups doesn't const label = selectNameFromEntity(entity) // MeterAnd Group, undefined for units - const defaultGraphicUnit = 'defaultGraphicUnit' in entity ? entity.defaultGraphicUnit : undefined - const meterOrGroup = 'meterType' in entity ? MeterOrGroup.meters : 'childMeters' in entity ? MeterOrGroup.groups : undefined + const defaultGraphicUnit = selectDefaultGraphicUnitFromEntity(entity) + const meterOrGroup = selectMeterOrGroupFromEntity(entity) return { value: Number(id), label: label, @@ -404,8 +400,8 @@ export function getSelectOptionsByEntity( .map(([id, entity]) => { const label = selectNameFromEntity(entity) // MeterAnd Group, undefined for units - const defaultGraphicUnit = 'defaultGraphicUnit' in entity ? entity.defaultGraphicUnit : undefined - const meterOrGroup = 'meterType' in entity ? MeterOrGroup.meters : 'childMeters' in entity ? MeterOrGroup.groups : undefined + const defaultGraphicUnit = selectDefaultGraphicUnitFromEntity(entity) + const meterOrGroup = selectMeterOrGroupFromEntity(entity) return { value: Number(id), label: label, @@ -416,11 +412,8 @@ export function getSelectOptionsByEntity( } as SelectOption }) - const compatible = _.sortBy(compatibleItemOptions, item => item.label.toLowerCase(), 'asc') const incompatible = _.sortBy(incompatibleItemOptions, item => item.label.toLowerCase(), 'asc') - - return { compatible, incompatible } } @@ -428,7 +421,9 @@ export function getSelectOptionsByEntity( // Helper function for area compatibility // areaNorm should be active when called -const isAreaNormCompatible = (id: number, selectedUnit: number, meterOrGroupData: MeterDataByID | GroupDataByID, unitDataById: UnitDataById) => { +export 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 diff --git a/src/client/app/redux/slices/appStateSlice.ts b/src/client/app/redux/slices/appStateSlice.ts index 8120875a2..3f1fceca5 100644 --- a/src/client/app/redux/slices/appStateSlice.ts +++ b/src/client/app/redux/slices/appStateSlice.ts @@ -2,6 +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 moment from 'moment'; +import { LanguageTypes } from '../../types/redux/i18n'; +import { deleteToken, getToken, hasToken } from '../../utils/token'; import { fetchMapsDetails } from '../actions/map'; import { authApi } from '../api/authApi'; import { conversionsApi } from '../api/conversionsApi'; @@ -12,10 +15,8 @@ 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'; +import { processGraphLink } from './graphSlice'; export interface AppState { initComplete: boolean; @@ -54,8 +55,8 @@ export const appStateSlice = createThunkSlice({ async (_: void, { dispatch }) => { // These queries will trigger a api request, and add a subscription to the store. // Typically they return an unsubscribe method, however we always want to be subscribed to any cache changes for these endpoints. - dispatch(versionApi.endpoints.getVersion.initiate()) dispatch(preferencesApi.endpoints.getPreferences.initiate()) + dispatch(versionApi.endpoints.getVersion.initiate()) dispatch(unitsApi.endpoints.getUnitsDetails.initiate()) dispatch(conversionsApi.endpoints.getConversionsDetails.initiate()) dispatch(conversionsApi.endpoints.getConversionArray.initiate()) @@ -100,10 +101,16 @@ export const appStateSlice = createThunkSlice({ ) }), extraReducers: builder => { - builder.addMatcher(preferencesApi.endpoints.getPreferences.matchFulfilled, (state, action) => { - state.selectedLanguage = action.payload.defaultLanguage - moment.locale(action.payload.defaultLanguage); - }) + builder + .addCase(processGraphLink, (state, { payload }) => { + if (payload.has('optionsVisibility')) { + state.optionsVisibility = payload.get('optionsVisibility') === 'true' + } + }) + .addMatcher(preferencesApi.endpoints.getPreferences.matchFulfilled, (state, action) => { + state.selectedLanguage = action.payload.defaultLanguage + moment.locale(action.payload.defaultLanguage); + }) }, selectors: { selectInitComplete: state => state.initComplete, diff --git a/src/client/app/redux/slices/graphSlice.ts b/src/client/app/redux/slices/graphSlice.ts index 139c21df5..f0f0e1798 100644 --- a/src/client/app/redux/slices/graphSlice.ts +++ b/src/client/app/redux/slices/graphSlice.ts @@ -9,7 +9,7 @@ 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 { ComparePeriod, SortingOrder, calculateCompareTimeInterval, validateComparePeriod, validateSortingOrder } from '../../utils/calculateCompare'; import { AreaUnitType } from '../../utils/getAreaUnitConversion'; const defaultState: GraphState = { @@ -33,7 +33,8 @@ const defaultState: GraphState = { meterOrGroupID: undefined, meterOrGroup: undefined, readingInterval: ReadingInterval.Hourly - } + }, + hotlinked: false }; interface History { @@ -241,12 +242,82 @@ export const graphSlice = createSlice({ state.current = next } }) + .addCase(processGraphLink, ({ current }, { payload }) => { + current.hotlinked = true + payload.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 'areaNormalization': + current.areaNormalization = value === 'true' + break; + case 'areaUnit': + current.selectedAreaUnit = value as AreaUnitType + break; + case 'barDuration': + current.barDuration = moment.duration(parseInt(value), 'days'); + break; + case 'barStacking': + current.barStacking = value === 'true' + break; + case 'chartType': + current.chartToRender = value as ChartTypes + break; + case 'comparePeriod': + { + current.comparePeriod = validateComparePeriod(value) + current.compareTimeInterval = calculateCompareTimeInterval(validateComparePeriod(value), moment()) + } + break; + case 'compareSortingOrder': + current.compareSortingOrder = validateSortingOrder(value); + break; + case 'groupIDs': + current.selectedGroups = value.split(',').map(s => parseInt(s)); + break; + case 'meterIDs': + current.selectedMeters = value.split(',').map(s => parseInt(s)); + break; + case 'meterOrGroup': + current.threeD.meterOrGroup = value as MeterOrGroup; + break; + case 'meterOrGroupID': + current.threeD.meterOrGroupID = parseInt(value); + break; + case 'minMax': + current.showMinMax = value === 'true' + break; + case 'rate': + { + const params = value.split(','); + const rate = { label: params[0], rate: parseFloat(params[1]) } as LineGraphRate; + current.lineGraphRate = rate; + } + break; + case 'readingInterval': + current.threeD.readingInterval = parseInt(value); + break; + case 'serverRange': + current.queryTimeInterval = TimeInterval.fromString(value); + break; + case 'sliderRange': + current.rangeSliderInterval = TimeInterval.fromString(value); + break; + case 'unitID': + current.selectedUnit = parseInt(value) + break; + } + }) + }) .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 + if (!current.hotlinked) { + const { defaultAreaUnit, defaultChartToRender, defaultBarStacking, defaultAreaNormalization } = action.payload + current.selectedAreaUnit = defaultAreaUnit + current.chartToRender = defaultChartToRender + current.barStacking = defaultBarStacking + current.areaNormalization = defaultAreaNormalization + } }) }, selectors: { @@ -331,4 +402,6 @@ export const { export const historyStepBack = createAction('graph/historyStepBack') export const historyStepForward = createAction('graph/historyStepForward') -export const updateHistory = createAction('graph/updateHistory') \ No newline at end of file +export const updateHistory = createAction('graph/updateHistory') + +export const processGraphLink = createAction('graph/graphLink') \ No newline at end of file diff --git a/src/client/app/styles/index.css b/src/client/app/styles/index.css index 4afc34566..d1e2c491a 100644 --- a/src/client/app/styles/index.css +++ b/src/client/app/styles/index.css @@ -15,3 +15,12 @@ body { margin: 0; padding: 0; } + +.no_scrollbar { + scrollbar-width: none; /* Firefox 64+ */ + -ms-overflow-style: none; /* Internet Explorer and Edge */ + /* Hide scrollbar for Webkit browsers */ + ::-webkit-scrollbar { + display: none; + } +} \ 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 e9e526e19..2968ad3ef 100644 --- a/src/client/app/types/redux/graph.ts +++ b/src/client/app/types/redux/graph.ts @@ -74,4 +74,5 @@ export interface GraphState { showMinMax: boolean; threeD: ThreeDState; queryTimeInterval: TimeInterval; + hotlinked: boolean; } \ No newline at end of file