From 9a493b35b4a3c55f35f7bab5d42f4f4e8dab81a1 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Fri, 21 Aug 2026 12:48:41 -0400 Subject: [PATCH 1/5] use controller in meter components --- .../TSX/SystemCenter/Meter/ByMeter.tsx | 178 ++++++++++-------- .../TSX/SystemCenter/Meter/MeterAsset.tsx | 68 +++++-- .../SystemCenter/Meter/MeterEventChannel.tsx | 48 +++-- .../TSX/SystemCenter/Meter/MeterLocation.tsx | 55 +++--- .../SystemCenter/Meter/MeterTrendChannel.tsx | 61 ++++-- 5 files changed, 261 insertions(+), 149 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/ByMeter.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/ByMeter.tsx index 7383d5de71..51f63d88cc 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/ByMeter.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/ByMeter.tsx @@ -26,10 +26,7 @@ import { Table, Column } from '@gpa-gemstone/react-table'; import * as _ from 'lodash'; import { Application, SystemCenter } from '@gpa-gemstone/application-typings'; import ExternalDBUpdate from '../CommonComponents/ExternalDBUpdate'; -import { Search, Modal, LoadingScreen } from '@gpa-gemstone/react-interactive'; -import { DefaultSearch } from '@gpa-gemstone/common-pages'; -import { ByMeterSlice } from '../Store/Store'; -import { useAppDispatch, useAppSelector } from '../hooks'; +import { Search, Modal, LoadingScreen, GenericController, SearchBar } from '@gpa-gemstone/react-interactive'; import { useNavigate } from "react-router-dom"; import { Paging } from '@gpa-gemstone/react-table'; @@ -37,95 +34,73 @@ declare var homePath: string; const ByMeter: Application.Types.iByComponent = (props) => { let navigate = useNavigate(); - const dispatch = useAppDispatch(); - - const data = useAppSelector(ByMeterSlice.SearchResults); + const [filters, setFilters] = React.useState[]>([]); + const [data, setData] = React.useState([]); const [ascending, setAscending] = React.useState(true); const [sortKey, setSortKey] = React.useState("Name"); - const cState = useAppSelector(ByMeterSlice.PagedStatus); - const allPages = useAppSelector(ByMeterSlice.TotalPages); - const currentPage = useAppSelector(ByMeterSlice.CurrentPage); - const [page, setPage] = React.useState(currentPage); + const [status, setStatus] = React.useState('uninitiated'); + const [allPages, setAllPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + const [addlFields, setAddlFields] = React.useState[]>([]) + const [page, setPage] = React.useState(0); const [showEXTModal, setShowExtModal] = React.useState(false); - const extDbUpdateAll = React.useRef<() => (() => void)>(undefined); - - React.useEffect(() => { - dispatch(ByMeterSlice.PagedSearch({ sortField: sortKey, ascending, page })); - }, [sortKey, ascending, page]); React.useEffect(() => { - if (cState === 'uninitiated' || cState === 'changed') - dispatch(ByMeterSlice.PagedSearch({ sortField: sortKey, ascending, page })); - }, [cState]); + setStatus('loading'); + const handle = new GenericController(`${homePath}api/OpenXDA/ByMeter`, "Name", true).PagedSearch(filters, sortKey, ascending, page); + handle.done((d) => { + setStatus('idle'); + setData(JSON.parse(d.Data as unknown as string)); + setAllPages(d.NumberOfPages); + setTotalRecords(d.TotalRecords); + setRecordsPerPage(d.RecordsPerPage); + if (page >= d.NumberOfPages) + setPage(Math.max(d.NumberOfPages - 1, 0)); + }) + handle.fail(() => setStatus('error')) + return () => { if (handle != null && handle.abort != null) handle.abort() } + }, [filters, sortKey, ascending, page]) function handleSelect(item) { navigate(`${homePath}index.cshtml?name=Meter&MeterID=${item.row.ID}`); } + function goNewMeterWizard() { navigate(`${homePath}index.cshtml?name=NewMeterWizard`); } - function getAdditionalFields(setFields) { - - let handle = $.ajax({ - type: "GET", - url: `${homePath}api/SystemCenter/AdditionalFieldView/ParentTable/Meter/FieldName/0`, - contentType: "application/json; charset=utf-8", - cache: false, - async: true - }); - - function ConvertType(type: string) { - if (type == 'string' || type == 'integer' || type == 'number' || type == 'datetime' || type == 'boolean') - return { type: type } - return { - type: 'enum', enum: [{ Label: type, Value: type }] - } - } - - handle.done((d: Array) => { - let ordered = _.orderBy(d.filter(item => item.Searchable).map(item => ( - { label: `[AF${item.ExternalDB != undefined ? " " + item.ExternalDB : ''}] ${item.FieldName}`, key: item.FieldName, ...ConvertType(item.Type), isPivotField: true } as Search.IField - )), ['label'], ["asc"]); - setFields(ordered) - }); - - return () => { - if (handle != null && handle.abort == null) { - handle.abort() - }; - }; - } - - function getEnum(setOptions, field) { - let handle = null; - if (field.type != 'enum' || field.enum == undefined || field.enum.length != 1) - return () => { }; - - handle = $.ajax({ - type: "GET", - url: `${homePath}api/ValueList/Group/${field.enum[0].Value}`, - contentType: "application/json; charset=utf-8", - dataType: 'json', - cache: true, - async: true - }); - - handle.done(d => setOptions(d.map(item => ({ Value: item.ID, Label: item.Value })))) - return () => { - if (handle != null && handle.abort == null) - handle.abort(); - } - } + React.useEffect(() => { + return getAdditionalFields(setAddlFields); + }, []) return (
- +
- + + CollumnList={[ + { label: 'Name', key: 'Name', type: 'string', isPivotField: false }, + { label: 'Key', key: 'LocationKey', type: 'string', isPivotField: false }, + { label: 'Asset Key', key: 'Asset', type: 'string', isPivotField: false }, + { label: 'Meter Key', key: 'Meter', type: 'string', isPivotField: false }, + { label: 'Number of Assets', key: 'Assets', type: 'integer', isPivotField: false }, + { label: 'Number of Meters', key: 'Meters', type: 'integer', isPivotField: false }, + { label: 'Description', key: 'Description', type: 'string', isPivotField: false }, + ...addlFields]} + SetFilter={setFilters} + Direction={'left'} + defaultCollumn={{ label: 'Name', key: 'Name', type: 'string', isPivotField: false }} + Width={'50%'} + Label={'Search'} + ShowLoading={status === 'loading'} + ResultNote={status === 'error' ? 'Could not complete Search' : `Displaying Meter(s) ${totalRecords > 0 ? (recordsPerPage * page + 1) : 0} - ${recordsPerPage * page + data.length} out of ${totalRecords}`} + GetEnum={getEnum} + StorageID={"MetersFilter"} + >
  • Wizards: @@ -143,7 +118,7 @@ const ByMeter: Application.Types.iByComponent = (props) => {
  • -
    +
    @@ -232,4 +207,57 @@ const ByMeter: Application.Types.iByComponent = (props) => { ) } -export default ByMeter; \ No newline at end of file +export default ByMeter; + +function getAdditionalFields(setFields) { + + let handle = $.ajax({ + type: "GET", + url: `${homePath}api/SystemCenter/AdditionalFieldView/ParentTable/Meter/FieldName/0`, + contentType: "application/json; charset=utf-8", + cache: false, + async: true + }); + + function ConvertType(type: string) { + if (type == 'string' || type == 'integer' || type == 'number' || type == 'datetime' || type == 'boolean') + return { type: type } + return { + type: 'enum', enum: [{ Label: type, Value: type }] + } + } + + handle.done((d: Array) => { + let ordered = _.orderBy(d.filter(item => item.Searchable).map(item => ( + { label: `[AF${item.ExternalDB != undefined ? " " + item.ExternalDB : ''}] ${item.FieldName}`, key: item.FieldName, ...ConvertType(item.Type), isPivotField: true } as Search.IField + )), ['label'], ["asc"]); + setFields(ordered) + }); + + return () => { + if (handle != null && handle.abort == null) { + handle.abort() + }; + }; +} + +function getEnum(setOptions, field) { + let handle = null; + if (field.type != 'enum' || field.enum == undefined || field.enum.length != 1) + return () => { }; + + handle = $.ajax({ + type: "GET", + url: `${homePath}api/ValueList/Group/${field.enum[0].Value}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: true, + async: true + }); + + handle.done(d => setOptions(d.map(item => ({ Value: item.ID, Label: item.Value })))) + return () => { + if (handle != null && handle.abort == null) + handle.abort(); + } +} diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterAsset.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterAsset.tsx index 60c0cd0f73..435a493ea5 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterAsset.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterAsset.tsx @@ -33,13 +33,12 @@ import TransformerAttributes from '../AssetAttribute/Transformer'; import { useNavigate } from 'react-router-dom'; import { AssetAttributes } from '../AssetAttribute/Asset'; import { getAssetTypes, getAssetWithAdditionalFields } from '../../../TS/Services/Asset'; -import { DBActionAsset, DBMeterAction, SelectAssetStatus } from '../Store/AssetSlice' import { ConfigurableTable, ConfigurableColumn, Column, Paging } from '@gpa-gemstone/react-table'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; import { Warning, Modal, LoadingScreen, GenericController, ServerErrorIcon } from '@gpa-gemstone/react-interactive'; import { ToolTip } from '@gpa-gemstone/react-forms'; import DERAttributes from '../AssetAttribute/DER'; -import { useAppDispatch, useAppSelector } from '../hooks'; +import { useAppSelector } from '../hooks'; import AssetSelect from '../Asset/AssetSelect'; import { SelectRoles } from '../Store/UserSettings'; import GenerationAttributes from '../AssetAttribute/Generation'; @@ -53,7 +52,7 @@ interface IProps { Meter: OpenXDA.Types.Meter } const MeterAssetWindow = (props: IProps) => { const navigate = useNavigate(); - const MeterAssetController = new GenericController(`${homePath}api/OpenXDA/DetailedMeterAsset/${props.Meter.ID}`, "AssetName", true); + const MeterAssetController = React.useMemo(() => new GenericController(`${homePath}api/OpenXDA/DetailedMeterAsset/${props.Meter.ID}`, "AssetName", true), [props.Meter.ID]); const PagingID = 'MeterAssetPage' const [assetTypes, setAssetTypes] = React.useState([]); @@ -65,12 +64,10 @@ const MeterAssetWindow = (props: IProps) => { const [showSelect, setShowSelect] = React.useState(false); - // Asset Slice Consts const [data, setData] = React.useState([]); - const dispatch = useAppDispatch(); - const status = useAppSelector(SelectAssetStatus) as Application.Types.Status; const [sortKey, setSortKey] = React.useState('AssetName'); const [ascending, setAscending] = React.useState(true); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); const [hover, setHover] = React.useState<('submit' | 'clear' | 'none')>('none'); const roles = useAppSelector(SelectRoles); @@ -113,7 +110,7 @@ const MeterAssetWindow = (props: IProps) => { setPageState('idle'); }).fail(() => setPageState('error')); return () => { if (handle != null && handle?.abort != null) handle.abort(); } - }, [sortKey, ascending, page, status]) + }, [sortKey, ascending, page, refreshTrigger]) function setActiveAsset(assetID: number, assetType: OpenXDA.Types.AssetTypeName) { if (assetID == 0) { @@ -145,6 +142,15 @@ const MeterAssetWindow = (props: IProps) => {

    Assets:

    +
    +
    +

    + {pageState === 'error' ? 'Could not complete Search' : + pageState === 'loading' ? 'Loading...' : + `Displaying Asset(s) ${pageInfo.TotalRecords > 0 ? (pageInfo.RecordsPerPage * page + 1) : 0} - ${pageInfo.RecordsPerPage * page + data.length} out of ${pageInfo.TotalRecords}`} +

    +
    +
    @@ -258,7 +264,10 @@ const MeterAssetWindow = (props: IProps) => { >

    - { if (confirmed) dispatch(DBMeterAction({ verb: 'DELETE', assetID: activeAsset.ID, meterID: props.Meter.ID, locationID: props.Meter.LocationID })); setShowDeleteWarning(false); }} Title={'Remove ' + (activeAsset?.AssetName ?? 'Asset') + ' from ' + (props.Meter?.Name ?? 'Meter')} Message={'This will permanently remove the Asset from this Meter.'} /> + { if (confirmed) removeAsset(activeAsset.ID, props.Meter.ID, props.Meter.LocationID).then(() => setRefreshTrigger(val => !val)); setShowDeleteWarning(false); }} + Title={'Remove ' + (activeAsset?.AssetName ?? 'Asset') + ' from ' + (props.Meter?.Name ?? 'Meter')} + Message={'This will permanently remove the Asset from this Meter.'} />
    @@ -275,7 +284,7 @@ const MeterAssetWindow = (props: IProps) => { CallBack={(confirm) => { setShoweditNew(false); if (confirm) { - dispatch(DBActionAsset({ verb: 'POST', record: activeAsset, meterID: props.Meter.ID, locationID: props.Meter.LocationID })); + addNewAsset(activeAsset, props.Meter.ID, props.Meter.LocationID).then(() => setRefreshTrigger(val => !val)); } }} ConfirmShowToolTip={AssetAttributes.AttributeError(activeAsset).length > 0} @@ -324,7 +333,7 @@ const MeterAssetWindow = (props: IProps) => { OnCloseFunction={(selected, confirm) => { setShowSelect(false); if (confirm) - dispatch(DBMeterAction({ verb: 'POST', assetID: selected[0].ID, meterID: props.Meter.ID, locationID: props.Meter.LocationID })); + addExistingAsset(selected[0].ID, props.Meter.ID, props.Meter.LocationID).then(() => setRefreshTrigger(val => !val)); }} /> @@ -355,4 +364,41 @@ const MeterAssetWindow = (props: IProps) => { } } -export default MeterAssetWindow; \ No newline at end of file +export default MeterAssetWindow; + +function addExistingAsset(assetID: number, meterID: number, locationID: number) { + return $.ajax({ + type: "POST", + url: `${homePath}api/OpenXDA/Meter/${meterID}/Asset/${assetID}/${locationID}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + data: JSON.stringify(''), + cache: false, + async: true + }); +} + +function addNewAsset(asset: OpenXDA.Types.Asset, meterID: number, locationID: number) { + const route = `${homePath}api/OpenXDA/Asset/New/Meter/${meterID}/${locationID}`; + return $.ajax({ + type: "POST", + url: route, + contentType: "application/json; charset=utf-8", + dataType: 'json', + data: JSON.stringify({ Asset: asset }), + cache: false, + async: true + }) +} + +function removeAsset(assetID: number, meterID: number, locationID: number) { + return $.ajax({ + type: "DELETE", + url: `${homePath}api/OpenXDA/Meter/${meterID}/Asset/${assetID}/${locationID}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + data: JSON.stringify(''), + cache: false, + async: true + }); +} \ No newline at end of file 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 20408154fb..ce0d7967a1 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterEventChannel.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterEventChannel.tsx @@ -23,9 +23,8 @@ 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 { Application, OpenXDA as GemstoneOpenXDA} from '@gpa-gemstone/application-typings'; +import { useAppSelector } 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'; @@ -44,7 +43,6 @@ type RecordChange = Map const EventChannelController = new GenericController(`${homePath}api/OpenXDA/EventChannel`, "Name"); const MeterEventChannelWindow = (props: IProps) => { - const dispatch = useAppDispatch(); const [data, setData] = React.useState([]); const [sortKey, setSortKey] = React.useState('Name'); @@ -57,12 +55,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 [phases, setPhases] = React.useState([]); + const [measurementTypes, setMeasurementTypes] = React.useState([]); const [assets, setAssets] = React.useState([]); - const pStatus = useAppSelector(PhaseSlice.Status) as Application.Types.Status; - const mtStatus = useAppSelector(MeasurmentTypeSlice.Status) as Application.Types.Status; + const [pStatus, setPStatus] = React.useState('uninitiated'); + const [mtStatus, setMtStatus] = React.useState('uninitiated'); const [assetStatus, setAssetStatus] = React.useState('idle') const [removeRecord, setRemoveRecord] = React.useState(null); @@ -72,14 +70,26 @@ const MeterEventChannelWindow = (props: IProps) => { const roles = useAppSelector(SelectRoles); React.useEffect(() => { - if (pStatus == 'uninitiated' || pStatus == 'changed') - dispatch(PhaseSlice.Fetch()); - }, [pStatus]) + setPStatus('loading'); + const handle = new GenericController(`${homePath}api/OpenXDA/Phase`, 'Name').Fetch(); + handle.done((d) => { + setPhases(d); + setPStatus('idle'); + }) + handle.fail(() => setPStatus('error')) + return () => { if (handle != null && handle.abort != null) handle.abort() } + }, []); React.useEffect(() => { - if (mtStatus == 'uninitiated' || mtStatus == 'changed') - dispatch(MeasurmentTypeSlice.Fetch()); - }, [mtStatus]) + setMtStatus('loading'); + const handle = new GenericController(`${homePath}api/OpenXDA/MeasurementType`, 'Name').Fetch(); + handle.done((d) => { + setMeasurementTypes(d); + setMtStatus('idle'); + }) + handle.fail(() => setMtStatus('error')) + return () => { if (handle != null && handle.abort != null) handle.abort() } + }, []); React.useEffect(() => { setStatus('loading'); @@ -509,7 +519,15 @@ const MeterEventChannelWindow = (props: IProps) => { Message={'This will permanently delete this Channel and cannot be undone.'} Show={removeRecord != null} Title={'Delete ' + (removeRecord?.Name ?? 'Channel')} - CallBack={(c) => { if (c) EventChannelController.DBAction("DELETE", removeRecord).then(() => { setRemoveRecord(null); setRefreshTrigger(val => !val) }); }} /> + CallBack={(c) => { + if (c) EventChannelController.DBAction("DELETE", removeRecord) + .then(() => { + setRemoveRecord(null); + setRefreshTrigger(val => !val) + }); + else + setRemoveRecord(null) + }} /> } diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterLocation.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterLocation.tsx index 022f6e61c7..f771b45abb 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterLocation.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Meter/MeterLocation.tsx @@ -24,24 +24,24 @@ import * as React from 'react'; import * as _ from 'lodash'; import { Application, OpenXDA } from '@gpa-gemstone/application-typings'; import { AssetAttributes } from '../AssetAttribute/Asset'; -import { useAppDispatch, useAppSelector } from '../hooks'; +import { useAppSelector } from '../hooks'; import { cloneDeep } from 'lodash'; import { ToolTip } from '@gpa-gemstone/react-forms'; import MeterLocationProperties from './PropertyUI/MeterLocationProperties'; -import { LocationSlice, ByMeterSlice } from '../Store/Store'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; +import { GenericController } from '@gpa-gemstone/react-interactive'; import { SelectRoles } from '../Store/UserSettings'; declare var homePath: string; +const LocationController = new GenericController(`${homePath}api/OpenXDA/Location`, "LocationKey", true); + interface IProps { Meter: OpenXDA.Types.Meter, StateSetter: (meter: OpenXDA.Types.Meter) => void } const LocationWindow = (props: IProps) => { // Location Slice consts - const dispatch = useAppDispatch(); - const locationStatus = useAppSelector(LocationSlice.Status) as Application.Types.Status; - const locationList = useAppSelector(LocationSlice.Data) as OpenXDA.Types.Location[]; - const [updateMeter, setUpdateMeter] = React.useState(false); + const [locationStatus, setLocationStatus] = React.useState('uninitiated'); + const [locationList, setLocationList] = React.useState([]); const newLocation: OpenXDA.Types.Location = { ID: 0, @@ -61,13 +61,18 @@ const LocationWindow = (props: IProps) => { const [hover, setHover] = React.useState<('Update' | 'Reset' | 'None')>('None'); const roles = useAppSelector(SelectRoles); - React.useEffect(() => { - if (locationStatus === 'uninitiated' || locationStatus === 'changed') - dispatch(LocationSlice.Fetch()); - else if (locationStatus === 'idle' && updateMeter) - setLocation(locationList.find((d: OpenXDA.Types.Location) => d.LocationKey === location.LocationKey) as OpenXDA.Types.Location); + const isValidLocation = valid('LocationKey') && valid('Name') && valid('Alias') && valid('ShortName') && valid('Latitude') && valid('Longitude') - }, [dispatch, locationStatus, updateMeter]); + React.useEffect(() => { + setLocationStatus('loading'); + const handle = LocationController.Fetch(); + handle.done((d) => { + setLocationStatus('idle'); + setLocationList(d); + }) + handle.fail(() => setLocationStatus('error')); + return () => { if (handle != null && handle.abort != null) handle.abort() } + }, []) React.useEffect(() => { setMeter(props.Meter); @@ -90,18 +95,6 @@ const LocationWindow = (props: IProps) => { setValidKey(location.ID == index[0].ID); }, [location, locationList]); - React.useEffect(() => { - if (location.ID < 1 || !updateMeter) return; - setUpdateMeter(false); - let updateMeterHandle = updateMeterLocation(location.ID); - return () => { - if (updateMeterHandle != null && updateMeterHandle.abort != null) { - updateMeterHandle.abort(); - } - }; - - }, [location, updateMeter]); - function valid(field: keyof OpenXDA.Types.Location): boolean { if (field == 'LocationKey') return location.LocationKey != null && location.LocationKey.length > 0 && location.LocationKey.length <= 50 && validKey; @@ -120,11 +113,17 @@ const LocationWindow = (props: IProps) => { return false; } - const isValidLocation = valid('LocationKey') && valid('Name') && valid('Alias') && valid('ShortName') && valid('Latitude') && valid('Longitude'); - function postLocation() { - setUpdateMeter(true); - dispatch(LocationSlice.DBAction({ verb: (location.ID > 0 ? 'PATCH' : 'POST'), record: location })); + LocationController.DBAction((location.ID > 0 ? 'PATCH' : 'POST'), location) + .then(() => { + LocationController.Fetch() + .then((data) => { + setLocationList(data); + const newLocation = data.find((d: OpenXDA.Types.Location) => d.LocationKey === location.LocationKey) + setLocation(newLocation as OpenXDA.Types.Location); + updateMeterLocation(newLocation.ID); + }) + }); } function setLocationFromMeter(locationID: number) { 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 56e63f0530..b060931164 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 } from '../hooks'; import { ConfigurableTable, ConfigurableColumn, Column, Paging } from '@gpa-gemstone/react-table'; import { SelectRoles } from '../Store/UserSettings'; @@ -42,7 +41,6 @@ interface IProps { Meter: GemstoneOpenXDA.Types.Meter, IsVisible: boolean } const TrendChannelController = new GenericController(`${homePath}api/OpenXDA/TrendChannel`, 'Name'); const MeterTrendChannelWindow = (props: IProps) => { - const dispatch = useAppDispatch(); const [data, setData] = React.useState([]); const [sortKey, setSortKey] = React.useState('Name'); @@ -52,17 +50,18 @@ const MeterTrendChannelWindow = (props: IProps) => { const [totalPages, setTotalPages] = React.useState(0); const [totalRecords, setTotalRecords] = React.useState(0); const [recordsPerPage, setRecordsPerPage] = React.useState(0); + 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 [phases, setPhases] = React.useState([]); + const [measurementTypes, setMeasurementTypes] = React.useState([]); + const [measurementCharacteristics, setMeasurementCharacteristics] = React.useState([]); 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 [phaseStatus, setPhaseStatus] = React.useState('uninitiated'); + const [mtStatus, setMtStatus] = React.useState('uninitiated'); + const [mcStatus, setMcStatus] = React.useState('uninitiated'); const [assetStatus, setAssetStatus] = React.useState('idle') const [removeRecord, setRemoveRecord] = React.useState(null); @@ -72,19 +71,37 @@ const MeterTrendChannelWindow = (props: IProps) => { const roles = useAppSelector(SelectRoles); React.useEffect(() => { - if (phaseStatus == 'uninitiated' || phaseStatus == 'changed') - dispatch(PhaseSlice.Fetch()); - }, [phaseStatus]); + setPhaseStatus('loading'); + const handle = new GenericController(`${homePath}api/OpenXDA/Phase`, 'Name').Fetch(); + handle.done((d) => { + setPhases(d); + setPhaseStatus('idle'); + }) + handle.fail(() => setPhaseStatus('error')) + return () => { if (handle != null && handle.abort != null) handle.abort() } + }, []); React.useEffect(() => { - if (mtStatus == 'uninitiated' || mtStatus == 'changed') - dispatch(MeasurmentTypeSlice.Fetch()); - }, [mtStatus]); + setMtStatus('loading'); + const handle = new GenericController(`${homePath}api/OpenXDA/MeasurementType`, 'Name').Fetch(); + handle.done((d) => { + setMeasurementTypes(d); + setMtStatus('idle'); + }) + handle.fail(() => setMtStatus('error')) + return () => { if (handle != null && handle.abort != null) handle.abort() } + }, []); React.useEffect(() => { - if (mcStatus == 'uninitiated' || mcStatus == 'changed') - dispatch(MeasurementCharacteristicSlice.Fetch()); - }, [mcStatus]); + setMcStatus('loading'); + const handle = new GenericController(`${homePath}api/OpenXDA/MeasurementCharacteristic`, 'Name').Fetch(); + handle.done((d) => { + setMeasurementCharacteristics(d); + setMcStatus('idle'); + }) + handle.fail(() => setMcStatus('error')) + return () => { if (handle != null && handle.abort != null) handle.abort() } + }, []); React.useEffect(() => { setStatus('loading'); @@ -583,7 +600,7 @@ const MeterTrendChannelWindow = (props: IProps) => { Trend: true } - TrendChannelController.DBAction('POST', newChannel).then(() => setRefreshTrigger(val => !val)); + TrendChannelController.DBAction( 'POST', newChannel).then(() => setRefreshTrigger(val => !val)); } }}>Add Channel
    @@ -609,7 +626,11 @@ const MeterTrendChannelWindow = (props: IProps) => {
    - { if (c) TrendChannelController.DBAction('DELETE', removeRecord).then(() => setRefreshTrigger(val => !val)); setRemoveRecord(null); }} /> + { if (c) TrendChannelController.DBAction('DELETE', removeRecord).then(() => setRefreshTrigger(val => !val)); setRemoveRecord(null); }} /> } From 3acd5aab4ad4f0a47c30510cc5e687455dd1d062 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Fri, 21 Aug 2026 12:49:21 -0400 Subject: [PATCH 2/5] use controller in user components --- .../User/AdditionalUserFieldsWindow.tsx | 131 ++++++++++++----- .../TSX/SystemCenter/User/User/ByUser.tsx | 136 +++++++++++------- .../TSX/SystemCenter/User/User/Info.tsx | 37 ++++- .../TSX/SystemCenter/User/User/User.tsx | 46 ++++-- .../TSX/SystemCenter/User/User/UserForm.tsx | 28 ++-- 5 files changed, 264 insertions(+), 114 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/AdditionalUserFieldsWindow.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/AdditionalUserFieldsWindow.tsx index dfacbcca6e..844f5a406b 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/AdditionalUserFieldsWindow.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/AdditionalUserFieldsWindow.tsx @@ -23,15 +23,14 @@ import * as React from 'react'; import { Table, Column } from '@gpa-gemstone/react-table'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; -import { Modal, ServerErrorIcon, Warning as WarningModal } from '@gpa-gemstone/react-interactive'; +import { Modal, ServerErrorIcon, Warning as WarningModal, GenericController } from '@gpa-gemstone/react-interactive'; import { SystemCenter, Application } from '@gpa-gemstone/application-typings'; import * as _ from 'lodash'; import { CheckBox, Input, Select, ToolTip } from '@gpa-gemstone/react-forms'; -import { UserAdditionalFieldSlice, ValueListSlice, ValueListGroupSlice } from '../Store/Store'; -import { useAppDispatch, useAppSelector } from '../hooks'; import { IsInteger, IsNumber } from '@gpa-gemstone/helper-functions'; interface IProps { + // the ID of the user account Id: string | number, EmptyField: Application.Types.iAdditionalUserField GetFieldValueIndex: (field: Application.Types.iAdditionalUserField, values: Application.Types.iAdditionalUserFieldValue[]) => number, @@ -41,28 +40,28 @@ interface IProps { CreateValue: (field: Application.Types.iAdditionalUserField) => Application.Types.iAdditionalUserFieldValue, FieldUI: (field: Application.Types.iAdditionalUserField, setField: (field: Application.Types.iAdditionalUserField) => void) => JSX.Element } +const additionalUserFieldController = new GenericController(`${homePath}api/SystemCenter/AdditionalUserField`, "FieldName"); function AdditionalField(props: IProps) { - const dispatch = useAppDispatch(); - const valueListItems = useAppSelector(ValueListSlice.Data); - const valueListItemStatus = useAppSelector(ValueListSlice.Status); + const [valueListItems, setValueListItems] = React.useState([]); + const [valueListItemStatus, setValueListItemStatus] = React.useState('uninitiated'); - const valueListGroups = useAppSelector(ValueListGroupSlice.Data); - const valueListGroupStatus= useAppSelector(ValueListGroupSlice.Status); + const [valueListGroups, setValueListGroups] = React.useState([]); + const [valueListGroupStatus, setValueListGroupStatus] = React.useState('uninitiated'); - const fields = useAppSelector(UserAdditionalFieldSlice.Fields); - const values = useAppSelector(UserAdditionalFieldSlice.Values); - const fieldStatus = useAppSelector(UserAdditionalFieldSlice.FieldStatus); - const valueStatus = useAppSelector(UserAdditionalFieldSlice.ValueStatus); - const valueParentID = useAppSelector(UserAdditionalFieldSlice.ValueParentId); + const [fields, setFields] = React.useState([]); + const [fieldStatus, setFieldStatus] = React.useState('uninitiated'); + const [values, setValues] = React.useState([]); + const [valueStatus, setValueStatus] = React.useState('uninitiated'); const [pageStatus, setPageStatus] = React.useState('uninitiated'); const [editValues, setEditValues] = React.useState([]); - const sortField = useAppSelector(UserAdditionalFieldSlice.SortField); - const ascending = useAppSelector(UserAdditionalFieldSlice.Ascending); + const [sortField, setSortField] = React.useState('FieldName'); + const [ascending, setAscending] = React.useState(true); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); const [newField, setNewField] = React.useState(props.EmptyField); const [showWarning, setShowWarning] = React.useState(false); @@ -88,24 +87,70 @@ function AdditionalField(props: IProps) { }, [fieldStatus, valueStatus, valueListGroupStatus, valueListItemStatus]) React.useEffect(() => { - if (fieldStatus === 'uninitiated' || fieldStatus === 'changed') - dispatch(UserAdditionalFieldSlice.FetchField()); - }, [dispatch, fieldStatus]); + setFieldStatus('loading'); + const handle = $.ajax({ + type: 'GET', + url: `${homePath}api/SystemCenter/AdditionalUserField/${sortField}/${ascending ? 1 : 0}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: true, + async: true + }) + handle.done((d) => { + setFields(JSON.parse(d)); + setFieldStatus('idle'); + }) + handle.fail(() => { + setFieldStatus('error'); + }) + return () => { if (handle != null && handle.abort != null) handle.abort() } + }, [sortField, ascending, refreshTrigger]) React.useEffect(() => { - if (valueStatus === 'uninitiated' || valueStatus === 'changed' || props.Id !== valueParentID) - dispatch(UserAdditionalFieldSlice.FetchValues(props.Id)); - }, [dispatch, valueStatus, props.Id, valueParentID]); + setValueStatus('loading'); + const handle = $.ajax({ + type: 'GET', + url: `${homePath}api/SystemCenter/AdditionalUserFieldValue/${props.Id}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: true, + async: true + }) + handle.done((d) => { + setValues(d); + setValueStatus('idle'); + }) + handle.fail(() => setValueStatus('error')) + return () => { if (handle != null && handle.abort != null) handle.abort() } + }, [props.Id, refreshTrigger]) React.useEffect(() => { - if (valueListItemStatus === 'uninitiated' || valueListItemStatus === 'changed') - dispatch(ValueListSlice.Fetch()); - }, [dispatch, valueListItemStatus]); + 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 (valueListGroupStatus === 'uninitiated' || valueListGroupStatus === 'changed') - dispatch(ValueListGroupSlice.Fetch()); - }, [dispatch, valueListGroupStatus]); + 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(() => { setEditValues(values) }, [values]) @@ -142,6 +187,19 @@ function AdditionalField(props: IProps) { setChangedFields(c) }, [values, editValues]) + async function updateValues(d: Application.Types.iAdditionalUserFieldValue[], userID: string) { + const data = d.map(v => ({ ...v, UserAccountID: userID })); + return $.ajax({ + type: "PATCH", + url: `${homePath}api/SystemCenter/AdditionalUserFieldValue/Array`, + contentType: "application/json; charset=utf-8", + data: JSON.stringify(data), + dataType: 'json', + cache: true, + async: true + }) + } + if (pageStatus === 'error') return
    @@ -186,9 +244,11 @@ function AdditionalField(props: IProps) { Ascending={ascending} OnSort={(d) => { if (d.colKey === sortField) - dispatch(UserAdditionalFieldSlice.Sort({ SortField: d.colField, Ascending: !ascending })) - else - dispatch(UserAdditionalFieldSlice.Sort({ SortField: d.colField, Ascending: true })) + setAscending(a => !a); + else { + setAscending(true); + setSortField(d.colKey as keyof Application.Types.iAdditionalUserField); + } }} TableStyle={{ padding: 0, width: '100%', tableLayout: 'fixed', display: 'flex', flexDirection: 'column', overflow: 'hidden' }} TheadStyle={{ fontSize: 'smaller', display: 'table', tableLayout: 'fixed', width: '100%' }} @@ -215,7 +275,7 @@ function AdditionalField(props: IProps) { Key={'Value'} - AllowSort={true} + AllowSort={false} HeaderStyle={{ width: 'auto' }} RowStyle={{ width: 'auto' }} Content={({ item }) => { @@ -266,7 +326,8 @@ function AdditionalField(props: IProps) {

    To add a new Field, switch to Edit mode by clicking on the Edit button on the upper right corner.

    -
    0)} Position={'top'} Target={"SaveValues"}> @@ -290,7 +351,7 @@ function AdditionalField(props: IProps) {
    ") + "' from all Users and all Values assigned to it."} - CallBack={(confirm: boolean) => { if (confirm) dispatch(UserAdditionalFieldSlice.FieldAction({ Verb: 'DELETE', Record: newField })); setShowWarning(false) }} /> + CallBack={(confirm: boolean) => { if (confirm) additionalUserFieldController.DBAction('DELETE', newField).then(() => setRefreshTrigger(val => !val)); setShowWarning(false) }} /> { if (confirmation) { if (props.FieldKeySelector(newField) === "new") - dispatch(UserAdditionalFieldSlice.FieldAction({ Verb: "POST", Record: newField })) + additionalUserFieldController.DBAction("POST", newField).then(() => setRefreshTrigger(val => !val)); else - dispatch(UserAdditionalFieldSlice.FieldAction({ Verb: "PATCH", Record: newField })) + additionalUserFieldController.DBAction("PATCH", newField).then(() => setRefreshTrigger(val => !val)); } setShowEdit(false); 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 863dadbf53..0449682015 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 a6feffdcd6..af49fbd7fa 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,20 @@ 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(); - const currentUser = useAppSelector((state) => UserAccountSlice.Datum(state,props.AccountId)); + // the user as represented in the database + const [currentUser, setCurrentUser] = React.useState(null) + const [status, setStatus] = React.useState('uninitiated'); + const [refreshTrigger, setRefreshTrigger] = React.useState(false); + + // 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 +73,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, refreshTrigger]) + function updateUser() { - dispatch(UserAccountSlice.DBAction({ verb: 'PATCH', record: user })) + new GenericController(`${homePath}api/SystemCenter/UserAccount`, "DisplayName", true).DBAction( 'PATCH', user).then(() => 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 dbd48a9fec..511f42829e 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,9 +37,8 @@ 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); @@ -59,9 +57,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]) if (status === 'error') return
    @@ -107,12 +123,12 @@ function User(props: IProps) { (user == null || user.Type == 'Database' ? 'This will permanently remove the User. Are you sure you want to continue?' : 'This will remove the User from openXDA. The User may still have rights and the ability to log in to the system if they are in an Azure or Active Directory group. Contact your domain administrator to have the User removed from Azure or AD.') } Title={'Delete ' + (user?.AccountName ?? 'User')} Show={showWarning} CallBack={(c) => { - 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 1b5c80999e..4415fccdd9 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) From cd40503f7ad42d38d3e72abac06d2e65f7bceae8 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Fri, 21 Aug 2026 12:49:35 -0400 Subject: [PATCH 3/5] use controller in user group components --- .../User/UserGroup/ByUserGroup.tsx | 86 +++++++++---------- .../SystemCenter/User/UserGroup/GroupForm.tsx | 24 ++++-- .../TSX/SystemCenter/User/UserGroup/Info.tsx | 8 +- .../SystemCenter/User/UserGroup/UserGroup.tsx | 41 ++++++--- 4 files changed, 88 insertions(+), 71 deletions(-) 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 e87c5a9b62..f888503748 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 78f9c0ec17..a179fc4767 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/Info.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/User/UserGroup/Info.tsx index 85d0dec6f8..1ad9d2614a 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,15 +22,13 @@ 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([]); @@ -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 461286c6fe..e544c488bb 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 263a7f5dd5b75e97c110bf047c8d57d01c1a3ee2 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Fri, 21 Aug 2026 12:49:46 -0400 Subject: [PATCH 4/5] use controller in value list components --- .../ValueListGroup/ValueListForm.tsx | 24 ++++++----- .../ValueListGroup/ValueListGroup.tsx | 41 ++++++++++++++----- .../ValueListGroup/ValueListGroupInfo.tsx | 6 +-- .../ValueListGroup/ValueListGroupItem.tsx | 2 + 4 files changed, 47 insertions(+), 26 deletions(-) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListForm.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListForm.tsx index a7eb038832..cd8d7522e5 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListForm.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListForm.tsx @@ -22,11 +22,10 @@ //****************************************************************************************************** import * as React from 'react'; -import { SystemCenter } from '@gpa-gemstone/application-typings'; +import { Application, SystemCenter } from '@gpa-gemstone/application-typings'; import { Input } from '@gpa-gemstone/react-forms'; import { IsInteger } from '@gpa-gemstone/helper-functions'; -import { useAppSelector, useAppDispatch } from '../hooks'; -import { ValueListSlice } from '../Store/Store'; +import { GenericController } from '@gpa-gemstone/react-interactive'; interface IProps { Record: SystemCenter.Types.ValueListItem, @@ -35,16 +34,19 @@ interface IProps { } export default function ValueListForm(props: IProps) { - const dispatch = useAppDispatch(); - - const data = useAppSelector(ValueListSlice.Data); - const status = useAppSelector(ValueListSlice.Status); - const parentID = useAppSelector(ValueListSlice.ParentID); + const [data, setData] = React.useState([]); + const [status, setStatus] = React.useState('uninitiated'); React.useEffect(() => { - if (status == 'uninitiated' || status == 'changed' || parentID != props.Record.GroupID) - dispatch(ValueListSlice.Fetch(props.Record.GroupID)); - }, [status, parentID, props.Record.GroupID]); + setStatus('loading'); + const handle = new GenericController(`${homePath}api/ValueList`, 'SortOrder').Fetch(props.Record.GroupID); + handle.done((d) => { + setStatus('idle'); + setData(d); + }) + handle.fail(() => setStatus('error')) + return () => { if (handle != null && handle.abort != null) handle.abort() } + }, [props.Record.GroupID]) React.useEffect(() => { if (props.SetErrors == undefined) diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListGroup.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListGroup.tsx index bea92be1d5..844ece9ae5 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListGroup.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListGroup.tsx @@ -21,10 +21,9 @@ // //****************************************************************************************************** -import { TabSelector } from '@gpa-gemstone/react-interactive'; +import { TabSelector, GenericController } from '@gpa-gemstone/react-interactive'; +import { Application, SystemCenter } from '@gpa-gemstone/application-typings'; import * as React from 'react'; -import { useAppDispatch, useAppSelector } from '../hooks'; -import { ValueListGroupSlice } from '../Store/Store'; import { ValueListGroupDelete } from './ValueListGroupDelete'; import ValueListGroupInfo from './ValueListGroupInfo'; import ValueListGroupItems from './ValueListGroupItem'; @@ -34,11 +33,11 @@ declare type Tab = 'info' | 'items' interface IProps { GroupID: number, Tab: Tab } -export default function ValueListGroup(props: IProps) { - const dispatch = useAppDispatch(); - const record = useAppSelector((state) => ValueListGroupSlice.Datum(state, props.GroupID)); - const valueListGroupStatus = useAppSelector(ValueListGroupSlice.Status); +const ValueListGroupController = new GenericController(`${homePath}api/ValueListGroup`, 'Name'); +export default function ValueListGroup(props: IProps) { + const [record, setRecord] = React.useState(null); + const [status, setStatus] = React.useState('uninitiated'); const [tab, setTab] = React.useState(getTab()); const [showRemove, setShowRemove] = React.useState(false); @@ -56,14 +55,34 @@ export default function ValueListGroup(props: IProps) { sessionStorage.setItem('ValueListGroup.Tab', JSON.stringify(tab)); }, [tab]); + React.useEffect(() => { - if (valueListGroupStatus == 'uninitiated' || valueListGroupStatus == 'changed') - dispatch(ValueListGroupSlice.Fetch()); - }, [valueListGroupStatus]); + setStatus('loading'); + const handle = $.ajax({ + type: "GET", + url: `${homePath}api/ValueListGroup/One/${props.GroupID}`, + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: true, + async: true + }) + handle.done((d) => { + setStatus('idle'); + setRecord(d); + }) + handle.fail(() => { + setStatus('error') + }) + return () => { + if (handle != null && handle.abort != null) + handle.abort() + } + }, [props.GroupID]) function Delete() { - dispatch(ValueListGroupSlice.DBAction({ verb: 'DELETE', record })) + ValueListGroupController.DBAction('DELETE', record).then(() => { window.location.href = homePath + 'index.cshtml?name=ValueLists'; + }) } if (record == null) return null; diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListGroupInfo.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListGroupInfo.tsx index 00d9b3d992..69e349cad8 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListGroupInfo.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListGroupInfo.tsx @@ -25,13 +25,11 @@ import * as React from 'react'; import * as _ from 'lodash'; import { SystemCenter } from '@gpa-gemstone/application-typings'; -import { useAppDispatch } from '../hooks'; -import { ValueListGroupSlice } from '../Store/Store'; +import { GenericController } from '@gpa-gemstone/react-interactive'; import ValueListGroupForm from './ValueListGroupForm'; const ValueListInfoWindow = (props: { Record: SystemCenter.Types.ValueListGroup }) => { const [record, setRecord] = React.useState(props.Record); - const dispatch = useAppDispatch(); React.useEffect(() => { setRecord(props.Record); @@ -52,7 +50,7 @@ const ValueListInfoWindow = (props: { Record: SystemCenter.Types.ValueListGroup
    - +
    diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListGroupItem.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListGroupItem.tsx index 18e0f77fe4..1138bbb071 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListGroupItem.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/ValueListGroup/ValueListGroupItem.tsx @@ -60,6 +60,8 @@ export default function ValueListGroupItems(props: IProps) { const [countDictionary, setCountDictionary] = React.useState<{ [key: string]: number }>({}); const [hover, setHover] = React.useState(''); + const controller = React.useMemo(() => new GenericController(`${homePath}api/ValueList`, 'SortOrder'), []); + const disallowReason = React.useCallback((ID: string) => { if (!RequiredValueLists.includes(props.Record?.Name)) return null; From 9669c4ba5c28f901b53e2f0ad5d90535c19e6e38 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Fri, 21 Aug 2026 12:50:02 -0400 Subject: [PATCH 5/5] remove unused slices from store --- .../wwwroot/Scripts/TSX/SystemCenter/Store/Store.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 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 0a797447e8..f479909cfc 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/Store.ts +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/Store.ts @@ -28,14 +28,13 @@ import { GenericSlice } from '@gpa-gemstone/react-interactive' import { EventChannelSlice } from './EventChannelSlice'; import { SystemCenter, OpenXDA, Application, PQBrowser } from '@gpa-gemstone/application-typings'; import NoteSlice from './NoteSlice'; -import AdditionalUserFieldSlice from './AdditionalUserFieldSlice'; import ConfigurationSlice from './ConfigurationSlice'; import { PQApplications } from '../ApplicationCategory/Applications'; 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' @@ -107,11 +106,8 @@ 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) @@ -171,8 +167,6 @@ const store = configureStore({ UserNote: UserNoteSlice.Reducer, LocationNote: LocationNoteSlice.Reducer, CustomerNote: CustomerNoteSlice.Reducer, - UserAccounts: UserAccountSlice.Reducer, - AdditionalUserFields: UserAdditionalFieldSlice.Reducer, EventType: EventTypeSlice.Reducer, MeasurementCharacteristic: MeasurementCharacteristicSlice.Reducer, DataOperation: DataOperationSlice.Reducer, @@ -191,7 +185,6 @@ const store = configureStore({ CustomerAsset: CustomerAssetSlice.Reducer, PQI: PQISlice, SourceImpedance: SourceImpedanceSlice.Reducer, - SecurityGroup: SecurityGroupSlice.Reducer, WidgetCategory: WidgetCategorySlice.Reducer, ApplicationRole: ApplicationRoleSlice.Reducer, SEBrowserWidget: SEBrowserWidgetSlice.Reducer,