From 15d31fd655b0e348cdc6b2fa228fbdfbd41ffe36 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 9 Sep 2026 13:22:02 -0400 Subject: [PATCH 01/11] create controller fetch hook --- .../wwwroot/Scripts/TSX/SystemCenter/hooks.ts | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/hooks.ts b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/hooks.ts index c9570207a..a6c96a4bd 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/hooks.ts +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/hooks.ts @@ -21,9 +21,12 @@ // //****************************************************************************************************** +import * as React from 'react'; import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import { Application } from '@gpa-gemstone/application-typings'; +import { GenericController } from '@gpa-gemstone/react-interactive'; import { AppDispatch, RootState } from './Store/Store'; -import * as React from 'react'; + export const useAppDispatch: () => AppDispatch = useDispatch; export const useAppSelector: TypedUseSelectorHook = useSelector; @@ -38,4 +41,38 @@ export const useBoundPaging = (currentPage: number, totalPages: number, setPage: } }, [totalPages, currentPage]) -} \ No newline at end of file +} + +export function useControllerFetch(controller: GenericController, sortField?: keyof T, ascending?: boolean, parentID?: string|number, refreshCount?: number,) { + + const fetchHandle = React.useRef | null>(null); + const [fetchData, setFetchData] = React.useState([]); + const [fetchStatus, setFetchStatus] = React.useState('uninitiated'); + const sortKey = sortField ?? controller.DefaultSort; + const asc = ascending ?? controller.Ascending; + + const refetchData = React.useCallback(() => { + setFetchStatus('loading'); + + fetchHandle.current = controller.Fetch(parentID, sortKey, asc) + .done((data: T[]) => { + setFetchData(data); + setFetchStatus('idle'); + }) + .fail(() => setFetchStatus('error')) + const cleanup = () => { + if (fetchHandle.current?.abort != null) fetchHandle.current.abort(); + } + return cleanup + }, [controller, parentID, sortKey, asc]) + + React.useEffect(() => { + const cleanup = refetchData(); + return cleanup; + }, [refetchData, refreshCount]) + + return { + Data: fetchData, + Status: fetchStatus + } +} \ No newline at end of file From f9d7daace65c0bc64a945444656fe1a7c3e3d56d Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 9 Sep 2026 13:24:22 -0400 Subject: [PATCH 02/11] replace assetTypeSlice with controller --- .../TSX/SystemCenter/Asset/AssetSelect.tsx | 18 ++++++-------- .../TSX/SystemCenter/Asset/ByAsset.tsx | 12 +++------- .../AssetGroups/AssetAssetGroup.tsx | 13 +--------- .../SystemCenter/AssetGroups/ByAssetGroup.tsx | 17 ++++++------- .../SystemCenter/Location/LocationAsset.tsx | 17 +++++-------- .../SystemCenter/NewMeterWizard/AssetPage.tsx | 24 +++++++------------ 6 files changed, 32 insertions(+), 69 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetSelect.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetSelect.tsx index 0031736e6..d25ccde33 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetSelect.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetSelect.tsx @@ -24,11 +24,11 @@ import * as React from 'react'; import * as _ from 'lodash'; -import { ByAssetSlice, AssetTypeSlice } from '../Store/Store'; +import { ByAssetSlice } from '../Store/Store'; import { OpenXDA, SystemCenter } from '@gpa-gemstone/application-typings'; -import { Search } from '@gpa-gemstone/react-interactive'; +import { Search, GenericController } from '@gpa-gemstone/react-interactive'; import { DefaultSelects } from '@gpa-gemstone/common-pages'; -import { useAppDispatch, useAppSelector } from '../hooks'; +import { useControllerFetch } from '../hooks'; import { Column } from '@gpa-gemstone/react-table'; declare var homePath: string; @@ -44,10 +44,11 @@ interface IProps { children?: React.ReactNode } +const AssetTypeController = new GenericController(`${homePath}api/OpenXDA/AssetType`, 'Name'); + export default function AssetSelect(props: IProps) { - const assetType = useAppSelector(AssetTypeSlice.Data); - const assetTypeStatus = useAppSelector(AssetTypeSlice.Status); - const dispatch = useAppDispatch(); + + const { Data: assetType } = useControllerFetch(AssetTypeController); const lineSegmentFilter: Search.IFilter = { @@ -58,11 +59,6 @@ export default function AssetSelect(props: IProps) { IsPivotColumn: false }; - React.useEffect(() => { - if (assetTypeStatus == 'changed' || assetTypeStatus == 'uninitiated') - dispatch(AssetTypeSlice.Fetch()); - }, [assetTypeStatus]); - function getEnum(setOptions, field) { if (field.key == 'AssetType' && field.type == 'enum') { setOptions(assetType.map((t) => ({ Value: t.Name, Label: t.Name }))) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/ByAsset.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/ByAsset.tsx index 170be9427..323c1f353 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/ByAsset.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/ByAsset.tsx @@ -36,9 +36,8 @@ import ExternalDBUpdate from '../CommonComponents/ExternalDBUpdate'; import CapBankRelayAttributes from '../AssetAttribute/CapBankRelay'; import { Search, Modal, LoadingIcon, ServerErrorIcon, TabSelector, SearchBar, GenericController } from '@gpa-gemstone/react-interactive'; import { Paging, Table, Column } from '@gpa-gemstone/react-table'; -import { useAppDispatch, useAppSelector } from '../hooks'; +import { useAppDispatch, useAppSelector, useControllerFetch } from '../hooks'; import { SelectAssetStatus, FetchAsset, SelectAssets } from '../Store/AssetSlice'; -import { AssetTypeSlice } from '../Store/Store'; import DERAttributes from '../AssetAttribute/DER'; import GenerationAttributes from '../AssetAttribute/Generation'; import StationAuxAttributes from '../AssetAttribute/StationAux'; @@ -60,6 +59,7 @@ const extDBTabList = [ { Label: 'Capacitor Bank Relay', Id:'CapacitorBankRelay'} ]; const AssetController = new GenericController(`${homePath}api/OpenXDA/ByRestrictedDetailedAsset`, "AssetName", true); +const AssetTypeController = new GenericController(`${homePath}api/OpenXDA/AssetType`, 'Name'); declare var homePath: string; @@ -67,8 +67,7 @@ const ByAsset: Application.Types.iByComponent = (props) => { let navigate = useNavigate(); const dispatch = useAppDispatch(); - const assetType = useAppSelector(AssetTypeSlice.Data); - const assetTypeStatus = useAppSelector(AssetTypeSlice.Status); + const { Data: assetType } = useControllerFetch(AssetTypeController); const [data, setData] = React.useState([]); const [ascending, setAscending] = React.useState(true); @@ -93,11 +92,6 @@ const ByAsset: Application.Types.iByComponent = (props) => { const [assetErrors, setAssetErrors] = React.useState([]); - React.useEffect(() => { - if (assetTypeStatus == 'changed' || assetTypeStatus == 'uninitiated') - dispatch(AssetTypeSlice.Fetch()); - }, [assetTypeStatus]); - React.useEffect(() => { if (assetType.length == 0) return; diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetAssetGroup.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetAssetGroup.tsx index 651c90bcc..154597a44 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetAssetGroup.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/AssetAssetGroup.tsx @@ -26,12 +26,11 @@ import * as React from 'react'; import * as _ from 'lodash'; import { useNavigate } from 'react-router-dom'; import { Table, Column, Paging } from '@gpa-gemstone/react-table'; -import { AssetTypeSlice } from '../Store/Store'; import { Application, SystemCenter } from '@gpa-gemstone/application-typings'; import { Warning } from '@gpa-gemstone/react-interactive'; import { ToolTip } from '@gpa-gemstone/react-forms'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; -import { useAppDispatch, useAppSelector } from '../hooks'; +import { useAppSelector } from '../hooks'; import AssetSelect from '../Asset/AssetSelect'; import { SelectRoles } from '../Store/UserSettings'; @@ -50,9 +49,6 @@ function AssetAssetGroupWindow(props: { AssetGroupID: number }) { const [recordsPerPage, setRecordsPerPage] = React.useState(0); const [assetStatus, setAssetStatus] = React.useState('uninitiated'); const [refreshTrigger, setRefreshTrigger] = React.useState(false); - const assetType = useAppSelector(AssetTypeSlice.Data); - const assetTypeStatus = useAppSelector(AssetTypeSlice.Status); - const dispatch = useAppDispatch(); const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); const roles = useAppSelector(SelectRoles); @@ -91,11 +87,6 @@ function AssetAssetGroupWindow(props: { AssetGroupID: number }) { } }, [props.AssetGroupID, refreshTrigger, ascending, page, sortKey]); - React.useEffect(() => { - if (assetTypeStatus == 'changed' || assetTypeStatus == 'uninitiated') - dispatch(AssetTypeSlice.Fetch()); - }, [assetTypeStatus]); - function saveItems(items: SystemCenter.Types.DetailedAsset[]) { let handle = $.ajax({ @@ -109,8 +100,6 @@ function AssetAssetGroupWindow(props: { AssetGroupID: number }) { }); handle.done(() => setRefreshTrigger(val => !val)) - - } function removeItem(id: number) { diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/ByAssetGroup.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/ByAssetGroup.tsx index 8b453ad23..0bb912ad6 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/ByAssetGroup.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/AssetGroups/ByAssetGroup.tsx @@ -26,12 +26,12 @@ import { Table, Column } from '@gpa-gemstone/react-table'; import * as _ from 'lodash'; import { useNavigate } from "react-router-dom"; import { Application, OpenXDA, SystemCenter } from '@gpa-gemstone/application-typings' -import { Search, Modal } from '@gpa-gemstone/react-interactive'; +import { Search, Modal, GenericController } from '@gpa-gemstone/react-interactive'; import { CheckBox, Input } from '@gpa-gemstone/react-forms'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; -import { AssetGroupSlice, ByAssetSlice, ByMeterSlice, AssetTypeSlice } from '../Store/Store'; +import { AssetGroupSlice, ByMeterSlice } from '../Store/Store'; import { DefaultSearch, DefaultSelects } from '@gpa-gemstone/common-pages'; -import { useAppDispatch, useAppSelector } from '../hooks'; +import { useAppDispatch, useAppSelector, useControllerFetch } from '../hooks'; import AssetSelect from '../Asset/AssetSelect'; declare var homePath: string; @@ -41,6 +41,8 @@ interface extendedAssetGroup extends OpenXDA.Types.AssetGroup { MeterList: Array const emptyAssetGroup: extendedAssetGroup = { ID: -1, Name: '', DisplayDashboard: true, AssetGroups: 0, Meters: 0, Assets: 0, Users: 0, MeterList: [], AssetList: [], UserList: [], AssetGroupList: [], DisplayEmail: false }; +const AssetTypeController = new GenericController(`${homePath}api/OpenXDA/AssetType`, 'Name'); + declare var homePath: string; const ByAssetGroup: Application.Types.iByComponent = (props) => { @@ -55,8 +57,8 @@ const ByAssetGroup: Application.Types.iByComponent = (props) => { const status = useAppSelector(AssetGroupSlice.Status); const allAssetGroups = useAppSelector(AssetGroupSlice.Data); - const assetType = useAppSelector(AssetTypeSlice.Data); - const assetTypeStatus = useAppSelector(AssetTypeSlice.Status); + const { Data: assetType } = useControllerFetch(AssetTypeController); + const [showFilter, setFilter] = React.useState<('None' | 'Meter' | 'Asset' | 'Asset Group' | 'Station')>('None'); @@ -74,11 +76,6 @@ const ByAssetGroup: Application.Types.iByComponent = (props) => { dispatch(AssetGroupSlice.DBSearch({ filter: searchFields })); }, [searchStatus]); - React.useEffect(() => { - if (assetTypeStatus == 'changed' || assetTypeStatus == 'uninitiated') - dispatch(AssetTypeSlice.Fetch()); - }, [assetTypeStatus]); - React.useEffect(() => { let e = []; if (newAssetGroup.Name == null || newAssetGroup.Name.length == 0) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Location/LocationAsset.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Location/LocationAsset.tsx index 8202b1822..352b7f88e 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Location/LocationAsset.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Location/LocationAsset.tsx @@ -32,11 +32,10 @@ import BusAttributes from '../AssetAttribute/Bus'; import CapBankAttributes from '../AssetAttribute/CapBank'; import LineAttributes from '../AssetAttribute/Line'; import TransformerAttributes from '../AssetAttribute/Transformer'; -import { useAppSelector, useAppDispatch } from '../hooks'; +import { useAppSelector, useAppDispatch, useControllerFetch } from '../hooks'; import { FetchAsset, SelectAssets, SelectAssetStatus } from '../Store/AssetSlice'; -import { AssetTypeSlice } from '../Store/Store'; import { getAssetWithAdditionalFields, editExistingAsset } from '../../../TS/Services/Asset'; -import { LoadingIcon, Modal, ServerErrorIcon } from '@gpa-gemstone/react-interactive'; +import { LoadingIcon, Modal, ServerErrorIcon, GenericController } from '@gpa-gemstone/react-interactive'; import { ToolTip } from '@gpa-gemstone/react-forms'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; import GenerationAttributes from '../AssetAttribute/Generation'; @@ -47,6 +46,8 @@ import { SelectRoles } from '../Store/UserSettings'; declare var homePath: string; const PagingID = 'LocationAssetPage'; +const AssetTypeController = new GenericController(`${homePath}api/OpenXDA/AssetType`, 'Name'); + function LocationAssetWindow(props: { Location: OpenXDA.Types.Location }): JSX.Element{ let navigate = useNavigate(); let dispatch = useAppDispatch(); @@ -67,22 +68,16 @@ function LocationAssetWindow(props: { Location: OpenXDA.Types.Location }): JSX.E const [lStatus, setLStatus] = React.useState<'error' | 'loading' | 'idle'>('idle'); const aStatus = useAppSelector(SelectAssetStatus); - const atStatus = useAppSelector(AssetTypeSlice.Status); - - const assetTypes = useAppSelector(AssetTypeSlice.Data); const allAssets = useAppSelector(SelectAssets); + const { Data: assetTypes, Status: atStatus } = useControllerFetch(AssetTypeController); + const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); const roles = useAppSelector(SelectRoles); const [page, setPage] = React.useState(0); const [pageInfo, setPageInfo] = React.useState<{ RecordsPerPage: number, NumberOfPages: number, TotalRecords: number }>({ RecordsPerPage: 0, NumberOfPages: 0, TotalRecords: 0 }); - React.useEffect(() => { - if (atStatus == 'uninitiated' || atStatus == 'changed') - dispatch(AssetTypeSlice.Fetch()); - }, []); - React.useEffect(() => { if (aStatus == 'uninitiated' || aStatus == 'changed') dispatch(FetchAsset()); diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/NewMeterWizard/AssetPage.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/NewMeterWizard/AssetPage.tsx index 83304a7b1..4a07b0500 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/NewMeterWizard/AssetPage.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/NewMeterWizard/AssetPage.tsx @@ -23,8 +23,7 @@ import * as React from 'react'; import * as _ from 'lodash'; -import { Application, OpenXDA, SystemCenter } from '@gpa-gemstone/application-typings'; -import { CreateGuid } from '@gpa-gemstone/helper-functions'; +import { OpenXDA, SystemCenter } from '@gpa-gemstone/application-typings'; import BreakerAttributes from '../AssetAttribute/Breaker'; import BusAttributes from '../AssetAttribute/Bus'; import CapBankAttributes from '../AssetAttribute/CapBank'; @@ -32,16 +31,15 @@ import LineAttributes from '../AssetAttribute/Line'; import TransformerAttributes from '../AssetAttribute/Transformer'; import { AssetAttributes } from '../AssetAttribute/Asset'; import CapBankRelayAttributes from '../AssetAttribute/CapBankRelay'; -import { useAppDispatch, useAppSelector } from '../hooks'; -import { ByAssetSlice, AssetTypeSlice } from '../Store/Store'; +import { useAppDispatch, useAppSelector, useControllerFetch } from '../hooks'; +import { ByAssetSlice } from '../Store/Store'; import { SelectAssetStatus, FetchAsset, SelectAssets } from '../Store/AssetSlice'; -import { Modal, Search, TabSelector } from '@gpa-gemstone/react-interactive'; +import { Modal, Search, TabSelector, GenericController } from '@gpa-gemstone/react-interactive'; import DERAttributes from '../AssetAttribute/DER'; import AssetSelect from '../Asset/AssetSelect'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; import { getAssetWithAdditionalFields } from '../../../TS/Services/Asset'; import LocationDrawings from '../Meter/PropertyUI/LocationDrawings'; -import { GetNodeSize } from '@gpa-gemstone/helper-functions'; import { Table, Column } from '@gpa-gemstone/react-table'; import GenerationAttributes from '../AssetAttribute/Generation'; import StationAuxAttributes from '../AssetAttribute/StationAux'; @@ -65,12 +63,14 @@ interface IProps { // temp key for new assets, this should never make it out of here const tempKey = "Something_is_wrong_with_NMW"; +const AssetTypeController = new GenericController(`${homePath}api/OpenXDA/AssetType`, 'Name'); + type AssetType = OpenXDA.Types.DetailedAsset export default function AssetPage(props: IProps) { const dispatch = useAppDispatch(); - const assetTypes = useAppSelector(AssetTypeSlice.Data); - const atStatus = useAppSelector(AssetTypeSlice.Status); + const { Data: assetTypes } = useControllerFetch(AssetTypeController); + const assets = useAppSelector(SelectAssets); const aStatus = useAppSelector(SelectAssetStatus); const byAssetStatus = useAppSelector(ByAssetSlice.Status); @@ -152,14 +152,6 @@ export default function AssetPage(props: IProps) { setChannelsWorking(props.Channels); }, [props.Channels]); - React.useEffect(() => { - if (atStatus === 'uninitiated' || atStatus === 'changed') { - dispatch(AssetTypeSlice.Fetch()); - return function () { - } - } - }, [atStatus]); - React.useEffect(() => { if (aStatus === 'uninitiated' || aStatus === 'changed') { dispatch(FetchAsset()); From e891d2ed9fc065d4915dc79a5a423c685656cea7 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 9 Sep 2026 13:24:51 -0400 Subject: [PATCH 03/11] replace dataOperationSlice with controller --- .../Scripts/TSX/SystemCenter/Settings/DataOperations.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Settings/DataOperations.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Settings/DataOperations.tsx index e9514aebc..673d7c159 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Settings/DataOperations.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Settings/DataOperations.tsx @@ -25,7 +25,6 @@ import { Application, OpenXDA } from '@gpa-gemstone/application-typings'; import { SystemCenter } from '../global'; import { GenericController, Modal, Warning } from '@gpa-gemstone/react-interactive'; import * as React from 'react'; -import { DataOperationSlice } from '../Store/Store'; import GenericByPage from '../CommonComponents/GenericByPage'; import { Input } from '@gpa-gemstone/react-forms'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; @@ -155,7 +154,6 @@ const DataOperations: Application.Types.iByComponent = (props) => { CallBack={(conf) => { if (conf) DataOperationController.DBAction("DELETE", editnewSetting).done(() => refreshData(x => x + 1)); - DataOperationSlice.DBAction({ verb: 'DELETE', record: editnewSetting }); setShowWarning(false); }} /> From 117de5930beee6289407abdd9d076f98ecb0db93 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 9 Sep 2026 13:26:14 -0400 Subject: [PATCH 04/11] replace eventTypeSlice with controller --- .../CommonComponents/FilterSelect.tsx | 18 +++++++----------- .../SystemCenter/EventType/EventTypeForm.tsx | 17 +++++++---------- .../MATLABAnalytics/MATLABAnalyticForm.tsx | 16 +++++++--------- 3 files changed, 21 insertions(+), 30 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/FilterSelect.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/FilterSelect.tsx index 842dffe5d..95ec370d7 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/FilterSelect.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/FilterSelect.tsx @@ -24,11 +24,11 @@ import * as React from 'react'; import * as _ from 'lodash'; -import { ByMeterSlice, ByAssetSlice, AssetTypeSlice, ByLocationSlice, CustomerSlice } from '../Store/Store'; +import { ByMeterSlice, ByAssetSlice, ByLocationSlice, CustomerSlice } from '../Store/Store'; import { OpenXDA, SystemCenter } from '@gpa-gemstone/application-typings'; -import { Search } from '@gpa-gemstone/react-interactive'; +import { Search, GenericController } from '@gpa-gemstone/react-interactive'; import { DefaultSelects } from '@gpa-gemstone/common-pages'; -import { useAppDispatch, useAppSelector } from '../hooks'; +import { useAppSelector, useControllerFetch } from '../hooks'; import { Column } from '@gpa-gemstone/react-table'; declare var homePath: string; @@ -46,10 +46,11 @@ interface IProps { type Data = (SystemCenter.Types.DetailedMeter | SystemCenter.Types.DetailedLocation | OpenXDA.Types.Customer |SystemCenter.Types.DetailedAsset); +const AssetTypeController = new GenericController(`${homePath}api/OpenXDA/AssetType`, 'Name'); + export default function FilterSelect(props: IProps) { - const assetType = useAppSelector(AssetTypeSlice.Data); - const assetTypeStatus = useAppSelector(AssetTypeSlice.Status); - const dispatch = useAppDispatch(); + + const { Data: assetType } = useControllerFetch(AssetTypeController); const select = React.useMemo(() => { switch (props.Type) { @@ -69,11 +70,6 @@ export default function FilterSelect(props: IProps) { setSelectedData(data.filter(i => props.Selected.has(i.ID))); }, [data, props.Selected]); - React.useEffect(() => { - if (assetTypeStatus == 'changed' || assetTypeStatus == 'uninitiated') - dispatch(AssetTypeSlice.Fetch()); - }, [assetTypeStatus]); - const closeCallback = React.useCallback((selected: Data[], confirmed: boolean) => { const newSelected = new Set(); selected.forEach(item => newSelected.add(item.ID)); diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/EventType/EventTypeForm.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/EventType/EventTypeForm.tsx index e170049fa..b6b175f16 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/EventType/EventTypeForm.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/EventType/EventTypeForm.tsx @@ -22,10 +22,11 @@ //****************************************************************************************************** import * as React from 'react'; -import { useAppSelector, useAppDispatch } from '../hooks'; -import { Input, CheckBox, TextArea } from '@gpa-gemstone/react-forms'; -import { AssetTypeSlice, EventTypeAssetTypeSlice, EventTypeSlice } from '../Store/Store'; +import { useAppSelector, useAppDispatch, useControllerFetch } from '../hooks'; +import { Input, CheckBox } from '@gpa-gemstone/react-forms'; +import { EventTypeAssetTypeSlice } from '../Store/Store'; import { Application, OpenXDA } from '@gpa-gemstone/application-typings'; +import { GenericController } from '@gpa-gemstone/react-interactive'; interface IProps { Record: OpenXDA.Types.EventType, @@ -34,6 +35,8 @@ interface IProps { setAssetTypeETs: (records: OpenXDA.Types.EventTypeAssetType[]) => void } +const AssetTypeController = new GenericController(`${homePath}api/OpenXDA/AssetType`, 'Name'); + export default function EventTypeForm(props: IProps) { React.useEffect(() => { let e = []; @@ -80,8 +83,7 @@ function EventTypeAssetTypeForm(props: IRightProps) { const dispatch = useAppDispatch(); - const assetTypes = useAppSelector(AssetTypeSlice.Data) as OpenXDA.Types.AssetType[]; - const atStatus = useAppSelector(AssetTypeSlice.Status) as Application.Types.Status; + const { Data: assetTypes } = useControllerFetch(AssetTypeController); const eventTypeAssetTypeData = useAppSelector(EventTypeAssetTypeSlice.Data); const eventTypeAssettypeParentID = useAppSelector(EventTypeAssetTypeSlice.ParentID); @@ -94,11 +96,6 @@ function EventTypeAssetTypeForm(props: IRightProps) { React.useEffect(() => { props.SetAssetTypeETs(etAt); }, [etAt]); - React.useEffect(() => { - if (atStatus == 'uninitiated' || atStatus == 'changed') - dispatch(AssetTypeSlice.Fetch()); - }, [atStatus]); - React.useEffect(() => { if (atetStatus == 'uninitiated' || atetStatus == 'changed' || eventTypeAssettypeParentID != props?.ID) dispatch(EventTypeAssetTypeSlice.Fetch(props?.ID)); diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/MATLABAnalytics/MATLABAnalyticForm.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/MATLABAnalytics/MATLABAnalyticForm.tsx index 85d77500f..a04335473 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/MATLABAnalytics/MATLABAnalyticForm.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/MATLABAnalytics/MATLABAnalyticForm.tsx @@ -23,10 +23,13 @@ import * as React from 'react'; import * as _ from 'lodash'; -import { useAppSelector, useAppDispatch } from '../hooks'; -import { MATLABAnalyticEventTypeSlice, MATLABAnalyticAssetTypeSlice, EventTypeSlice, AssetTypeSlice } from '../Store/Store'; +import { useAppSelector, useAppDispatch, useControllerFetch } from '../hooks'; +import { MATLABAnalyticEventTypeSlice, MATLABAnalyticAssetTypeSlice, EventTypeSlice } from '../Store/Store'; import { OpenXDA } from '@gpa-gemstone/application-typings'; import { Input, MultiCheckBoxSelect } from '@gpa-gemstone/react-forms'; +import { GenericController } from '@gpa-gemstone/react-interactive'; + +const AssetTypeController = new GenericController(`${homePath}api/OpenXDA/AssetType`, 'Name'); export default function MATLABAnalyticForm(props: { Record: OpenXDA.Types.MATLABAnalytic, @@ -48,8 +51,8 @@ export default function MATLABAnalyticForm(props: { const allEventTypes = useAppSelector(EventTypeSlice.Data); const allEventTypesStatus = useAppSelector(EventTypeSlice.Status); - const allAssetTypes = useAppSelector(AssetTypeSlice.Data); - const allAssetTypesStatus = useAppSelector(AssetTypeSlice.Status); + + const { Data: allAssetTypes } = useControllerFetch(AssetTypeController); const eventTypeParentID = useAppSelector(MATLABAnalyticEventTypeSlice.ParentID); const assetTypeParentID = useAppSelector(MATLABAnalyticAssetTypeSlice.ParentID); @@ -59,11 +62,6 @@ export default function MATLABAnalyticForm(props: { dispatch(EventTypeSlice.Fetch()); }, [allEventTypesStatus]); - React.useEffect(() => { - if (allAssetTypesStatus == 'uninitiated' || allAssetTypesStatus == 'changed') - dispatch(AssetTypeSlice.Fetch()); - }, [allAssetTypesStatus]); - React.useEffect(() => { if (eventTypeStatus == 'uninitiated' || eventTypeStatus == 'changed' || eventTypeParentID != props.Record.ID) dispatch(MATLABAnalyticEventTypeSlice.Fetch(props.Record.ID)); From a853cc38f24f1b67fc247219762602fab304fbba Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 9 Sep 2026 13:27:32 -0400 Subject: [PATCH 05/11] create controllerSelectPopup --- .../ControllerSelectPopup.tsx | 172 ++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/ControllerSelectPopup.tsx diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/ControllerSelectPopup.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/ControllerSelectPopup.tsx new file mode 100644 index 000000000..92d4f34f7 --- /dev/null +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/ControllerSelectPopup.tsx @@ -0,0 +1,172 @@ +// ****************************************************************************************************** +// ControllerSelectPopup.tsx - Gbtc +// +// Copyright © 2026, Grid Protection Alliance. All Rights Reserved. +// +// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See +// the NOTICE file distributed with this work for additional information regarding copyright ownership. +// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this +// file except in compliance with the License. You may obtain a copy of the License at: +// +// http://opensource.org/licenses/MIT +// +// Unless agreed to in writing, the subject software distributed under the License is distributed on an +// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the +// License for the specific language governing permissions and limitations. +// +// Code Modification History: +// ---------------------------------------------------------------------------------------------------- +// 08/12/2026 - Natalie Beatty +// Generated original version of source code. +// ****************************************************************************************************** + +import { Table, Column, FilterableColumn, ConfigurableColumn } from "@gpa-gemstone/react-table"; +import * as React from 'react'; +import { Modal, GenericController, Search } from "@gpa-gemstone/react-interactive"; +import * as _ from "lodash"; +import { ReactIcons } from "@gpa-gemstone/gpa-symbols"; + +interface U { ID: number | string } + +interface IProps { + Controller: GenericController, + Selection: T[], + OnClose: (selected: T[], conf: boolean) => void + Show: boolean, + Searchbar: (children: React.ReactNode, setFilters: (filters: Search.IFilter[]) => void) => React.ReactNode, + Type?: 'single' | 'multiple', + Title: string, + MinSelection?: number, + children?: React.ReactNode, + DisableConfirm?: boolean +} + + +export default function ControllerSelectPopup(props: IProps) { + const [filters, setFilters] = React.useState[]>([]); + const [sortField, setSortField] = React.useState('ID') + const [ascending, setAscending] = React.useState(true); + const [data, setData] = React.useState([]); + + const [selectedData, setSelectedData] = React.useState(props.Selection); + + const [sortKeySelected, setSortKeySelected] = React.useState(''); + const [ascendingSelected, setAscendingSelected] = React.useState(false); + + React.useEffect(() => { + const handle = props.Controller.DBSearch(filters, sortField, ascending); + handle.done((d) => setData(d)); + return () => { if (handle != null && handle.abort != null) handle.abort() } + }, [props.Controller, ascending, sortField, filters]) + + React.useEffect(() => { + setSelectedData(props.Selection); + }, [props.Selection]) + + React.useEffect(() => { + setFilters([]); // initialize filter list, which should add additional filters + }, []) + + function AddCurrentList() { + const updatedData = selectedData.concat(data); + setSelectedData(_.uniqBy(updatedData, (d) => d.ID)); + } + + return (<> + props.OnClose(selectedData, conf)} + DisableConfirm={props.MinSelection !== undefined && selectedData.length < props.MinSelection || (props.DisableConfirm ?? false)} + ConfirmShowToolTip={props.MinSelection !== undefined && selectedData.length < props.MinSelection} + ConfirmToolTipContent={

At least {props.MinSelection} items must be selected.

} + > +
+
+ {props.Searchbar( + <> + {props.Type === 'multiple' ?
  • +
    + Quick Selects: +
    +
    +
    { event.preventDefault(); AddCurrentList(); }}>Add Current List to Selection
    +
    +
    +
    { event.preventDefault(); setSelectedData([]) }}>Remove All
    +
    +
    +
    +
  • : null} + {React.Children.map(props.children, (e) => { + if (React.isValidElement(e)) { + if (((e as React.ReactElement).type === FilterableColumn) || + ((e as React.ReactElement).type === Column) || + ((e as React.ReactElement).type === ConfigurableColumn) + ) return null; + return e; + } + return null; + })} + , setFilters)} +
    +
    +
    +
    + + TableClass="table table-hover" + Data={data} + SortKey={sortField as string} + Ascending={ascending} + OnSort={(d) => { + if (d.colKey === "Scroll") + return; + if (d.colKey === sortField) + setAscending(asc => !asc); + else { + setSortField(d.colKey as keyof T) + setAscending(true); + } + }} + OnClick={(d) => { + if (props.Type === undefined || props.Type === 'single') + setSelectedData([d.row]) + else + setSelectedData((s) => [...s.filter(item => item.ID !== d.row.ID), d.row]) + }} + Selected={(item) => selectedData.findIndex(d => d.ID === item.ID) > -1} + KeySelector={item => item.ID} + > + {props.children} + +
    + {props.Type === 'multiple' ?
    +
    +

    Current Selection

    +
    + + TableClass="table table-hover" + Data={selectedData} + SortKey={sortKeySelected} + Ascending={ascendingSelected} + OnSort={(d) => { + if (d.colKey === sortKeySelected) { + const ordered = _.orderBy(selectedData, [d.colKey], [(!ascendingSelected ? "asc" : "desc")]) as T[]; + setAscendingSelected(!ascendingSelected); + setSelectedData(ordered); + } + else { + const ordered = _.orderBy(selectedData, [d.colKey], ["asc"]) as T[]; + setAscendingSelected(!ascendingSelected); + setSelectedData(ordered); + setSortKeySelected(d.colKey); + } + }} + OnClick={(d) => setSelectedData([...selectedData.filter(item => item.ID !== d.row.ID)])} + Selected={() => false} + KeySelector={item => item.ID} + > + {props.children} + +
    : null} +
    +
    + ) +} \ No newline at end of file From f7006374b6a956b119c4d6652ae3c4b0f00de486 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 9 Sep 2026 13:29:07 -0400 Subject: [PATCH 06/11] replace meaurement and phase slices with controllers --- .../TSX/SystemCenter/Asset/AssetChannel.tsx | 10 +---- .../ChannelGroup/ChannelGroupItemForm.tsx | 28 ++++-------- .../ChannelScaling/ChannelScalingForm.tsx | 34 ++++----------- .../SystemCenter/Meter/MeterEventChannel.tsx | 24 +++-------- .../SystemCenter/Meter/MeterTrendChannel.tsx | 36 +++++----------- .../NewMeterWizard/ChannelPage.tsx | 43 ++++++------------- .../NewMeterWizard/TemplateWindow.tsx | 6 +-- 7 files changed, 49 insertions(+), 132 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetChannel.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetChannel.tsx index 3c5c63d60..b591a8143 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetChannel.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Asset/AssetChannel.tsx @@ -24,9 +24,7 @@ import * as React from 'react'; import * as _ from 'lodash'; import { Application } from '@gpa-gemstone/application-typings'; -import { PhaseSlice, MeasurmentTypeSlice } from '../Store/Store' import { Table, Column, Paging } from '@gpa-gemstone/react-table'; -import { useAppSelector } from '../hooks'; import { LoadingIcon, ServerErrorIcon } from '@gpa-gemstone/react-interactive'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; @@ -64,16 +62,12 @@ interface ChannelDetail { //TODO: Move to Gemstone Trend: boolean } - const AssetChannelWindow = (props: IProps) => { const [assetChannels, setAssetChannels] = React.useState([]); const [page, setPage] = React.useState(0); const [totalPages, setTotalPages] = React.useState(0); const [totalRecords, setTotalRecords] = React.useState(0); const [recordsPerPage, setRecordsPerPage] = React.useState(0); - - const pStatus = useAppSelector(PhaseSlice.Status) as Application.Types.Status; - const mtStatus = useAppSelector(MeasurmentTypeSlice.Status) as Application.Types.Status; const [status, setStatus] = React.useState('idle'); const [sortField, setSortField] = React.useState('Name'); const [ascending, setAscending] = React.useState(true); @@ -109,7 +103,7 @@ const AssetChannelWindow = (props: IProps) => { } }, [props.ID, sortField, ascending, page]); - if (status == 'error' || pStatus == 'error' || mtStatus == 'error') + if (status == 'error') return
    @@ -127,7 +121,7 @@ const AssetChannelWindow = (props: IProps) => {
    - if (status == 'loading' || pStatus == 'loading' || mtStatus == 'loading') + if (status == 'loading') return
    diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ChannelGroup/ChannelGroupItemForm.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ChannelGroup/ChannelGroupItemForm.tsx index 29924d0ee..388b4f23a 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ChannelGroup/ChannelGroupItemForm.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ChannelGroup/ChannelGroupItemForm.tsx @@ -22,11 +22,11 @@ //****************************************************************************************************** import * as React from 'react'; -import { SystemCenter } from '@gpa-gemstone/application-typings'; +import { OpenXDA, SystemCenter } from '@gpa-gemstone/application-typings'; import { Input, Select } from '@gpa-gemstone/react-forms'; -import { MeasurmentTypeSlice, MeasurementCharacteristicSlice, ValueListSlice, ValueListGroupSlice } from '../Store/Store'; -import { useAppSelector, useAppDispatch } from '../hooks'; -import { LoadingIcon } from '@gpa-gemstone/react-interactive'; +import { ValueListSlice, ValueListGroupSlice } from '../Store/Store'; +import { useAppSelector, useAppDispatch, useControllerFetch } from '../hooks'; +import { LoadingIcon, GenericController } from '@gpa-gemstone/react-interactive'; interface IProps { Record: SystemCenter.Types.ChannelGroupDetails, @@ -34,13 +34,13 @@ interface IProps { SetErrors?: (e: string[]) => void } +const MeasurementTypeController = new GenericController(`${homePath}api/OpenXDA/MeasurementType`, 'Name'); +const MeasurementCharacteristicController = new GenericController(`${homePath}api/OpenXDA/MeasurementCharacteristic`, 'Name'); + export default function ChannelGroupItemForm(props: IProps) { const dispatch = useAppDispatch(); - - const measurementTypeData = useAppSelector(MeasurmentTypeSlice.Data); - const measurementTypeStatus = useAppSelector(MeasurmentTypeSlice.Status); - const measurementCharacteristicData = useAppSelector(MeasurementCharacteristicSlice.Data); - const measurementCharacteristicStatus = useAppSelector(MeasurementCharacteristicSlice.Status); + const { Data: measurementTypeData, Status: measurementTypeStatus } = useControllerFetch(MeasurementTypeController); + const { Data: measurementCharacteristicData, Status: measurementCharacteristicStatus } = useControllerFetch(MeasurementCharacteristicController); const valueListData = useAppSelector(ValueListSlice.Data); const valueListStatus = useAppSelector(ValueListSlice.Status); const valueListGroupData = useAppSelector(ValueListGroupSlice.Data); @@ -56,16 +56,6 @@ export default function ChannelGroupItemForm(props: IProps) { dispatch(ValueListSlice.Fetch()); }, [valueListStatus]); - React.useEffect(() => { - if (measurementTypeStatus == 'uninitiated' || measurementTypeStatus == 'changed') - dispatch(MeasurmentTypeSlice.Fetch()); - }, [measurementTypeStatus]); - - React.useEffect(() => { - if (measurementCharacteristicStatus == 'uninitiated' || measurementCharacteristicStatus == 'changed') - dispatch(MeasurementCharacteristicSlice.Fetch()); - }, [measurementCharacteristicStatus]); - function Valid(field: keyof (SystemCenter.Types.ChannelGroupDetails)): boolean { if (field == 'DisplayName') return props.Record.DisplayName != null && props.Record.DisplayName.length > 0 && props.Record.DisplayName.length <= 200; diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/ChannelScaling/ChannelScalingForm.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/ChannelScaling/ChannelScalingForm.tsx index 8533e425f..94b17fe38 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/ChannelScaling/ChannelScalingForm.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/ChannelScaling/ChannelScalingForm.tsx @@ -26,9 +26,8 @@ import * as React from 'react'; import * as _ from 'lodash'; import { Application, OpenXDA } from '@gpa-gemstone/application-typings'; -import { useAppSelector, useAppDispatch } from '../../hooks'; -import { MeasurementCharacteristicSlice, MeasurmentTypeSlice, PhaseSlice } from '../../Store/Store'; -import { LoadingIcon, ServerErrorIcon } from '@gpa-gemstone/react-interactive'; +import { useAppSelector, useControllerFetch } from '../../hooks'; +import { LoadingIcon, ServerErrorIcon, GenericController } from '@gpa-gemstone/react-interactive'; import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { ChannelScalingWrapper, ChannelScalingType, IMultiplier } from './ChannelScalingWrapper'; import { Input, ToolTip } from '@gpa-gemstone/react-forms'; @@ -46,20 +45,18 @@ interface IProps { TotalPages?: number } +const PhaseController = new GenericController(`${homePath}api/OpenXDA/Phase`, 'Name'); +const MeasurementTypeController = new GenericController(`${homePath}api/OpenXDA/MeasurementType`, 'Name'); +const MeasurementCharacteristicController = new GenericController(`${homePath}api/OpenXDA/MeasurementCharacteristic`, 'Name'); const ChannelScalingForm = (props: IProps) => { - const dispatch = useAppDispatch(); - const [multiplier, setMultiplier] = React.useState({ Voltage: 1, Current: 1 }); const [Wrappers, setWrappers] = React.useState([]); - const phases = useAppSelector(PhaseSlice.Data) as OpenXDA.Types.Phase[]; - const measurementTypes = useAppSelector(MeasurmentTypeSlice.Data) as OpenXDA.Types.MeasurementType[]; - const measurementCharacteristics = useAppSelector(MeasurementCharacteristicSlice.Data) as OpenXDA.Types.MeasurementCharacteristic[]; + const { Data: phases, Status: pStatus } = useControllerFetch(PhaseController); + const { Data: measurementTypes, Status: mtStatus } = useControllerFetch(MeasurementTypeController); + const { Data: measurementCharacteristics, Status: mcStatus } = useControllerFetch(MeasurementCharacteristicController); - const pStatus = useAppSelector(PhaseSlice.Status) as Application.Types.Status; - const mtStatus = useAppSelector(MeasurmentTypeSlice.Status) as Application.Types.Status; - const mcStatus = useAppSelector(MeasurementCharacteristicSlice.Status) as Application.Types.Status; const [status, setStatus] = React.useState('idle'); const [hover, setHover] = React.useState<('Reset' | 'None' | 'Replace' | 'Adjust')>('None'); @@ -100,21 +97,6 @@ const ChannelScalingForm = (props: IProps) => { localStorage.setItem('SystemCenter.ChannelScaling.' + props.Key + '.I', multiplier.Current.toString()); }, [multiplier.Voltage, multiplier.Current]); - React.useEffect(() => { - if (pStatus == 'uninitiated' || pStatus == 'changed') - dispatch(PhaseSlice.Fetch()); - }, [pStatus]) - - React.useEffect(() => { - if (mtStatus == 'uninitiated' || pStatus == 'changed') - dispatch(MeasurmentTypeSlice.Fetch()); - }, [mtStatus]) - - React.useEffect(() => { - if (mcStatus == 'uninitiated' || pStatus == 'changed') - dispatch(MeasurementCharacteristicSlice.Fetch()); - }, [mcStatus]) - React.useEffect(() => { initializeWrappers(); }, [props.Channels, status, pStatus, mtStatus, mcStatus]); diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterEventChannel.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterEventChannel.tsx index 20408154f..3e27c1b18 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterEventChannel.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterEventChannel.tsx @@ -24,8 +24,7 @@ import * as React from 'react'; import * as _ from 'lodash'; import { Application, OpenXDA as GemstoneOpenXDA } from '@gpa-gemstone/application-typings'; -import { PhaseSlice, MeasurmentTypeSlice } from '../Store/Store' -import { useAppSelector, useAppDispatch } from '../hooks'; +import { useAppSelector, useControllerFetch } from '../hooks'; import { LoadingIcon, ServerErrorIcon, Warning, GenericController } from '@gpa-gemstone/react-interactive'; import { Input, Select, ToolTip } from '@gpa-gemstone/react-forms'; import { AssetAttributes } from '../AssetAttribute/Asset'; @@ -42,9 +41,10 @@ interface IProps { Meter: GemstoneOpenXDA.Types.Meter, IsVisible: boolean } type RecordChange = Map>; const EventChannelController = new GenericController(`${homePath}api/OpenXDA/EventChannel`, "Name"); +const PhaseController = new GenericController(`${homePath}api/OpenXDA/Phase`, 'Name'); +const MeasurementTypeController = new GenericController(`${homePath}api/OpenXDA/MeasurementType`, 'Name'); const MeterEventChannelWindow = (props: IProps) => { - const dispatch = useAppDispatch(); const [data, setData] = React.useState([]); const [sortKey, setSortKey] = React.useState('Name'); @@ -57,12 +57,12 @@ const MeterEventChannelWindow = (props: IProps) => { const [status, setStatus] = React.useState('uninitiated'); const [refreshTrigger, setRefreshTrigger] = React.useState(false); - const phases = useAppSelector(PhaseSlice.Data) as GemstoneOpenXDA.Types.Phase[]; - const measurementTypes = useAppSelector(MeasurmentTypeSlice.Data) as GemstoneOpenXDA.Types.MeasurementType[]; + + const { Data: phases, Status: pStatus } = useControllerFetch(PhaseController); + const { Data: measurementTypes, Status: mtStatus } = useControllerFetch(MeasurementTypeController); + const [assets, setAssets] = React.useState([]); - const pStatus = useAppSelector(PhaseSlice.Status) as Application.Types.Status; - const mtStatus = useAppSelector(MeasurmentTypeSlice.Status) as Application.Types.Status; const [assetStatus, setAssetStatus] = React.useState('idle') const [removeRecord, setRemoveRecord] = React.useState(null); @@ -71,16 +71,6 @@ const MeterEventChannelWindow = (props: IProps) => { const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None' | 'Add')>('None'); const roles = useAppSelector(SelectRoles); - React.useEffect(() => { - if (pStatus == 'uninitiated' || pStatus == 'changed') - dispatch(PhaseSlice.Fetch()); - }, [pStatus]) - - React.useEffect(() => { - if (mtStatus == 'uninitiated' || mtStatus == 'changed') - dispatch(MeasurmentTypeSlice.Fetch()); - }, [mtStatus]) - React.useEffect(() => { setStatus('loading'); const handle = EventChannelController.PagedSearch([], sortKey, ascending, page, props.Meter.ID); diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterTrendChannel.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterTrendChannel.tsx index 56e63f053..e0084315a 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterTrendChannel.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterTrendChannel.tsx @@ -29,9 +29,8 @@ import { LoadingIcon, ServerErrorIcon, Warning, GenericController } from '@gpa-g import { Input, Select, ToolTip } from '@gpa-gemstone/react-forms'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; import { IsNumber } from '@gpa-gemstone/helper-functions'; -import { PhaseSlice, MeasurmentTypeSlice, MeasurementCharacteristicSlice } from '../Store/Store'; import { AssetAttributes } from '../AssetAttribute/Asset'; -import { useAppSelector, useAppDispatch } from '../hooks'; +import { useAppSelector, useControllerFetch } from '../hooks'; import { ConfigurableTable, ConfigurableColumn, Column, Paging } from '@gpa-gemstone/react-table'; import { SelectRoles } from '../Store/UserSettings'; @@ -40,9 +39,11 @@ declare var homePath: string; interface IProps { Meter: GemstoneOpenXDA.Types.Meter, IsVisible: boolean } const TrendChannelController = new GenericController(`${homePath}api/OpenXDA/TrendChannel`, 'Name'); +const PhaseController = new GenericController(`${homePath}api/OpenXDA/Phase`, 'Name'); +const MeasurementTypeController = new GenericController(`${homePath}api/OpenXDA/MeasurementType`, 'Name'); +const MeasurementCharacteristicController = new GenericController(`${homePath}api/OpenXDA/MeasurementCharacteristic`, 'Name'); const MeterTrendChannelWindow = (props: IProps) => { - const dispatch = useAppDispatch(); const [data, setData] = React.useState([]); const [sortKey, setSortKey] = React.useState('Name'); @@ -55,14 +56,12 @@ const MeterTrendChannelWindow = (props: IProps) => { const [recordChanges, setRecordChanges] = React.useState>>(new Map()); const [refreshTrigger, setRefreshTrigger] = React.useState(false); - const phases = useAppSelector(PhaseSlice.Data) as GemstoneOpenXDA.Types.Phase[]; - const measurementTypes = useAppSelector(MeasurmentTypeSlice.Data) as GemstoneOpenXDA.Types.MeasurementType[]; - const measurementCharacteristics = useAppSelector(MeasurementCharacteristicSlice.Data) as GemstoneOpenXDA.Types.MeasurementCharacteristic[]; + const { Data: phases, Status: phaseStatus } = useControllerFetch(PhaseController); + const { Data: measurementTypes, Status: mtStatus } = useControllerFetch(MeasurementTypeController); + const { Data: measurementCharacteristics, Status: mcStatus } = useControllerFetch(MeasurementCharacteristicController); + const [assets, setAssets] = React.useState([]); - const phaseStatus = useAppSelector(PhaseSlice.Status) as Application.Types.Status; - const mtStatus = useAppSelector(MeasurmentTypeSlice.Status) as Application.Types.Status; - const mcStatus = useAppSelector(MeasurementCharacteristicSlice.Status) as Application.Types.Status; const [assetStatus, setAssetStatus] = React.useState('idle') const [removeRecord, setRemoveRecord] = React.useState(null); @@ -71,21 +70,6 @@ const MeterTrendChannelWindow = (props: IProps) => { const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None' | 'Add')>('None'); const roles = useAppSelector(SelectRoles); - React.useEffect(() => { - if (phaseStatus == 'uninitiated' || phaseStatus == 'changed') - dispatch(PhaseSlice.Fetch()); - }, [phaseStatus]); - - React.useEffect(() => { - if (mtStatus == 'uninitiated' || mtStatus == 'changed') - dispatch(MeasurmentTypeSlice.Fetch()); - }, [mtStatus]); - - React.useEffect(() => { - if (mcStatus == 'uninitiated' || mcStatus == 'changed') - dispatch(MeasurementCharacteristicSlice.Fetch()); - }, [mcStatus]); - React.useEffect(() => { setStatus('loading'); const handle = TrendChannelController.PagedSearch([], sortKey, ascending, page, props.Meter.ID); @@ -219,7 +203,7 @@ const MeterTrendChannelWindow = (props: IProps) => { return true; } - if (assetStatus == 'error' || phaseStatus == 'error' || mcStatus == 'error' || status == 'error') + if (assetStatus == 'error' || phaseStatus == 'error' || mcStatus == 'error' || mtStatus === 'error' || status == 'error') return
    @@ -244,7 +228,7 @@ const MeterTrendChannelWindow = (props: IProps) => {
    - if (assetStatus == 'loading' || phaseStatus == 'loading' || mcStatus == 'loading' || status == 'loading') + if (assetStatus == 'loading' || phaseStatus == 'loading' || mcStatus == 'loading' || mtStatus === "loading" || status == 'loading') return
    diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/NewMeterWizard/ChannelPage.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/NewMeterWizard/ChannelPage.tsx index 5eb1bd966..19d0eb655 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/NewMeterWizard/ChannelPage.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/NewMeterWizard/ChannelPage.tsx @@ -26,13 +26,12 @@ import * as _ from 'lodash'; import { OpenXDA, Application, SystemCenter } from '@gpa-gemstone/application-typings'; import CFGParser from '../../../TS/CFGParser'; import { Input, Select, TextArea } from '@gpa-gemstone/react-forms'; -import { Modal, Warning, ServerErrorIcon, BtnDropdown } from '@gpa-gemstone/react-interactive'; +import { Modal, Warning, ServerErrorIcon, BtnDropdown, GenericController } from '@gpa-gemstone/react-interactive'; import PARParser from '../../../TS/PARParser'; import { ConfigurableTable, ConfigurableColumn, Column } from '@gpa-gemstone/react-table'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; import ChannelScalingForm from '../Meter/ChannelScaling/ChannelScalingForm'; -import { MeasurementCharacteristicSlice, MeasurmentTypeSlice, PhaseSlice } from '../Store/Store'; -import { useAppDispatch, useAppSelector } from '../hooks'; +import { useControllerFetch } from '../hooks'; import TemplateWindow from './TemplateWindow'; import VirtualChannelModal from './VirtualChannelModal'; declare var homePath: string; @@ -47,9 +46,11 @@ interface IProps { TrendChannels: boolean, IsEngineer: boolean, } +const PhaseController = new GenericController(`${homePath}api/OpenXDA/Phase`, 'Name'); +const MeasurementTypeController = new GenericController(`${homePath}api/OpenXDA/MeasurementType`, 'Name'); +const MeasurementCharacteristicController = new GenericController(`${homePath}api/OpenXDA/MeasurementCharacteristic`, 'Name'); export default function ChannelPage(props: IProps) { - const dispatch = useAppDispatch(); const fileInput = React.useRef(null); const [showCFGError, setShowCFGError] = React.useState(false); @@ -64,17 +65,14 @@ export default function ChannelPage(props: IProps) { const [showVirtualChannelModal, setShowVirtualChannelModal] = React.useState(false); const [spareList, setSpareList] = React.useState([]); const [listStatus, setListStatus] = React.useState('idle'); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); const [sortKey, setSortKey] = React.useState('Series'); const [asc, setAsc] = React.useState(true); - const phases = useAppSelector(PhaseSlice.Data); - const measurementCharateristics = useAppSelector(MeasurementCharacteristicSlice.Data); - const measurementTypes = useAppSelector(MeasurmentTypeSlice.Data); - - const pStatus = useAppSelector(PhaseSlice.Status); - const mCStatus = useAppSelector(MeasurementCharacteristicSlice.Status); - const mTStatus = useAppSelector(MeasurmentTypeSlice.Status); + const { Data: phases, Status: pStatus } = useControllerFetch(PhaseController); + const { Data: measurementTypes, Status: mTStatus } = useControllerFetch(MeasurementTypeController); + const { Data: measurementCharateristics, Status: mCStatus } = useControllerFetch(MeasurementCharacteristicController); const baseWarnings: string[] = ["Ensure all Scaling values are correct.", "Ensure all virtual Channels are configured."]; const serverParsedExtensions: string[] = ['pqd', 'sel', 'cev', 'eve', 'ctl', 'txt']; @@ -100,22 +98,7 @@ export default function ChannelPage(props: IProps) { return () => { if (handle != null && handle.abort != null) handle.abort(); } - }, []); - - React.useEffect(() => { - if (mTStatus == 'uninitiated' || mTStatus == 'changed') - dispatch(MeasurmentTypeSlice.Fetch()); - }, [mTStatus]); - - React.useEffect(() => { - if (mCStatus == 'uninitiated' || mCStatus == 'changed') - dispatch(MeasurementCharacteristicSlice.Fetch()); - }, [mCStatus]); - - React.useEffect(() => { - if (pStatus == 'uninitiated' || pStatus == 'changed') - dispatch(PhaseSlice.Fetch()); - }, [pStatus]); + }, [refreshTrigger]); React.useEffect(() => { props.SetWarning(baseWarnings) @@ -134,7 +117,7 @@ export default function ChannelPage(props: IProps) { return () => { $(".custom-file-input").off('change'); } - }, [props.TrendChannels]) + }, [props.TrendChannels, refreshTrigger]) React.useEffect(() => { setSelectedFile(''); @@ -200,9 +183,7 @@ export default function ChannelPage(props: IProps) { const channels = sortChannels(data); handleParsedChannels(channels); // Need to fetch these after since the server parser will add new things to these if it spots them - dispatch(PhaseSlice.SetChanged()); - dispatch(MeasurementCharacteristicSlice.SetChanged()); - dispatch(MeasurmentTypeSlice.SetChanged()); + setRefreshTrigger(val => !val); }).fail(() => { setChannelStatus('error'); }); diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/NewMeterWizard/TemplateWindow.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/NewMeterWizard/TemplateWindow.tsx index a1c63d48f..318c2dde9 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/NewMeterWizard/TemplateWindow.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/NewMeterWizard/TemplateWindow.tsx @@ -23,14 +23,10 @@ import * as React from 'react'; import * as _ from 'lodash'; -import { OpenXDA, Application } from '@gpa-gemstone/application-typings'; -import CFGParser from '../../../TS/CFGParser'; import { CheckBox, Input, ToolTip } from '@gpa-gemstone/react-forms'; import { Modal, LoadingIcon } from '@gpa-gemstone/react-interactive'; -import PARParser from '../../../TS/PARParser'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; -import ChannelScalingForm from '../Meter/ChannelScaling/ChannelScalingForm'; -import { MeasurementCharacteristicSlice, MeasurmentTypeSlice, PhaseSlice, ChannelTemplateSlice } from '../Store/Store'; +import { ChannelTemplateSlice } from '../Store/Store'; import { useAppDispatch, useAppSelector } from '../hooks'; import { Table, Column } from '@gpa-gemstone/react-table'; import { SystemCenter } from '../global'; From 5e5b7089268cfa930a83f077122c4ec53c33325b Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 9 Sep 2026 13:29:24 -0400 Subject: [PATCH 07/11] replace lscvs slice with controller --- .../TSX/SystemCenter/Customer/MDMKeys.tsx | 39 ++++++------------- 1 file changed, 11 insertions(+), 28 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Customer/MDMKeys.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Customer/MDMKeys.tsx index beda696f2..5148e77e7 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Customer/MDMKeys.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Customer/MDMKeys.tsx @@ -21,27 +21,20 @@ // //****************************************************************************************************** -import { Application, OpenXDA, SystemCenter } from "@gpa-gemstone/application-typings"; +import { SystemCenter } from "@gpa-gemstone/application-typings"; import { ReactIcons } from "@gpa-gemstone/gpa-symbols"; import { Input, ToolTip } from "@gpa-gemstone/react-forms"; -import { Modal, Search } from "@gpa-gemstone/react-interactive"; +import { Modal, GenericController, LoadingScreen, ServerErrorIcon } from "@gpa-gemstone/react-interactive"; import { Table, Column } from "@gpa-gemstone/react-table"; import React from "react"; -import { useAppDispatch, useAppSelector } from "../hooks"; -import { LSCVSAccountSlice } from "../Store/Store"; +import { useAppSelector, useControllerFetch } from "../hooks"; import { SelectRoles } from "../Store/UserSettings"; interface IProps {CustomerID: number} -function MDMKeys(props: IProps) { - - const searchStatus: Application.Types.Status = useAppSelector(LSCVSAccountSlice.SearchStatus); - const search: Search.IFilter[] = useAppSelector(LSCVSAccountSlice.SearchFilters); - const status: Application.Types.Status = useAppSelector(LSCVSAccountSlice.Status); - const data: SystemCenter.Types.LSCVSAccount[] = useAppSelector(LSCVSAccountSlice.SearchResults); - const parentID = useAppSelector(LSCVSAccountSlice.ParentID); - const dispatch = useAppDispatch(); +const MDMKeyController = new GenericController(`${homePath}api/LSCVSAccount`, "AccountID", false); +function MDMKeys(props: IProps) { const emptyLSCVS = { ID: 0, AccountID: '', CustomerID: props.CustomerID }; const [ascending, setAscending] = React.useState(false); @@ -49,23 +42,11 @@ function MDMKeys(props: IProps) { const [showAdd, setShowAdd] = React.useState(false); const [sortField, setSortField] = React.useState('AccountID'); const [errors, setErrors] = React.useState([]); - + const [refreshCount, refreshData] = React.useState(0); const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); const roles = useAppSelector(SelectRoles); - React.useEffect(() => { - if (status === 'uninitiated' || status === 'changed' || parentID != props.CustomerID) - dispatch(LSCVSAccountSlice.Fetch(props.CustomerID)); - }, [status, props.CustomerID, parentID]); - - React.useEffect(() => { - if (searchStatus === 'uninitiated' || status === 'changed') - dispatch(LSCVSAccountSlice.DBSearch({ filter: search, sortField, ascending })); - }, [searchStatus, status]); - - React.useEffect(() => { - dispatch(LSCVSAccountSlice.DBSearch({ filter: search, sortField, ascending })); - }, [ascending]); + const { Data: mdmKeys, Status: mdmKeyStatus } = useControllerFetch(MDMKeyController, sortField, ascending, props.CustomerID, refreshCount); React.useEffect(() => { const e: string[] = []; @@ -87,6 +68,8 @@ function MDMKeys(props: IProps) { return ( <> + +
    @@ -99,7 +82,7 @@ function MDMKeys(props: IProps) {
    TableClass="table table-hover" - Data={data} + Data={mdmKeys} SortKey={sortField} Ascending={ascending} OnSort={(d) => { @@ -140,7 +123,7 @@ function MDMKeys(props: IProps) { ShowX={true} CallBack={(conf, isBtn) => { if (conf && errors.length == 0) - dispatch(LSCVSAccountSlice.DBAction({ verb: 'POST', record: newLSCVSAccount })) + MDMKeyController.DBAction('POST', newLSCVSAccount).then(() => refreshData(x => x + 1)); setShowAdd(false); }} DisableConfirm={errors.length > 0} From c8845a2580c5a208d947a29618a217c5bf299aa4 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 9 Sep 2026 13:29:39 -0400 Subject: [PATCH 08/11] replace dbcleanup slice with controller --- .../wwwroot/Scripts/TSX/SystemCenter/DB/DBCleanup.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DB/DBCleanup.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DB/DBCleanup.tsx index 5b3a35fde..3fe96aaef 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DB/DBCleanup.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/DB/DBCleanup.tsx @@ -27,9 +27,9 @@ import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; import { GenericController, Modal, Warning } from '@gpa-gemstone/react-interactive'; import { Application } from '@gpa-gemstone/application-typings'; import { SystemCenter } from '../global'; -import { DBCleanupSlice } from '../Store/Store'; import GenericByPage from '../CommonComponents/GenericByPage'; -import { useAppSelector } from '../hooks'; +import { useControllerFetch } from '../hooks'; + export interface DBCleanup { ID: number; Name: string; @@ -46,9 +46,9 @@ const controllerPath = `${homePath}api/OpenXDA/DBCleanup` const DBCleanupController = new GenericController(controllerPath, "ID", true); const DBCleanup: Application.Types.iByComponent = (props) => { - const allDBCleanup: DBCleanup[] = useAppSelector(DBCleanupSlice.Data); const emptyDBCleanup = { ID: 0, Name: '', SQLCommand: '', Schedule: '' }; + const [editNewDBCleanup, setEditNewDBCleanup] = React.useState(emptyDBCleanup); const [editNew, setEditNew] = React.useState('New'); const [showWarning, setShowWarning] = React.useState(false); @@ -57,6 +57,8 @@ const DBCleanup: Application.Types.iByComponent = (props) => { const [errors, setErrors] = React.useState([]); const [refreshCount, refreshData] = React.useState(0); + const { Data: dbCleanup, Status: dbCleanupStatus } = useControllerFetch(DBCleanupController, undefined, undefined, undefined, refreshCount); + React.useEffect(() => { setHasChanged(false) }, [showModal]); React.useEffect(() => { @@ -65,7 +67,7 @@ const DBCleanup: Application.Types.iByComponent = (props) => { e.push("A Schedule is required."); if (editNewDBCleanup.Name == null || editNewDBCleanup.Name.length === 0) e.push("A Name is required."); - if (editNewDBCleanup.Name != null && editNewDBCleanup.Name.length > 0 && allDBCleanup.findIndex(s => s.Name.toLowerCase() === editNewDBCleanup.Name.toLowerCase() && s.ID !== editNewDBCleanup.ID) > -1) + if (editNewDBCleanup.Name != null && editNewDBCleanup.Name.length > 0 && dbCleanup.findIndex(s => s.Name.toLowerCase() === editNewDBCleanup.Name.toLowerCase() && s.ID !== editNewDBCleanup.ID) > -1) e.push('Name must be unique.') if (editNewDBCleanup.SQLCommand == null || editNewDBCleanup.SQLCommand.length === 0) e.push("A SQL Command is required."); From f2dd6f57938a7b4eac62c6a0b4a927a236f102eb Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 9 Sep 2026 13:30:09 -0400 Subject: [PATCH 09/11] replace user account and security group slices with controller --- .../TSX/SystemCenter/User/User/ByUser.tsx | 136 +++++++++++------- .../TSX/SystemCenter/User/User/Info.tsx | 44 +++++- .../TSX/SystemCenter/User/User/User.tsx | 49 ++++--- .../TSX/SystemCenter/User/User/UserForm.tsx | 28 ++-- .../User/UserGroup/ByUserGroup.tsx | 86 ++++++----- .../SystemCenter/User/UserGroup/GroupForm.tsx | 24 ++-- .../User/UserGroup/GroupUsers.tsx | 60 ++++++-- .../TSX/SystemCenter/User/UserGroup/Info.tsx | 12 +- .../SystemCenter/User/UserGroup/UserGroup.tsx | 41 ++++-- 9 files changed, 314 insertions(+), 166 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx index 863dadbf5..044968201 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/ByUser.tsx @@ -23,13 +23,11 @@ import * as React from 'react'; import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; -import { SearchBar, Search, Modal, ServerErrorIcon, LoadingScreen } from '@gpa-gemstone/react-interactive'; +import { SearchBar, Search, Modal, ServerErrorIcon, LoadingScreen, GenericController } from '@gpa-gemstone/react-interactive'; import { SystemCenter, Application } from '@gpa-gemstone/application-typings'; import * as _ from 'lodash'; import UserForm from './UserForm'; -import { useAppDispatch, useAppSelector, useBoundPaging } from '../../hooks'; import { useNavigate } from "react-router-dom"; -import { ValueListSlice, ValueListGroupSlice, UserAdditionalFieldSlice, UserAccountSlice } from '../../Store/Store'; import { IUserAccount } from '../Types'; import moment from 'moment'; @@ -61,74 +59,111 @@ const newAcct: IUserAccount = { DisplayName: '' } +const UserAccountController = new GenericController(`${homePath}api/SystemCenter/UserAccount`, "DisplayName", true); + const ByUser: Application.Types.iByComponent = (props) => { let navigate = useNavigate(); - const dispatch = useAppDispatch(); - const search = useAppSelector(UserAccountSlice.SearchFilters); + const [search, setSearch] = React.useState[]>([]) - const data = useAppSelector(UserAccountSlice.SearchResults); - const userStatus: Application.Types.Status = useAppSelector(UserAccountSlice.Status); - const searchStatus: Application.Types.Status = useAppSelector(UserAccountSlice.SearchStatus); + const [data, setData] = React.useState([]); + const [searchStatus, setSearchStatus] = React.useState('uninitiated'); - const sortField = useAppSelector(UserAccountSlice.SortField) - const ascending = useAppSelector(UserAccountSlice.Ascending) - const currentPage = useAppSelector(UserAccountSlice.CurrentPage); - const totalPages = useAppSelector(UserAccountSlice.TotalPages); - const totalRecords = useAppSelector(UserAccountSlice.TotalRecords); + const [sortField, setSortField] = React.useState('DisplayName'); + const [ascending, setAscending] = React.useState(true); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); - const adlFields: Application.Types.iAdditionalUserField[] = useAppSelector(UserAdditionalFieldSlice.Fields) - const adlFieldStatus: Application.Types.Status = useAppSelector(UserAdditionalFieldSlice.FieldStatus) + const [adlFields, setAdlFields] = React.useState([]) + const [adlFieldStatus, setAdlFieldStatus] = React.useState('uninitiated'); const [filterableList, setFilterableList] = React.useState[]>(defaultSearchcols); const [showModal, setShowModal] = React.useState(false); const [userError, setUserError] = React.useState([]); - const valueListItems: SystemCenter.Types.ValueListItem[] = useAppSelector(ValueListSlice.Data); - const valueListItemStatus: Application.Types.Status = useAppSelector(ValueListSlice.Status); + const [valueListItems, setValueListItems] = React.useState([]); + const [valueListItemStatus, setValueListItemStatus] = React.useState('uninitiated'); - const valueListGroups: SystemCenter.Types.ValueListGroup[] = useAppSelector(ValueListGroupSlice.Data); - const valueListGroupStatus: Application.Types.Status = useAppSelector(ValueListGroupSlice.Status); + const [valueListGroups, setValueListGroups] = React.useState([]); + const [valueListGroupStatus, setValueListGroupStatus] = React.useState('uninitiated'); const [act, setAct] = React.useState(newAcct) const [pageStatus, setPageStatus] = React.useState('uninitiated'); React.useEffect(() => { - if (userStatus === 'error' || adlFieldStatus === 'error' || valueListItemStatus === 'error' || valueListGroupStatus === 'error') + if (adlFieldStatus === 'error' || valueListItemStatus === 'error' || valueListGroupStatus === 'error') setPageStatus('error') - else if (userStatus === 'loading' || adlFieldStatus === 'loading' || valueListItemStatus === 'loading' || valueListGroupStatus === 'loading') + else if (adlFieldStatus === 'loading' || valueListItemStatus === 'loading' || valueListGroupStatus === 'loading') setPageStatus('loading') else setPageStatus('idle'); - }, [userStatus, adlFieldStatus, valueListItemStatus, valueListGroupStatus]) + }, [adlFieldStatus, valueListItemStatus, valueListGroupStatus]) React.useEffect(() => { - if (searchStatus === 'uninitiated' || searchStatus === 'changed') - dispatch(UserAccountSlice.PagedSearch({ filter: search, sortField: sortField ?? "ID", ascending: ascending, page: currentPage})); - }, [searchStatus, search, sortField, ascending, currentPage]); + setAdlFieldStatus('loading') + const handle = new GenericController(`${homePath}api/SystemCenter/AdditionalUserField`, "FieldName").Fetch(); + handle.done((d) => { + setAdlFields(d) + setAdlFieldStatus('idle') + }).fail(() => { + setAdlFieldStatus('error') + }) + return () => { + if (handle != null && handle.abort != null) + handle.abort() + }; + }, []); React.useEffect(() => { - if (adlFieldStatus === 'uninitiated' || adlFieldStatus === 'changed') - dispatch(UserAdditionalFieldSlice.FetchField()); - }, [adlFieldStatus]); + setValueListItemStatus('loading') + const handle = new GenericController(`${homePath}api/ValueList`, 'SortOrder').Fetch(); + handle.done((d) => { + setValueListItems(d) + setValueListItemStatus('idle') + }).fail(() => { + setValueListItemStatus('error') + }) + + return () => { + if (handle != null && handle.abort != null) handle.abort(); + } + }, []); React.useEffect(() => { - if (valueListItemStatus === 'uninitiated' || valueListItemStatus === 'changed') - dispatch(ValueListSlice.Fetch()); - }, [valueListItemStatus]); + setValueListGroupStatus('loading') + const handle = new GenericController(`${homePath}api/ValueListGroup`, 'Name').Fetch(); + handle.done((d) => { + setValueListGroups(d) + setValueListGroupStatus('idle') + }).fail(() => { + setValueListGroupStatus('error') + }) + + return () => { + if (handle != null && handle.abort != null) handle.abort(); + } + }, []); React.useEffect(() => { - if (valueListGroupStatus === 'uninitiated' || valueListGroupStatus === 'changed') - dispatch(ValueListGroupSlice.Fetch()); - }, [valueListGroupStatus]); - - const setPage = React.useCallback((page) => { - dispatch(UserAccountSlice.PagedSearch({ filter: search, sortField: sortField ?? "ID", ascending: ascending, page: page - 1 })) - }, [sortField, ascending, search]) - - useBoundPaging(currentPage, totalPages, setPage) + setSearchStatus('loading') + const handle = UserAccountController.PagedSearch(search, sortField, ascending, page); + handle.done((d) => { + setData(JSON.parse(d.Data as unknown as string)); + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + setSearchStatus('idle') + }) + handle.fail(() => setSearchStatus('error')) + return () => { if (handle != null && handle.abort != null) handle.abort() } + }, [page, search, sortField, ascending, refreshTrigger]) React.useEffect(() => { function ConvertType(type: string) { @@ -143,10 +178,6 @@ const ByUser: Application.Types.iByComponent = (props) => { setFilterableList(ordered) }, [adlFields]); - const setFilters = React.useCallback((filters: Search.IFilter[]) => { - dispatch(UserAccountSlice.PagedSearch({ sortField: sortField ?? "ID", ascending: ascending, filter: filters, page: currentPage })) - }, [sortField, ascending, currentPage]) - if (pageStatus === 'error') return
    @@ -155,9 +186,9 @@ const ByUser: Application.Types.iByComponent = (props) => { return (
    - CollumnList={filterableList} SetFilter={setFilters} + CollumnList={filterableList} SetFilter={setSearch} Direction={'left'} defaultCollumn={{ label: 'Username', key: 'DisplayName', type: 'string', isPivotField: false }} Width={'50%'} Label={'Search'} - ShowLoading={searchStatus === 'loading'} ResultNote={searchStatus === 'error' ? 'Could not complete Search' : 'Found ' + totalRecords + ' User Account(s)'} + ShowLoading={searchStatus === 'loading'} ResultNote={searchStatus === 'error' ? 'Could not complete Search' : `Displaying User(s) ${totalRecords > 0 ? (recordsPerPage * page + 1) : 0} - ${recordsPerPage * page + data.length} out of ${totalRecords}`} StorageID="UsersFilter" GetEnum={(setOptions, field) => { @@ -192,7 +223,12 @@ const ByUser: Application.Types.iByComponent = (props) => { SortKey={sortField} Ascending={ascending} OnSort={(d) => { - dispatch(UserAccountSlice.Sort({ SortField: d.colField, Ascending: d.ascending })); + if (d.colKey === sortField) + setAscending(a => !a); + else { + setAscending(true); + setSortField(d.colKey as keyof IUserAccount); + } }} OnClick={(d) => navigate(`${homePath}index.cshtml?name=User&UserAccountID=${d.row.ID}`)} TableStyle={{ @@ -259,8 +295,8 @@ const ByUser: Application.Types.iByComponent = (props) => {
    setPage(p - 1)} Total={totalPages} />
    @@ -268,7 +304,7 @@ const ByUser: Application.Types.iByComponent = (props) => { { if (confirm) - dispatch(UserAccountSlice.DBAction({ verb: 'POST', record: act })) + UserAccountController.DBAction('POST', act).then(() => setRefreshTrigger(val => !val)) setAct(newAcct); setShowModal(false); }} diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/Info.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/Info.tsx index a6feffdcd..ab9aadae6 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/Info.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/Info.tsx @@ -23,16 +23,25 @@ import * as React from 'react'; import * as _ from 'lodash'; import UserForm from './UserForm'; +import { Application } from '@gpa-gemstone/application-typings'; import { ToolTip } from '@gpa-gemstone/react-forms'; +import { GenericController } from '@gpa-gemstone/react-interactive' import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; -import { UserAccountSlice } from '../../Store/Store'; -import { useAppDispatch, useAppSelector } from '../../hooks'; import { IUserAccount } from '../Types'; -const UserInfo = (props: { AccountId: string }) => { - const dispatch = useAppDispatch(); +interface IProps { + AccountId: string, + SetRefreshTrigger: React.Dispatch>, + RefreshTrigger: boolean +} + +const UserInfo = (props: IProps) => { + + // the user as represented in the database + const [currentUser, setCurrentUser] = React.useState(null) + const [status, setStatus] = React.useState('uninitiated'); - const currentUser = useAppSelector((state) => UserAccountSlice.Datum(state,props.AccountId)); + // the user as represented in the form const [user, setUser] = React.useState(currentUser); const [warnings, setWarning] = React.useState([]); const [hover, setHover] = React.useState<('None' | 'Clear')>('None'); @@ -69,8 +78,31 @@ const UserInfo = (props: { AccountId: string }) => { React.useEffect(() => { setUser(currentUser) }, [currentUser]) + React.useEffect(() => { + setStatus('loading'); + const handle = $.ajax({ + type: "GET", + url: `${homePath}api/SystemCenter/UserAccount/One/${props.AccountId}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: true, + async: true + }) + handle.done((d) => { + setStatus('idle'); + setCurrentUser(d); + }) + handle.fail(() => { + setStatus('error') + }) + return () => { + if (handle != null && handle.abort != null) + handle.abort() + } + }, [props.AccountId, props.RefreshTrigger]) + function updateUser() { - dispatch(UserAccountSlice.DBAction({ verb: 'PATCH', record: user })) + new GenericController(`${homePath}api/SystemCenter/UserAccount`, "DisplayName", true).DBAction( 'PATCH', user).then(() => props.SetRefreshTrigger(val => !val)) } return ( diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/User.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/User.tsx index dbd48a9fe..7b6750d91 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/User.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/User.tsx @@ -21,15 +21,14 @@ // ****************************************************************************************************** import * as React from 'react'; -import { LoadingScreen, ServerErrorIcon, TabSelector, Warning } from '@gpa-gemstone/react-interactive'; +import { LoadingScreen, ServerErrorIcon, TabSelector, Warning, GenericController } from '@gpa-gemstone/react-interactive'; import { Application } from '@gpa-gemstone/application-typings'; import * as _ from 'lodash'; import UserInfo from './Info'; import UserPermissions from './Permissions'; -import AdditionalField from '../AdditionalUserFieldsWindow' -import { useAppDispatch, useAppSelector } from '../../hooks'; +import AdditionalField from '../AdditionalUserFieldsWindow'; +import { IUserAccount } from '../Types'; import { CheckBox } from '@gpa-gemstone/react-forms'; -import { UserAccountSlice } from '../../Store/Store'; import { useNavigate } from "react-router-dom"; declare type Tab = 'userInfo' | 'permissions' | 'additionalFields' @@ -38,11 +37,11 @@ interface IProps { UserID: string, Tab: Tab } function User(props: IProps) { const navigate = useNavigate(); - const dispatch = useAppDispatch(); - const user = useAppSelector((state) => UserAccountSlice.Datum(state, props.UserID)); - const status: Application.Types.Status = useAppSelector(UserAccountSlice.Status); + const [user, setUser] = React.useState(null); + const [status, setStatus] = React.useState('uninitiated'); const [tab, setTab] = React.useState(getTab()); const [showWarning, setShowWarning] = React.useState(false); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); function getTab(): Tab { if (props.Tab != undefined) return props.Tab; @@ -59,9 +58,27 @@ function User(props: IProps) { }, [tab]); React.useEffect(() => { - if (status === 'uninitiated' || status === 'changed') - dispatch(UserAccountSlice.Fetch()); - }, [status]); + setStatus('loading'); + const handle = $.ajax({ + type: "GET", + url: `${homePath}api/SystemCenter/UserAccount/One/${props.UserID}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: true, + async: true + }) + handle.done((d) => { + setStatus('idle'); + setUser(d); + }) + handle.fail(() => { + setStatus('error') + }) + return () => { + if (handle != null && handle.abort != null) + handle.abort() + } + }, [props.UserID, refreshTrigger]) if (status === 'error') return
    @@ -88,7 +105,7 @@ function User(props: IProps) {
    setTab(t)} Tabs={Tabs} /> - {tab === "userInfo" ? : null} + {tab === "userInfo" ? : null} {tab === "permissions" ? (user == null ? null : ) : null} {tab === "additionalFields" ? ( { - setShowWarning(false); - if (c) { - dispatch(UserAccountSlice.DBAction({ verb: 'DELETE', record: user })); - navigate(`${homePath}index.cshtml?name=Users`); + setShowWarning(false); + if (c) { + new GenericController(`${homePath}api/SystemCenter/UserAccount`, "DisplayName", true).DBAction('DELETE', user).then(() => navigate(`${homePath}index.cshtml?name=Users`)) + }; } - }} /> + } />
    ) } diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/UserForm.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/UserForm.tsx index 1b5c80999..4415fccdd 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/UserForm.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/User/UserForm.tsx @@ -21,13 +21,13 @@ // ****************************************************************************************************** import * as React from 'react'; -import { Input, DatePicker, CheckBox } from '@gpa-gemstone/react-forms' +import { Input, DatePicker, CheckBox } from '@gpa-gemstone/react-forms'; +import { GenericController } from '@gpa-gemstone/react-interactive'; import * as _ from 'lodash'; -import { UserAccountSlice } from '../../Store/Store'; -import { useAppDispatch, useAppSelector } from '../../hooks'; import { IUserAccount } from '../Types'; import * as CryptoJS from 'crypto-js'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; +import { Application } from '@gpa-gemstone/application-typings'; import moment from 'moment'; interface IProps { @@ -38,18 +38,28 @@ interface IProps { } function UserForm(props: IProps) { - const dispatch = useAppDispatch(); - const allUsers = useAppSelector(UserAccountSlice.Data); - const userStatus = useAppSelector(UserAccountSlice.Status); + const [allUsers, setAllUsers] = React.useState([]); + const [userStatus, setUserStatus] = React.useState('uninitiated'); const [errors, setErrors] = React.useState([]); const [valid, setValid] = React.useState<'valid' | 'resolving' | 'invalid' | 'unknown'>("valid") React.useEffect(() => { - if (userStatus === 'uninitiated' || userStatus === 'changed') - dispatch(UserAccountSlice.Fetch()); - }, [userStatus]); + setUserStatus('loading'); + const handle = new GenericController(`${homePath}api/SystemCenter/UserAccount`, "DisplayName", true).Fetch(); + handle.done((d) => { + setAllUsers(d); + setUserStatus('idle'); + }) + handle.fail(() => { + setUserStatus('error'); + }) + return () => { + if (handle != null && handle.abort != null) + handle.abort() + } + }, []); React.useEffect(() => { if (props.SetErrors !== undefined) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/ByUserGroup.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/ByUserGroup.tsx index e87c5a9b6..f88850374 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/ByUserGroup.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/ByUserGroup.tsx @@ -23,14 +23,11 @@ import * as React from 'react'; import { Table, Column, Paging } from '@gpa-gemstone/react-table'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; -import { SearchBar, Search, Modal, ServerErrorIcon, LoadingScreen } from '@gpa-gemstone/react-interactive'; +import { SearchBar, Search, Modal, ServerErrorIcon, LoadingScreen, GenericController } from '@gpa-gemstone/react-interactive'; import { Application } from '@gpa-gemstone/application-typings'; import * as _ from 'lodash'; -import { useAppDispatch, useBoundPaging } from '../../hooks'; import { useNavigate } from "react-router-dom"; -import { SecurityGroupSlice } from '../../Store/Store'; import { ISecurityGroup } from '../Types'; -import { useSelector } from 'react-redux'; import GroupForm from './GroupForm'; const defaultSearchcols: Search.IField[] = [ @@ -41,23 +38,24 @@ const defaultSearchcols: Search.IField[] = [ const emptyGroup: ISecurityGroup = { Name: "", CreatedBy: "", CreatedOn: new Date(), Description: "", DisplayName: "", ID: "00000000-0000-0000-0000-000000000000", Type: "Database", UpdatedOn: new Date() }; +const SecurityGroupController = new GenericController(`${homePath}api/SystemCenter/FullSecurityGroup`, "DisplayName" as keyof ISecurityGroup); const ByUser: Application.Types.iByComponent = (props) => { let navigate = useNavigate(); - const dispatch = useAppDispatch(); - const search = useSelector(SecurityGroupSlice.SearchFilters); - const data = useSelector(SecurityGroupSlice.SearchResults); - const currentPage = useSelector(SecurityGroupSlice.CurrentPage); - const totalPages = useSelector(SecurityGroupSlice.TotalPages); - const totalRecords = useSelector(SecurityGroupSlice.TotalRecords); + const [search, setSearch] = React.useState[]>([]); + const [data, setData] = React.useState([]); - const [status, setStatus] = React.useState('uninitiated'); - const searchStatus = useSelector(SecurityGroupSlice.SearchStatus); + const [searchStatus, setSearchStatus] = React.useState('uninitiated'); - const sortField = useSelector(SecurityGroupSlice.SortField); - const ascending = useSelector(SecurityGroupSlice.Ascending); + const [sortField, setSortField] = React.useState('DisplayName'); + const [ascending, setAscending] = React.useState(true); + const [page, setPage] = React.useState(0); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); const [showModal, setShowModal] = React.useState(false); const [groupError, setGroupError] = React.useState([]); @@ -65,42 +63,35 @@ const ByUser: Application.Types.iByComponent = (props) => { const [pageStatus, setPageStatus] = React.useState('uninitiated'); + // fetch data paged and sorted for the table React.useEffect(() => { - if (status === 'error') - setPageStatus('error') - else if (status === 'loading' ) - setPageStatus('loading') - else - setPageStatus('idle'); - }, [status]) - - React.useEffect(() => { - if (searchStatus == 'uninitiated' || searchStatus == 'changed') - dispatch(SecurityGroupSlice.PagedSearch({ sortField, ascending, filter: search, page: currentPage})) - }, [searchStatus, sortField, ascending, search, currentPage]) - - const setFilters = React.useCallback((filters: Search.IFilter[]) => { - dispatch(SecurityGroupSlice.PagedSearch({ sortField, ascending, filter: filters, page: currentPage})) - }, [sortField, ascending, currentPage]) - - const setPage = React.useCallback((page) => { - dispatch(SecurityGroupSlice.PagedSearch({ filter: search, sortField: sortField ?? "ID", ascending: ascending, page: page - 1 })) - }, [sortField, ascending, search]) + setSearchStatus('loading'); + const handle = SecurityGroupController.PagedSearch(search, sortField, ascending, page); + handle.done((d) => { + setData(JSON.parse(d.Data as unknown as string)) + setTotalPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + setSearchStatus('idle') + }).fail(() => setSearchStatus('error')); + + return () => { if (handle != null && handle.abort != null) handle.abort() } + }, [sortField, ascending, search, page, refreshTrigger]) if (pageStatus === 'error') return
    ; - useBoundPaging(currentPage, totalPages, setPage) - return (
    - CollumnList={defaultSearchcols} SetFilter={setFilters} + CollumnList={defaultSearchcols} SetFilter={setSearch} Direction={'left'} defaultCollumn={{ label: 'Name', key: 'DisplayName', type: 'string', isPivotField: false }} Width={'50%'} Label={'Search'} - ShowLoading={searchStatus === 'loading'} ResultNote={searchStatus === 'error' ? 'Could not complete Search' : 'Found ' + totalRecords + ' User Group(s)'} + ShowLoading={searchStatus === 'loading'} ResultNote={searchStatus === 'error' ? 'Could not complete Search' : `Displaying User Group(s) ${totalRecords > 0 ? (recordsPerPage * page + 1) : 0} - ${recordsPerPage * page + data.length} out of ${totalRecords}`} StorageID="UsersGroupFilter" GetEnum={() => { return () => { } @@ -125,7 +116,12 @@ const ByUser: Application.Types.iByComponent = (props) => { SortKey={sortField} Ascending={ascending} OnSort={(d) => { - dispatch(SecurityGroupSlice.Sort({ SortField: d.colField, Ascending: d.ascending })); + if (d.colKey === sortField) + setAscending(a => !a); + else { + setAscending(true); + setSortField(d.colKey as keyof ISecurityGroup); + } }} OnClick={(d) => navigate(`${homePath}index.cshtml?name=Group&GroupID=${d.row.ID}`)} TableStyle={{ @@ -184,8 +180,8 @@ const ByUser: Application.Types.iByComponent = (props) => {
    setPage(p - 1)} Total={totalPages} />
    @@ -193,10 +189,10 @@ const ByUser: Application.Types.iByComponent = (props) => { { if (confirm) - dispatch(SecurityGroupSlice.DBAction({ - verb: 'POST', - record: { ...newGroup, Name: ((newGroup.Name?.length ?? 0) > 0? newGroup.Name : newGroup.DisplayName) } - })) + SecurityGroupController.DBAction( + 'POST', + { ...newGroup, Name: ((newGroup.Name?.length ?? 0) > 0 ? newGroup.Name : newGroup.DisplayName) } + ).then(() => setRefreshTrigger(val => !val)); setShowModal(false); }} ConfirmShowToolTip={groupError.length > 0} diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/GroupForm.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/GroupForm.tsx index 78f9c0ec1..a179fc476 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/GroupForm.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/GroupForm.tsx @@ -23,10 +23,10 @@ import * as React from 'react'; import { Input, TextArea } from '@gpa-gemstone/react-forms' import * as _ from 'lodash'; -import { SecurityGroupSlice } from '../../Store/Store'; -import { useAppDispatch, useAppSelector } from '../../hooks'; import { ISecurityGroup } from '../Types'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; +import { GenericController } from '@gpa-gemstone/react-interactive'; +import { Application } from '@gpa-gemstone/application-typings'; interface IProps { Group: ISecurityGroup, @@ -36,18 +36,26 @@ interface IProps { } function GroupForm(props: IProps) { - const dispatch = useAppDispatch(); - const allGroups = useAppSelector(SecurityGroupSlice.Data); - const groupStatus = useAppSelector(SecurityGroupSlice.Status); + const [allGroups, setAllGroups] = React.useState([]); + const [groupStatus, setGroupStatus] = React.useState('uninitiated'); const [errors, setErrors] = React.useState([]); const [valid, setValid] = React.useState<'valid' | 'resolving' | 'invalid' | 'unknown'>("valid") React.useEffect(() => { - if (groupStatus === 'uninitiated' || groupStatus === 'changed') - dispatch(SecurityGroupSlice.Fetch()); - }, [groupStatus]); + setGroupStatus('loading') + const handle = new GenericController(`${homePath}api/SystemCenter/FullSecurityGroup`, "DisplayName").Fetch(); + handle.done((d) => { + setAllGroups(d); + setGroupStatus('idle') + }) + handle.fail(() => setGroupStatus('error')) + return () => { + if (handle != null && handle.abort != null) + handle.abort(); + } + }, []) React.useEffect(() => { if (props.SetErrors !== undefined) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/GroupUsers.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/GroupUsers.tsx index 6441e9274..2848123a7 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/GroupUsers.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/GroupUsers.tsx @@ -23,16 +23,18 @@ import * as React from 'react'; import { Application } from '@gpa-gemstone/application-typings'; import * as _ from 'lodash'; -import { LoadingScreen } from '@gpa-gemstone/react-interactive'; -import { UserAccountSliceRemote } from '../../Store/Store'; +import { LoadingScreen, GenericController, SearchBar } from '@gpa-gemstone/react-interactive'; import { ISecurityGroup } from '../Types'; import { Table, Column, Paging } from '@gpa-gemstone/react-table'; -import { DefaultSelects } from '@gpa-gemstone/common-pages'; +import ControllerSelectPopup from '../../CommonComponents/ControllerSelectPopup' + +const RemoteUserAccountController = new GenericController(`${homePath}api/SystemCenter/RemoteUserAccount`, "Name", false); const GroupUser = (props: { Group: ISecurityGroup }) => { const [showSelect, setShowSelect] = React.useState(false); const [users, setUsers] = React.useState([]); + const [groupUsers, setGroupUsers] = React.useState([]); const [asc, setAsc] = React.useState(true); const [sortField, setSortField] = React.useState('AccountName'); @@ -44,7 +46,9 @@ const GroupUser = (props: { Group: ISecurityGroup }) => { const [refreshTrigger, setRefreshTrigger] = React.useState(false); const [status, setStatus] = React.useState('uninitiated'); + const [groupStatus, setGroupStatus] = React.useState('uninitiated'); + // fetch group's users, paged and sorted for the table React.useEffect(() => { if (props.Group.Type != 'Database') return; @@ -71,6 +75,27 @@ const GroupUser = (props: { Group: ISecurityGroup }) => { return () => { if (handle != null && handle.abort != null) handle.abort(); } }, [props.Group.ID, props.Group.Type, asc, sortField, page, refreshTrigger]) + // fetch all of group's users to use as the base 'selected' users in the selection popup. + React.useEffect(() => { + if (props.Group.Type != 'Database') + return; + + setGroupStatus('loading') + + const handle = $.ajax({ + type: "GET", + url: `${homePath}api/SystemCenter/FullSecurityGroup/Users/${props.Group.ID}`, + contentType: "application/json; charset=utf-8", + cache: false, + async: true + }).done((d) => { + setGroupStatus('idle'); + setGroupUsers(JSON.parse(d.Data as unknown as string)); + }).fail(() => setGroupStatus('error')); + + return () => { if (handle != null && handle.abort != null) handle.abort(); } + }, [props.Group.ID, props.Group.Type, refreshTrigger]) + function saveUser(u) { if (props.Group.Type != 'Database') return; @@ -110,7 +135,7 @@ const GroupUser = (props: { Group: ISecurityGroup }) => {
    - +
    {props.Group.Type == 'Azure' ?
    Users in an Azure Group cannot be edited in System Center. To add or remove Users, please contact your Azure Administrator. @@ -204,10 +229,9 @@ const GroupUser = (props: { Group: ISecurityGroup }) => { disabled={props.Group.Type !== 'Database'}>Add Users
    - - + Controller={RemoteUserAccountController} + Selection={groupUsers} OnClose={(selected, conf) => { setShowSelect(false); if (!conf) return; @@ -216,8 +240,21 @@ const GroupUser = (props: { Group: ISecurityGroup }) => { Show={showSelect} Type={'multiple'} Title={"Add Users to " + props.Group.Name} - GetEnum={() => () => { }} - GetAddlFields={() => () => { }} + DisableConfirm={groupStatus != 'idle'} + Searchbar={(children, setFilters) => + SetFilter={setFilters} + CollumnList={[ + { label: 'First Name', key: 'FirstName', type: 'string', isPivotField: false }, + { label: 'Last Name', key: 'LastName', type: 'string', isPivotField: false }, + { label: 'Email', key: 'Email', type: 'string', isPivotField: false } + ]} + Direction={'left'} + defaultCollumn={{ label: 'Username', key: 'Name', type: 'string', isPivotField: false }} + Width={'50%'} + Label={'Search'} + > + {children} + } > Username @@ -229,8 +266,7 @@ const GroupUser = (props: { Group: ISecurityGroup }) => { >Phone Email - - +
    ); diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/Info.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/Info.tsx index 85d0dec6f..2bf0db485 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/Info.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/Info.tsx @@ -22,21 +22,19 @@ import * as React from 'react'; import * as _ from 'lodash'; +import { GenericController } from '@gpa-gemstone/react-interactive'; import { ToolTip } from '@gpa-gemstone/react-forms'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; -import { SecurityGroupSlice } from '../../Store/Store'; -import { useAppDispatch } from '../../hooks'; import { ISecurityGroup } from '../Types'; import GroupForm from './GroupForm'; -const GroupInfo = (props: {Group: ISecurityGroup}) => { - const dispatch = useAppDispatch(); +const GroupInfo = (props: { Group: ISecurityGroup }) => { const [group, setGroup] = React.useState(props.Group); const [warnings, setWarning] = React.useState([]); const [errors, setError] = React.useState([]); const [hover, setHover] = React.useState<('None' | 'Clear' | 'Save')>('None'); - + const [refreshTrigger, setRefreshTrigger] = React.useState(false); React.useEffect(() => { if (group == null || props.Group == null) @@ -49,7 +47,7 @@ const GroupInfo = (props: {Group: ISecurityGroup}) => { w.push('Changes to Name will be lost.') setWarning(w); - }, [group, props.Group]) + }, [group, props.Group, refreshTrigger]) React.useEffect(() => { setGroup(props.Group) }, [props.Group]) @@ -76,7 +74,7 @@ const GroupInfo = (props: {Group: ISecurityGroup}) => {
    diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/UserGroup.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/UserGroup.tsx index 461286c6f..e544c488b 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/UserGroup.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/UserGroup.tsx @@ -21,11 +21,10 @@ // ****************************************************************************************************** import * as React from 'react'; -import { LoadingScreen, ServerErrorIcon, TabSelector, Warning } from '@gpa-gemstone/react-interactive'; +import { LoadingScreen, ServerErrorIcon, TabSelector, Warning, GenericController } from '@gpa-gemstone/react-interactive'; import * as _ from 'lodash'; -import { useAppDispatch, useAppSelector } from '../../hooks'; +import { Application } from '@gpa-gemstone/application-typings'; import { useNavigate } from "react-router-dom"; -import { SecurityGroupSlice } from '../../Store/Store'; import { ISecurityGroup } from '../Types'; import GroupInfo from './Info'; import GroupUser from './GroupUsers'; @@ -37,9 +36,8 @@ interface IProps { GroupID: string, Tab: Tab } function UserGroup(props: IProps) { const navigate = useNavigate(); - const dispatch = useAppDispatch(); - const group: ISecurityGroup = useAppSelector((state) => SecurityGroupSlice.Datum(state, props.GroupID) as ISecurityGroup); - const status = useAppSelector(SecurityGroupSlice.Status); + const [group, setGroup] = React.useState(null) + const [status, setStatus] = React.useState('uninitiated'); const [tab, setTab] = React.useState(getTab()); const [showWarning, setShowWarning] = React.useState(false); @@ -51,17 +49,35 @@ function UserGroup(props: IProps) { return 'info'; } + React.useEffect(() => { + setStatus('loading'); + const handle = $.ajax({ + type: "GET", + url: `${homePath}api/SystemCenter/FullSecurityGroup/One/${props.GroupID}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: true, + async: true + }) + handle.done((d) => { + setStatus('idle'); + setGroup(d); + }) + handle.fail(() => { + setStatus('error') + }) + return () => { + if (handle != null && handle.abort != null) + handle.abort() + } + }, [props.GroupID]) + React.useEffect(() => { const saved = getTab(); if (saved !== tab) sessionStorage.setItem('UserGroup.Tab', JSON.stringify(tab)); }, [tab]); - React.useEffect(() => { - if (status == 'uninitiated' || status == 'changed') - dispatch(SecurityGroupSlice.Fetch()); - }, [status]) - if (status === 'error') return
    @@ -94,8 +110,7 @@ function UserGroup(props: IProps) { { setShowWarning(false); if (c) { - dispatch(SecurityGroupSlice.DBAction({ verb: 'DELETE', record: group })); - navigate(`${homePath}index.cshtml?name=Groups`); + new GenericController(`${homePath}api/SystemCenter/FullSecurityGroup`, "DisplayName").DBAction('DELETE', group).then(() => navigate(`${homePath}index.cshtml?name=Groups`)); } }} />
    From f6cc0ea052ddc899c737c9b9faa268162e18ea2d Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 9 Sep 2026 13:30:46 -0400 Subject: [PATCH 10/11] remove unused slices from store --- .../Scripts/TSX/SystemCenter/Store/Store.ts | 47 +------------------ 1 file changed, 1 insertion(+), 46 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/Store.ts b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/Store.ts index 0a797447e..3bc909fdd 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/Store.ts +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/Store.ts @@ -35,7 +35,7 @@ import { DBCleanup } from '../DB/DBCleanup'; import { ApplicationCategory } from '../ApplicationCategory/ByApplicationCategory'; import { OpenXDA as LocalXDA, SystemCenter as LocalSystemCenter } from '../global' import PQISlice from './PQISlice'; -import { IApplicationRole, ISecurityGroup, IUserAccount } from '../User/Types'; +import { IApplicationRole } from '../User/Types'; import UserSettingsReducer from './UserSettings'; import { EventWidget } from '../../../../../EventWidgets/TSX/global'; import { IAPIAccessKey } from '../APIAccessKeys/APIAccessKeys' @@ -54,41 +54,24 @@ export const ValueListSlice = new GenericSlice export const LocationDrawingSlice = new GenericSlice('LocationDrawing', `${homePath}api/LocationDrawing`, 'Name'); export const ChannelGroupSlice = new GenericSlice('ChannelGroup', `${homePath}api/ChannelGroup`, 'Name'); -export const ChannelGroupViewSlice = new GenericSlice('ChannelGroupView', `${homePath}api/ChannelGroup`, 'Name'); export const ChannelGroupDetailsSlice = new GenericSlice('ChannelGroupDetails', `${homePath}api/ChannelGroupDetails`, 'DisplayName'); export const SystemCenterSettingSlice = new GenericSlice('SystemCenterSetting', `${homePath}api/Setting`, 'Name'); -export const OpenXDASettingSlice = new GenericSlice('OpenXDASetting', `${homePath}api/OpenXDA/Setting`, 'Name'); -export const MiMDSettingSlice = new GenericSlice('MiMDSetting', `${homePath}api/MiMD/Setting`, 'Name'); -export const OpenSEESettingSlice = new GenericSlice('OpenSEESetting', `${homePath}api/OpenSEE/Setting`, 'Name'); -export const SEBrowserSettingSlice = new GenericSlice('SEBrowserSetting', `${homePath}api/SEBrowser/Setting`, 'Name'); -export const PQDigestSettingSlice = new GenericSlice('PQDigestSetting', `${homePath}api/PQDigest/Setting`, 'Name'); - export const AssetConnectionTypeSlice = new GenericSlice("AssetConnectionType", `${homePath}api/OpenXDA/AssetConnectionType`, 'Name'); -export const AssetTypeSlice = new GenericSlice("AssetType", `${homePath}api/OpenXDA/AssetType`, 'Name'); -export const PhaseSlice = new GenericSlice("Phase", `${homePath}api/OpenXDA/Phase`, 'Name'); -export const MeasurmentTypeSlice = new GenericSlice("MeasurementType", `${homePath}api/OpenXDA/MeasurementType`, 'Name'); export const MeasurementCharacteristicSlice = new GenericSlice("MeasurementCharacteristic", `${homePath}api/OpenXDA/MeasurementCharacteristic`, 'Name'); export const EventTypeAssetTypeSlice = new GenericSlice("EventTypeAssetType", `${homePath}api/OpenXDA/EventTypeAssetType`, "ID", false); export const CustomerSlice = new GenericSlice("Customer", `${homePath}api/SystemCenter/Customer`, "Name", true); -export const CustomerMeterSlice = new GenericSlice('CustomerMeter', `${homePath}api/SystemCenter/CustomerMeter`, 'MeterName', true); -export const CustomerAssetSlice = new GenericSlice('CustomerAsset', `${homePath}api/SystemCenter/CustomerAsset`, 'AssetName', true); - export const EventTypeSlice = new GenericSlice("EventType", `${homePath}api/OpenXDA/EventType`, "Name"); export const LocationSlice = new GenericSlice("Location", `${homePath}api/OpenXDA/Location`, "LocationKey", true); -export const DataOperationSlice = new GenericSlice("DataOperation", `${homePath}api/OpenXDA/DataOperation`, "LoadOrder"); -export const DataReaderSlice = new GenericSlice("DataReader", `${homePath}api/OpenXDA/DataReader`, "LoadOrder"); export const ExternalDatabasesSlice = new GenericSlice("ExternalDatabases", `${homePath}api/SystemCenter/ExternalDatabases`, "Name", false); export const ExternalDBTablesSlice = new GenericSlice("ExternalDataBaseTable", `${homePath}api/SystemCenter/extDBTables`, "TableName", true); export const AdditionalFieldsSlice = new GenericSlice("AdditionalFields", `${homePath}api/SystemCenter/AdditionalFieldView`, "FieldName", true); export const ExternalXDAFieldsSlice = new GenericSlice("ExternalOpenXDAField", `${homePath}api/SystemCenter/ExternalOpenXDAField`, "FieldName", true); export const ApplicationNodeSlice = new GenericSlice("ApplicationNode", `${homePath}api/OpenXDA/ApplicationNode`, "Name", true); -export const LSCVSAccountSlice = new GenericSlice("LSCVSAccount", `${homePath}api/LSCVSAccount`, "AccountID", false); -export const DBCleanupSlice = new GenericSlice("DBCleanup", `${homePath}api/OpenXDA/DBCleanup`, "ID", true); export const ApplicationCategorySlice = new GenericSlice("PQApplicationCategory", `${homePath}api/OpenXDA/ApplicationCategory`, "Name", true); export const PQApplicationsSlice = new GenericSlice("PQApplications", `${homePath}api/OpenXDA/PQApplications`, "SortOrder", true); @@ -100,18 +83,14 @@ export const ByMeterSlice = new GenericSlice(" export const RemoteXDAInstanceSlice = new GenericSlice("remoteXDAInstance", `${homePath}api/OpenXDA/remoteXDAInstance`, "Name", false); export const RemoteXDAMeterSlice = new GenericSlice("RemoteXDAMeter", `${homePath}api/OpenXDA/RemoteXDAMeter`, "LocalMeterName", false); export const RemoteXDAAssetSlice = new GenericSlice("RemoteXDAAsset", `${homePath}api/OpenXDA/RemoteXDAAsset`, "LocalAssetName", false); -export const UserAccountSliceRemote = new GenericSlice("UserAccountRemote", `${homePath}api/SystemCenter/RemoteUserAccount`, "Name", false); export const AssetNoteSlice = new NoteSlice('Asset'); export const MeterNoteSlice = new NoteSlice('Meter'); export const UserNoteSlice = new NoteSlice('User'); export const LocationNoteSlice = new NoteSlice('Location'); export const CustomerNoteSlice = new NoteSlice('Customer'); -export const UserAccountSlice = new GenericSlice('UserAccounts', `${homePath}api/SystemCenter/UserAccount`, "DisplayName", true); export const UserAdditionalFieldSlice = new AdditionalUserFieldSlice('AdditionalUserFields', `${homePath}api/SystemCenter`); -export const SourceImpedanceSlice = new GenericSlice("SourceImpedance", `${homePath}api/OpenXDA/SourceImpedance`, "AssetLocationID", false); -export const SecurityGroupSlice = new GenericSlice("SecurityGroup", `${homePath}api/SystemCenter/FullSecurityGroup`, "DisplayName", true) export const ApplicationRoleSlice = new GenericSlice("ApplicationRole", `${homePath}api/SystemCenter/ApplicationRole`, "Name", false) export const WidgetCategorySlice = new GenericSlice("WidgetCategory", `${homePath}api/SystemCenter/WidgetCategory`, "OrderBy", true) @@ -121,7 +100,6 @@ export const PQDigestHomeScreenSlice = new GenericSlice("SEBrowserWidgetView", `${homePath}api/SEbrowser/WidgetView`, "Name", true) -export const MagDurCurveSlice = new GenericSlice('MagDurCurve', `${homePath}api/SystemCenter/StandardMagDurCurve`, 'Name'); export const APIAccessKeySlice = new GenericSlice('APIAccessKey', `${homePath}api/OpenXDA/APIAccessKey`, 'RegistrationKey'); @@ -132,8 +110,6 @@ export const MATLABAnalyticSlice = new GenericSlice("MATLABAnalyticEventType", `${homePath}api/OpenXDA/MATLABAnalyticEventType`, 'ID'); export const MATLABAnalyticAssetTypeSlice = new GenericSlice("MATLABAnalyticAssetType", `${homePath}api/OpenXDA/MATLABAnalyticAssetType`, 'ID'); -export const TrendChannelSlice = new GenericSlice('TrendChannels', `${homePath}api/OpenXDA/TrendChannel`, 'Name'); - export const ChannelTemplateSlice = new GenericSlice('ChannelTemplate', `${homePath}api/SystemCenter/ChannelTemplateFile`, 'ID', true); export const ConfigSlice = new ConfigurationSlice("Config"); @@ -141,14 +117,11 @@ export const ConfigSlice = new ConfigurationSlice("Config"); const store = configureStore({ reducer: { UserSettings: UserSettingsReducer, - LSCVSAccount: LSCVSAccountSlice.Reducer, EventTypeAssetType: EventTypeAssetTypeSlice.Reducer, AssetGroup: AssetGroupSlice.Reducer, ByAsset: ByAssetSlice.Reducer, ByMeter: ByMeterSlice.Reducer, ByLocation: ByLocationSlice.Reducer, - MeasurementType: MeasurmentTypeSlice.Reducer, - Phase: PhaseSlice.Reducer, AssetConnectionType: AssetConnectionTypeSlice.Reducer, Meter: MeterReducer, Asset: AssetReducer, @@ -159,46 +132,30 @@ const store = configureStore({ ChannelGroupDetails: ChannelGroupDetailsSlice.Reducer, LocationDrawing: LocationDrawingSlice.Reducer, SystemCenterSetting: SystemCenterSettingSlice.Reducer, - OpenXDASetting: OpenXDASettingSlice.Reducer, - MiMDSetting: MiMDSettingSlice.Reducer, - OpenSEESetting: OpenSEESettingSlice.Reducer, - SEBrowserSetting: SEBrowserSettingSlice.Reducer, - PQDigestSetting: PQDigestSettingSlice.Reducer, - AssetType: AssetTypeSlice.Reducer, Customer: CustomerSlice.Reducer, AssetNote: AssetNoteSlice.Reducer, MeterNote: MeterNoteSlice.Reducer, UserNote: UserNoteSlice.Reducer, LocationNote: LocationNoteSlice.Reducer, CustomerNote: CustomerNoteSlice.Reducer, - UserAccounts: UserAccountSlice.Reducer, AdditionalUserFields: UserAdditionalFieldSlice.Reducer, EventType: EventTypeSlice.Reducer, MeasurementCharacteristic: MeasurementCharacteristicSlice.Reducer, - DataOperation: DataOperationSlice.Reducer, - DataReader: DataReaderSlice.Reducer, ExternalDataBaseTable: ExternalDBTablesSlice.Reducer, remoteXDAInstance: RemoteXDAInstanceSlice.Reducer, RemoteXDAAsset: RemoteXDAAssetSlice.Reducer, RemoteXDAMeter: RemoteXDAMeterSlice.Reducer, - UserAccountRemote: UserAccountSliceRemote.Reducer, ApplicationNode: ApplicationNodeSlice.Reducer, PQApplicationCategory: ApplicationCategorySlice.Reducer, PQApplications: PQApplicationsSlice.Reducer, - DBCleanup: DBCleanupSlice.Reducer, EventChannels: EventChannelSlice.reducer, - CustomerMeter: CustomerMeterSlice.Reducer, - CustomerAsset: CustomerAssetSlice.Reducer, PQI: PQISlice, - SourceImpedance: SourceImpedanceSlice.Reducer, - SecurityGroup: SecurityGroupSlice.Reducer, WidgetCategory: WidgetCategorySlice.Reducer, ApplicationRole: ApplicationRoleSlice.Reducer, SEBrowserWidget: SEBrowserWidgetSlice.Reducer, PQDigestWidget: PQDigestWidgetSlice.Reducer, PQDigestHomeScreenWidget: PQDigestHomeScreenSlice.Reducer, SEBrowserWidgetView: SEBrowserWidgetViewSlice.Reducer, - MagDurCurve: MagDurCurveSlice.Reducer, APIAccessKey: APIAccessKeySlice.Reducer, ExternalDatabases: ExternalDatabasesSlice.Reducer, AdditionalFields: AdditionalFieldsSlice.Reducer, @@ -207,9 +164,7 @@ const store = configureStore({ MATLABAnalytic: MATLABAnalyticSlice.Reducer, MATLABAnalyticEventType: MATLABAnalyticEventTypeSlice.Reducer, MATLABAnalyticAssetType: MATLABAnalyticAssetTypeSlice.Reducer, - TrendChannels: TrendChannelSlice.Reducer, ChannelTemplate: ChannelTemplateSlice.Reducer, - ChannelGroupView: ChannelGroupViewSlice.Reducer, ValueListGroupView: ValueListGroupViewSlice.Reducer, Config: ConfigSlice.Reducer } From e59dc013f71770b5ce6d7e5de57311d7eb459d4a Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Wed, 9 Sep 2026 15:07:08 -0400 Subject: [PATCH 11/11] update pointers --- Source/Applications/SystemCenter/EventWidgets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Applications/SystemCenter/EventWidgets b/Source/Applications/SystemCenter/EventWidgets index c6484bbdc..abe4bbb53 160000 --- a/Source/Applications/SystemCenter/EventWidgets +++ b/Source/Applications/SystemCenter/EventWidgets @@ -1 +1 @@ -Subproject commit c6484bbdcbb6cdae0b434407593b46cecbeb5015 +Subproject commit abe4bbb5332b32f291c368878418feff526d05a8