From 5c505346da5a4d29f2b6d02ed05e88d1428ef908 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Mon, 24 Aug 2026 10:45:19 -0400 Subject: [PATCH 1/6] add controller select popup to common components --- .../ControllerSelectPopup.tsx | 171 ++++++++++++++++++ 1 file changed, 171 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..a6143ffaa --- /dev/null +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/ControllerSelectPopup.tsx @@ -0,0 +1,171 @@ +// ****************************************************************************************************** +// 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 +} + + +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} + 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 0853fab729a48a2364027b7f107e6f5be9b18558 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Mon, 24 Aug 2026 10:51:46 -0400 Subject: [PATCH 2/6] use controller in user group and user components --- .../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 ++-- .../User/UserGroup/ByUserGroup.tsx | 86 ++++++----- .../SystemCenter/User/UserGroup/GroupForm.tsx | 24 ++-- .../User/UserGroup/GroupUsers.tsx | 31 ++-- .../TSX/SystemCenter/User/UserGroup/Info.tsx | 8 +- .../SystemCenter/User/UserGroup/UserGroup.tsx | 41 ++++-- 9 files changed, 277 insertions(+), 160 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..af49fbd7f 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 dbd48a9fe..511f42829 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 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..16936182c 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,11 +23,12 @@ 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 }) => { @@ -204,9 +205,8 @@ const GroupUser = (props: { Group: ISecurityGroup }) => { disabled={props.Group.Type !== 'Database'}>Add Users
    - - + Controller={RemoteUserAccountController} Selection={users} OnClose={(selected, conf) => { setShowSelect(false); @@ -216,8 +216,20 @@ const GroupUser = (props: { Group: ISecurityGroup }) => { Show={showSelect} Type={'multiple'} Title={"Add Users to " + props.Group.Name} - GetEnum={() => () => { }} - GetAddlFields={() => () => { }} + 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 +241,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..1ad9d2614 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 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 c4e6efd7dadcd3ac7ca410a0ff825aef0cf45ff0 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Mon, 24 Aug 2026 10:51:53 -0400 Subject: [PATCH 3/6] remove security group, user account, and remote user account slices --- .../wwwroot/Scripts/TSX/SystemCenter/Store/Store.ts | 8 +------- 1 file changed, 1 insertion(+), 7 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..9f6e4498a 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' @@ -100,18 +100,15 @@ 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) @@ -171,7 +168,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, @@ -181,7 +177,6 @@ const store = configureStore({ remoteXDAInstance: RemoteXDAInstanceSlice.Reducer, RemoteXDAAsset: RemoteXDAAssetSlice.Reducer, RemoteXDAMeter: RemoteXDAMeterSlice.Reducer, - UserAccountRemote: UserAccountSliceRemote.Reducer, ApplicationNode: ApplicationNodeSlice.Reducer, PQApplicationCategory: ApplicationCategorySlice.Reducer, PQApplications: PQApplicationsSlice.Reducer, @@ -191,7 +186,6 @@ const store = configureStore({ CustomerAsset: CustomerAssetSlice.Reducer, PQI: PQISlice, SourceImpedance: SourceImpedanceSlice.Reducer, - SecurityGroup: SecurityGroupSlice.Reducer, WidgetCategory: WidgetCategorySlice.Reducer, ApplicationRole: ApplicationRoleSlice.Reducer, SEBrowserWidget: SEBrowserWidgetSlice.Reducer, From 17d20554c15e36ff7db61a4d01755de982c7852e Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Mon, 24 Aug 2026 10:52:04 -0400 Subject: [PATCH 4/6] update gemstone --- .../SystemCenter/package-lock.json | 46 +++++++++---------- Source/Applications/SystemCenter/package.json | 4 +- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/Source/Applications/SystemCenter/package-lock.json b/Source/Applications/SystemCenter/package-lock.json index 6d5668477..45a32266a 100644 --- a/Source/Applications/SystemCenter/package-lock.json +++ b/Source/Applications/SystemCenter/package-lock.json @@ -9,8 +9,8 @@ "version": "0.0.1", "dependencies": { "@arcgis/core": "5.1.17", - "@gpa-gemstone/common-pages": "0.0.185", - "@gpa-gemstone/react-graph": "1.0.113", + "@gpa-gemstone/common-pages": "0.0.189", + "@gpa-gemstone/react-graph": "1.0.115", "@reduxjs/toolkit": "1.8.3", "@turf/turf": "7.4.0", "assert": "2.0.0", @@ -1364,17 +1364,17 @@ } }, "node_modules/@gpa-gemstone/common-pages": { - "version": "0.0.185", - "resolved": "https://registry.npmjs.org/@gpa-gemstone/common-pages/-/common-pages-0.0.185.tgz", - "integrity": "sha512-IgM8kJfCsKcIEuO6tYUPwRL66WJAkhUpOmK/ufE3p2QggnHCaDylxMs8k+Fat2YTlE2b6+ePncV4WYEy9FP1dA==", + "version": "0.0.189", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/common-pages/-/common-pages-0.0.189.tgz", + "integrity": "sha512-dK/rupXT7jwElbYdbGUnI9n/aAj35r/3t7bIcg+koesPoK193z3/EkpUj1cKqLlsZ5JpeOy2P0bXr2WZnlNlmg==", "license": "MIT", "dependencies": { "@gpa-gemstone/application-typings": "0.0.99", "@gpa-gemstone/gpa-symbols": "0.0.65", "@gpa-gemstone/helper-functions": "0.0.61", - "@gpa-gemstone/react-forms": "1.1.126", - "@gpa-gemstone/react-interactive": "1.0.194", - "@gpa-gemstone/react-table": "1.2.118", + "@gpa-gemstone/react-forms": "1.1.127", + "@gpa-gemstone/react-interactive": "1.0.195", + "@gpa-gemstone/react-table": "1.2.119", "@reduxjs/toolkit": "1.8.3", "crypto-js": "^4.2.0", "moment": "^2.29.4", @@ -1419,9 +1419,9 @@ } }, "node_modules/@gpa-gemstone/react-forms": { - "version": "1.1.126", - "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-forms/-/react-forms-1.1.126.tgz", - "integrity": "sha512-E39VGo3DwYnQJlLmyCRgzI4wF+MCVpktt0UyyU9+PSFdKoZYaWO5PfpZ6s9uwxuAz585FIwBhaVc2VROrrLvig==", + "version": "1.1.127", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-forms/-/react-forms-1.1.127.tgz", + "integrity": "sha512-Cb0j3ZBN0LVBZVgomiYWB1rrVFeFAuZVeOrxasNv3rGsB/uWAH4pUg+o8QWvjTvhZD83IHzVS7gudZC/W3GbMw==", "license": "MIT", "dependencies": { "@gpa-gemstone/application-typings": "0.0.99", @@ -1438,14 +1438,14 @@ } }, "node_modules/@gpa-gemstone/react-graph": { - "version": "1.0.113", - "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-graph/-/react-graph-1.0.113.tgz", - "integrity": "sha512-2yq6EBlcykSgzLP1W7Wy93Sk2S2R4s4vDDfh85qYUQ0b6p5W2CjwASrxF38z33W3EL8kLsARXTjmvuxV0ieGHw==", + "version": "1.0.115", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-graph/-/react-graph-1.0.115.tgz", + "integrity": "sha512-7JZqqEyvk+IxKqbQ6cALV9EuD/Pe7jed1WscpdgMR37wJZXB01Z7jMktr7zYsB4WbR7HkonUSBwFBBgYMaa7xw==", "license": "MIT", "dependencies": { "@gpa-gemstone/gpa-symbols": "0.0.65", "@gpa-gemstone/helper-functions": "0.0.61", - "@gpa-gemstone/react-forms": "1.1.126", + "@gpa-gemstone/react-forms": "1.1.127", "html2canvas": "^1.4.1", "lodash": "^4.17.21", "moment": "2.29.4" @@ -1456,15 +1456,15 @@ } }, "node_modules/@gpa-gemstone/react-interactive": { - "version": "1.0.194", - "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-interactive/-/react-interactive-1.0.194.tgz", - "integrity": "sha512-eldz+DOZUOZMlxam34pSduTkfS1LT8+HDZR1fy04p2hJUnp8YqPz94hYaLW4E6teHjWQrP3JM2wMhN3bJtXS2A==", + "version": "1.0.195", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-interactive/-/react-interactive-1.0.195.tgz", + "integrity": "sha512-QMW0EF6OjqMtXFsYZnTimQSAIWMjM5u5OXsuWOFgxQBTl0UkDezdxuDouTOONCqKcx8bwga5FCLP0LCnsjx1Aw==", "license": "MIT", "dependencies": { "@gpa-gemstone/application-typings": "0.0.99", "@gpa-gemstone/gpa-symbols": "0.0.65", "@gpa-gemstone/helper-functions": "0.0.61", - "@gpa-gemstone/react-forms": "1.1.126", + "@gpa-gemstone/react-forms": "1.1.127", "@reduxjs/toolkit": "1.8.3", "jquery": "^3.6.0", "lodash": "^4.17.21", @@ -1480,14 +1480,14 @@ } }, "node_modules/@gpa-gemstone/react-table": { - "version": "1.2.118", - "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-table/-/react-table-1.2.118.tgz", - "integrity": "sha512-tPWFXuGSGrGzgNzaMy4ACvsoZlTeuCUp+e72G8ynPw77+Gtz3IZh7MJfN5zsOLgks+vyTCRzkMlkXVN+HoedUQ==", + "version": "1.2.119", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-table/-/react-table-1.2.119.tgz", + "integrity": "sha512-S2ZpVDiggcYWp2ADZEGz/wWXfXpWkMJRfCAscwGC2cI8+QnSsxNnyzlp+heEvhQFP7L086/vpL1noe3CgyGPsw==", "license": "MIT", "dependencies": { "@gpa-gemstone/gpa-symbols": "0.0.65", "@gpa-gemstone/helper-functions": "0.0.61", - "@gpa-gemstone/react-interactive": "1.0.194", + "@gpa-gemstone/react-interactive": "1.0.195", "lodash": "^4.17.21", "react-portal": "4.2.2" }, diff --git a/Source/Applications/SystemCenter/package.json b/Source/Applications/SystemCenter/package.json index cc9081c3b..a85272d6e 100644 --- a/Source/Applications/SystemCenter/package.json +++ b/Source/Applications/SystemCenter/package.json @@ -27,11 +27,11 @@ "webpack-cli": "4.7.2" }, "dependencies": { - "@gpa-gemstone/react-graph": "1.0.113", + "@gpa-gemstone/react-graph": "1.0.115", "@turf/turf": "7.4.0", "@arcgis/core": "5.1.17", "proj4leaflet": "^1.0.2", - "@gpa-gemstone/common-pages": "0.0.185", + "@gpa-gemstone/common-pages": "0.0.189", "@reduxjs/toolkit": "1.8.3", "assert": "2.0.0", "buffer": "6.0.3", From c4329e740b4c58bb00c513a880ea7bb08e5b0ab6 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Mon, 24 Aug 2026 11:04:50 -0400 Subject: [PATCH 5/6] keep full list of group users for selection --- .../User/UserGroup/GroupUsers.tsx | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) 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 16936182c..d892b1b67 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 @@ -34,6 +34,7 @@ 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'); @@ -45,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; @@ -72,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: "POST", + url: `${homePath}api/SystemCenter/FullSecurityGroup/Users/${props.Group.ID}`, + contentType: "application/json; charset=utf-8", + cache: false, + async: true + }).done((d) => { + setGroupUsers(JSON.parse(d.Data as unknown as string)); + setGroupStatus('idle'); + }).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; @@ -207,7 +231,7 @@ const GroupUser = (props: { Group: ISecurityGroup }) => {
    Controller={RemoteUserAccountController} - Selection={users} + Selection={groupUsers} OnClose={(selected, conf) => { setShowSelect(false); if (!conf) return; From 5e45b5d0335faa05be8b9a7dd1e53cb1f4d35f43 Mon Sep 17 00:00:00 2001 From: natalie beatty Date: Mon, 24 Aug 2026 11:44:11 -0400 Subject: [PATCH 6/6] remove unused slices --- .../Scripts/TSX/SystemCenter/Store/Store.ts | 27 ------------------- 1 file changed, 27 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 9f6e4498a..37d953f87 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/Store.ts +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/Store.ts @@ -54,17 +54,10 @@ 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'); @@ -74,14 +67,10 @@ export const MeasurementCharacteristicSlice = 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); @@ -108,7 +97,6 @@ export const LocationNoteSlice = new NoteSlice('Location'); export const CustomerNoteSlice = new NoteSlice('Customer'); export const UserAdditionalFieldSlice = new AdditionalUserFieldSlice('AdditionalUserFields', `${homePath}api/SystemCenter`); -export const SourceImpedanceSlice = new GenericSlice("SourceImpedance", `${homePath}api/OpenXDA/SourceImpedance`, "AssetLocationID", false); export const ApplicationRoleSlice = new GenericSlice("ApplicationRole", `${homePath}api/SystemCenter/ApplicationRole`, "Name", false) export const WidgetCategorySlice = new GenericSlice("WidgetCategory", `${homePath}api/SystemCenter/WidgetCategory`, "OrderBy", true) @@ -118,7 +106,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'); @@ -129,8 +116,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"); @@ -156,11 +141,6 @@ 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, @@ -172,7 +152,6 @@ const store = configureStore({ EventType: EventTypeSlice.Reducer, MeasurementCharacteristic: MeasurementCharacteristicSlice.Reducer, DataOperation: DataOperationSlice.Reducer, - DataReader: DataReaderSlice.Reducer, ExternalDataBaseTable: ExternalDBTablesSlice.Reducer, remoteXDAInstance: RemoteXDAInstanceSlice.Reducer, RemoteXDAAsset: RemoteXDAAssetSlice.Reducer, @@ -182,17 +161,13 @@ const store = configureStore({ PQApplications: PQApplicationsSlice.Reducer, DBCleanup: DBCleanupSlice.Reducer, EventChannels: EventChannelSlice.reducer, - CustomerMeter: CustomerMeterSlice.Reducer, - CustomerAsset: CustomerAssetSlice.Reducer, PQI: PQISlice, - SourceImpedance: SourceImpedanceSlice.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, @@ -201,9 +176,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 }