diff --git a/Source/Applications/SystemCenter/AppDebug.config b/Source/Applications/SystemCenter/AppDebug.config index a631b9982..d95bc28b2 100644 --- a/Source/Applications/SystemCenter/AppDebug.config +++ b/Source/Applications/SystemCenter/AppDebug.config @@ -1,184 +1,184 @@  - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Source/Applications/SystemCenter/SystemCenter.csproj b/Source/Applications/SystemCenter/SystemCenter.csproj index 25979961b..27de5f394 100644 --- a/Source/Applications/SystemCenter/SystemCenter.csproj +++ b/Source/Applications/SystemCenter/SystemCenter.csproj @@ -617,9 +617,12 @@ - + + + + @@ -873,7 +876,7 @@ False - + diff --git a/Source/Applications/SystemCenter/package-lock.json b/Source/Applications/SystemCenter/package-lock.json index 8c3506eb4..c06a921f4 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.190", - "@gpa-gemstone/react-graph": "1.0.115", + "@gpa-gemstone/common-pages": "0.0.191", + "@gpa-gemstone/react-graph": "1.0.116", "@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.190", - "resolved": "https://registry.npmjs.org/@gpa-gemstone/common-pages/-/common-pages-0.0.190.tgz", - "integrity": "sha512-rZqtPdyKp8cjEQHvBdEd1ll0scVjy2ldJQ7Sfof0/kZ9l4pbsIw+Fh5N5Kdv0ds8mlV0qKePMV/wrwnwj29THQ==", + "version": "0.0.191", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/common-pages/-/common-pages-0.0.191.tgz", + "integrity": "sha512-degtKkDWQXVTospmoU2rdPo3Dk/Fq3U3Yz8s6dGXObPhigAbE9R/Zj0CctiI0Al7Uw9wNNER/PBqVOGZjGcvkA==", "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.127", - "@gpa-gemstone/react-interactive": "1.0.195", - "@gpa-gemstone/react-table": "1.2.120", + "@gpa-gemstone/helper-functions": "0.0.62", + "@gpa-gemstone/react-forms": "1.1.128", + "@gpa-gemstone/react-interactive": "1.0.196", + "@gpa-gemstone/react-table": "1.2.121", "@reduxjs/toolkit": "1.8.3", "crypto-js": "^4.2.0", "moment": "^2.29.4", @@ -1406,9 +1406,9 @@ } }, "node_modules/@gpa-gemstone/helper-functions": { - "version": "0.0.61", - "resolved": "https://registry.npmjs.org/@gpa-gemstone/helper-functions/-/helper-functions-0.0.61.tgz", - "integrity": "sha512-WA1o0V/aK/yN70Ly4iAD0T2ubAu1yWNDRMngp4YBoLgYgwD9wDXzCk1oG9xH+pAqZ4XAlLmZior8OhjSDpvt/g==", + "version": "0.0.62", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/helper-functions/-/helper-functions-0.0.62.tgz", + "integrity": "sha512-QydrNqr9ZIfmqfGeJ4YJz21FYRj3Co2l4BKgl/ytyuaTNiudhgdq1uMgJY4X4IHLjT28qDfaEDEH7h1iMQkRDA==", "license": "MIT", "dependencies": { "@gpa-gemstone/application-typings": "0.0.99", @@ -1419,14 +1419,14 @@ } }, "node_modules/@gpa-gemstone/react-forms": { - "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==", + "version": "1.1.128", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-forms/-/react-forms-1.1.128.tgz", + "integrity": "sha512-HEqJM9QVQ39rPb+mmZRqz1l3IXeQjYa1zDhUrpGHu5IC+cr9WsPZ92m1WFUD8q0c1Vz5uMIK1Pxt+yIF+/7GHg==", "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/helper-functions": "0.0.62", "lodash": "^4.17.21", "moment": "2.29.4", "react-color": "^2.19.3", @@ -1438,14 +1438,14 @@ } }, "node_modules/@gpa-gemstone/react-graph": { - "version": "1.0.115", - "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-graph/-/react-graph-1.0.115.tgz", - "integrity": "sha512-7JZqqEyvk+IxKqbQ6cALV9EuD/Pe7jed1WscpdgMR37wJZXB01Z7jMktr7zYsB4WbR7HkonUSBwFBBgYMaa7xw==", + "version": "1.0.116", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-graph/-/react-graph-1.0.116.tgz", + "integrity": "sha512-qRrr58jqv/H+cC0EeXbivemczCeCFIGlmFWtpqLvnRLB7yActzw02CS5XU9ZVeo2k9PEctud9n8MDmq8OwN2Zw==", "license": "MIT", "dependencies": { "@gpa-gemstone/gpa-symbols": "0.0.65", - "@gpa-gemstone/helper-functions": "0.0.61", - "@gpa-gemstone/react-forms": "1.1.127", + "@gpa-gemstone/helper-functions": "0.0.62", + "@gpa-gemstone/react-forms": "1.1.128", "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.195", - "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-interactive/-/react-interactive-1.0.195.tgz", - "integrity": "sha512-QMW0EF6OjqMtXFsYZnTimQSAIWMjM5u5OXsuWOFgxQBTl0UkDezdxuDouTOONCqKcx8bwga5FCLP0LCnsjx1Aw==", + "version": "1.0.196", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-interactive/-/react-interactive-1.0.196.tgz", + "integrity": "sha512-BUOUMQCcDNNNamET9dCiHcQZ8m3+fdYi2gsU8bPOGE1lN+AGQ5dzwyY9lO8+ju+I50GNr0uofoSsMGFbr0XbxQ==", "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.127", + "@gpa-gemstone/helper-functions": "0.0.62", + "@gpa-gemstone/react-forms": "1.1.128", "@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.120", - "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-table/-/react-table-1.2.120.tgz", - "integrity": "sha512-Gz9GCAPmeZ14NjFdFxUsyZzWJkd6+BvARIth7np2lVVIp/zLqmU+ll0d0K2UpTKOaxQJz9VDD1CUMmRZDz/Zdw==", + "version": "1.2.121", + "resolved": "https://registry.npmjs.org/@gpa-gemstone/react-table/-/react-table-1.2.121.tgz", + "integrity": "sha512-5sI0IoThNp/+t/rOk3qTQe9t7as8fN6e/pgdfr9h/FSihQuszwSXN0QgRS8t+GR5U+hHhveSK3UnLr0V4qHlzA==", "license": "MIT", "dependencies": { "@gpa-gemstone/gpa-symbols": "0.0.65", - "@gpa-gemstone/helper-functions": "0.0.61", - "@gpa-gemstone/react-interactive": "1.0.195", + "@gpa-gemstone/helper-functions": "0.0.62", + "@gpa-gemstone/react-interactive": "1.0.196", "lodash": "^4.17.21", "react-portal": "4.2.2" }, diff --git a/Source/Applications/SystemCenter/package.json b/Source/Applications/SystemCenter/package.json index f17a8fde0..aa9b40f81 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.115", + "@gpa-gemstone/react-graph": "1.0.116", "@turf/turf": "7.4.0", "@arcgis/core": "5.1.17", "proj4leaflet": "^1.0.2", - "@gpa-gemstone/common-pages": "0.0.190", + "@gpa-gemstone/common-pages": "0.0.191", "@reduxjs/toolkit": "1.8.3", "assert": "2.0.0", "buffer": "6.0.3", diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/ControllerSelectPopup.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/ControllerSelectPopup.tsx index 92d4f34f7..8e1bcefac 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/ControllerSelectPopup.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/ControllerSelectPopup.tsx @@ -39,9 +39,9 @@ interface IProps { MinSelection?: number, children?: React.ReactNode, DisableConfirm?: boolean + Filters?: Search.IFilter[] } - export default function ControllerSelectPopup(props: IProps) { const [filters, setFilters] = React.useState[]>([]); const [sortField, setSortField] = React.useState('ID') @@ -64,8 +64,9 @@ export default function ControllerSelectPopup(props: IProps) { }, [props.Selection]) React.useEffect(() => { - setFilters([]); // initialize filter list, which should add additional filters - }, []) + if (props.Filters != null) setFilters(props.Filters) + else setFilters([]); // initialize filter list, which should add additional filters + }, [props.Filters]) function AddCurrentList() { const updatedData = selectedData.concat(data); diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/GenericInfo.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/GenericInfo.tsx new file mode 100644 index 000000000..50d891778 --- /dev/null +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/GenericInfo.tsx @@ -0,0 +1,140 @@ +//****************************************************************************************************** +// GenericInfo.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: +// ---------------------------------------------------------------------------------------------------- +// 09/10/2026 - Natalie Beatty +// Generated original version of source code. +// +//****************************************************************************************************** + +import * as React from 'react'; +import { ToolTip } from '@gpa-gemstone/react-forms'; +import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; + +interface IProps { + /** + * Forms to display within the tab. + */ + Forms: Array<(record: T, setter: React.Dispatch>, setErrors: React.Dispatch>) => React.ReactNode> + /** + * Determine whether the user has permissions to edit the record. + */ + HasPermissions: () => boolean + /** + * User-facing name for the record type. + */ + RecordType: string + /** + * Record to edit in the form and send as the new version of the model in a patch. + */ + SelectedRecord: T + /** + * Setter for the selected record. + */ + SetSelectedRecord: React.Dispatch> + /** + * List of errors for the selected record. + */ + Errors: string[] + /** + * Setter for the errors + */ + SetErrors: React.Dispatch> + /** + * List of warnings for the selected record. + */ + Warnings: string[] + /** + * Callback to set Selected Record back to its original state. + */ + ClearChanges: () => void; + /** + * Callback to patch the original record to reflect changes made on the selected record. + */ + Patch: () => void; +} + +/** + * A generic tab for record of type T for viewing and editing fields of the record. + * @param props + * @returns + */ +function GenericInfo({ + Forms, + HasPermissions, + RecordType, + SelectedRecord, + SetSelectedRecord, + Errors, + SetErrors, + Warnings, + ClearChanges, + Patch +}: React.PropsWithChildren>) { + const [hover, setHover] = React.useState<('None' | 'Clear' | 'Submit')>('None'); + + return ( +
+
+
+
+

{RecordType} Information:

+
+
+
+
+ { + Forms.map((form) => { + return form(SelectedRecord, SetSelectedRecord, SetErrors) + }) + } +
+
+
+ +
+ 0} Position={'top'} Target={"Update"}> + {Errors.map((t, i) =>

{t}

)} +
+ +

Your role does not have permission. Please contact your Administrator if you believe this to be in error.

+
+
+ +
+ 0} Position={'top'} Target={"Clr"}> + {Warnings.map((t, i) =>

{t}

)} +
+
+
+ ); +} + +export default GenericInfo; \ No newline at end of file diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/GenericRecord.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/GenericRecord.tsx new file mode 100644 index 000000000..8e201aba3 --- /dev/null +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/GenericRecord.tsx @@ -0,0 +1,235 @@ +//****************************************************************************************************** +// GenericModel.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: +// ---------------------------------------------------------------------------------------------------- +// 09/09/2026 - Natalie Beatty +// Generated original version of source code. +// +//****************************************************************************************************** + +import * as React from 'react'; +import { GenericController, LoadingScreen, ServerErrorIcon, TabSelector, Warning, Modal } from '@gpa-gemstone/react-interactive' +import { useGetOne } from '../hooks'; +import { Application } from '@gpa-gemstone/application-typings'; +import { IError } from '@gpa-gemstone/common-pages/lib/Gemstone/GenericSlices/ReadOnlyGenericSlice'; + +declare var homePath: string; + +interface Error { + Message: string +} + +interface IProps { + /** + * ID of the record selected by the user. + */ + RecordID: string | number, + /** + * String representation of the record type for user-facing messages. + */ + RecordType: string, + /** + * Local storage key for storing the users' tab + */ + TabLocalStorage: string, + /** + * Default tab to use if no tab is stored in local storage + */ + DefaultTab: string, + /** + * API path for record patch, delete, or post functions. + */ + ControllerPath: string, + /** + * Key to sort by default + */ + DefaultSort: keyof T, + /** + * API path for + */ + GetOnePath: string, + /** + * URL to redirect the user to on record delete. + */ + Redirect: string, + /** + * Function for deriving the user-facing name of this record. + * @param record + * @returns + */ + GetName: (record: T) => string, + /** + * List of tabs to render with the record fetched in this component. + */ + Tabs: IRecordTab[], + /** + * Callback to determine whether or not user has permission to delete record. + */ + HasDeletePermission: () => boolean + /** + * Callback to determine whether or not user has patch permission for the record. + */ + HasPatchPermission: () => boolean + /** + * Initial tab to render, overriding locally stored tab. + */ + InitialTab?: string +} + +interface ITab { + Label: string, + Id: string +} + +interface IErrorMessage { + ExceptionMessage: string + ExceptionType: string +} + +export interface IRecordTab extends ITab { + Content: (record: T, setRecord: React.Dispatch>, patch: () => void, clearChanges: () => void, errors: string[], setErrors: React.Dispatch>, warnings: string[]) => React.ReactNode +} + +/** + * Generic Record keeps a copy of the database record to pass along to the tabs for user view and interaction. + * When changes are made to the record, that should happen in this component, so it can refresh and pass the updated database record into the tabs. + * Renders the tabs passed into it. + * This should also include other records? Like the AssetAttributes or the AdditionalFields? + * @param props + * @returns + */ +function GenericRecord({ + RecordID, + RecordType, + TabLocalStorage, + DefaultTab, + ControllerPath, + DefaultSort, + GetOnePath, + Redirect, + GetName, + Tabs, + HasDeletePermission, + HasPatchPermission, + InitialTab +}: IProps) { + const [tab, setTab] = React.useState(() => getTab(TabLocalStorage, DefaultTab, InitialTab)); + const [showWarning, setShowWarning] = React.useState(false); + const [refreshCount, refreshData] = React.useState(0); + const [selectedRecord, setSelectedRecord] = React.useState(null); + const { Data: originalRecord, Status: originalRecordStatus } = useGetOne(GetOnePath, RecordID, refreshCount); + const [deleteStatus, setDeleteStatus] = React.useState('idle'); + const [patchStatus, setPatchStatus] = React.useState('idle'); + const [errorMessage, setErrorMessage] = React.useState(null); + const [errors, setErrors] = React.useState([]); + + React.useEffect(() => { + setSelectedRecord(originalRecord); + }, [originalRecord]) + + const warnings: string[] = React.useMemo(() => { + const result: string[] = []; + if (selectedRecord == null) return + Object.keys(selectedRecord).forEach((key) => { + if (!(key in (selectedRecord as object))) return + const originalValue = originalRecord[key]; + const warning = `Changes to ${key} will be lost.`; + if (originalValue != selectedRecord[key]) result.push(warning); + }) + return result; + }, [originalRecord, selectedRecord]) + + const setAndSaveTab = React.useCallback((tabID: string) => { + const saved = getTab(TabLocalStorage, DefaultTab); + if (saved != tabID) sessionStorage.setItem(TabLocalStorage, JSON.stringify(tabID)); + setTab(tabID); + }, [TabLocalStorage, DefaultTab]) + + const clearChanges = React.useCallback(() => { + setSelectedRecord(originalRecord); + }, [originalRecord]) + + const patch = React.useCallback(() => { + if (!HasPatchPermission()) return + setPatchStatus('loading'); + new GenericController(ControllerPath, DefaultSort).DBAction("PATCH", selectedRecord) + .done(() => { setPatchStatus('idle'), refreshData(x => x + 1) }) + .fail((error) => { + setPatchStatus('error'); + const errorResponse: IErrorMessage = error.responseJSON; + setErrorMessage(errorResponse.ExceptionMessage); + }) + }, [ControllerPath, DefaultSort, selectedRecord]) + + const card = React.useMemo(() => { + if (selectedRecord == null) return null + const i = Tabs.findIndex(t => t.Id === tab) + if (i < 0) return null + return Tabs[i].Content(selectedRecord, setSelectedRecord, patch, clearChanges, errors, setErrors, warnings); + }, [tab, Tabs, selectedRecord]); + + function deleteRecord() { + if (!HasDeletePermission()) return + setDeleteStatus('loading'); + new GenericController(ControllerPath, DefaultSort).DBAction("DELETE", originalRecord) + .done(() => window.location.href = Redirect) + .fail((error) => { + setDeleteStatus('error'); + const errorResponse: IErrorMessage = error.responseJSON; + setErrorMessage(errorResponse.ExceptionMessage); + }) + } + + if (originalRecordStatus == 'uninitiated' || originalRecordStatus == 'loading') + return ; + + if (originalRecordStatus == 'error') + return ; + + if (originalRecord == null) + return null; + + return ( +
+
+
+

{GetName(originalRecord)}

+
+
+ +
+
+
+ + setAndSaveTab(t)} Tabs={Tabs} /> + {card} + { if (c) deleteRecord(); setShowWarning(false) }} /> + { setErrorMessage(null); setDeleteStatus('idle'); setPatchStatus('idle') }} Show={errorMessage != null} ShowConfirm={false} ShowCancel={false} ShowX={true}> +

{errorMessage}

+
+
+ ) +} + +export default GenericRecord; + +function getTab(storage: string, defaultTab: string, initialTab?: string): string { + if (initialTab != null) return initialTab; + const localTab = JSON.parse(sessionStorage.getItem(storage)); + if (localTab != null) return localTab; + return defaultTab; +} \ No newline at end of file diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/GenericRelation.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/GenericRelation.tsx new file mode 100644 index 000000000..af34668da --- /dev/null +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/CommonComponents/GenericRelation.tsx @@ -0,0 +1,339 @@ +//****************************************************************************************************** +// GenericRelation.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: +// ---------------------------------------------------------------------------------------------------- +// 09/16/2026 - Natalie Beatty +// Generated original version of source code. +// +//******************************************************************************************************s + +import * as React from 'react'; +import { GenericController, Search, LoadingScreen, ServerErrorIcon, Warning, Modal } from '@gpa-gemstone/react-interactive'; +import { Table, Column, Paging } from '@gpa-gemstone/react-table'; +import { usePagedSearch } from '../hooks'; +import { SystemCenter as SC } from '../global'; +import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; +import { ToolTip } from '@gpa-gemstone/react-forms'; + +interface U { + ID: number | string +} + +interface IProps { + /** + * User-facing name for the relation, i.e. Customer Asset or Remote XDA Meter + */ + RecordType: string + /** + * Controller for relation. + */ + Controller: GenericController + /** + * Default filters for the table. + */ + Filters?: Search.IFilter[] + /** + * Default sort field for the table. If not provided, uses the default sort property of the controller. + */ + DefaultSort?: keyof T + /** + * ParentID for the table. + */ + ParentID?: string | number + /** + * Relation table's columns, except for the edit and delete columns. + */ + Columns: SC.IByCol[] + /** + * If provided, will add an edit column to the table, which displays the ReactElement resulting from this function. + * + * @param record + * @param recordSetter + * @param setErrors + * @returns + */ + EditForm?: (record: T, recordSetter: (record: T) => void, setErrors: (errors: string[]) => void) => React.ReactElement; + /** + * Whether or not to include a delete column. + */ + DeleteColumn?: boolean + /** + * If provided, adds an "Add New" button to the relation, and uses this prop as the callback. + * @returns + */ + AddNew?: () => void + /** + * A callback to push search results up to parent component. + * @param results + * @returns + */ + SetSearchResults?: (results: T[]) => void + /** + * Function to use to determine whether or not a user may edit a relation. + * @param item + * @returns + */ + IsEditable: (item: T) => boolean + /** + * Function to derive the user-facing name of the relation from the relation's record. + * @param record + * @returns + */ + GetName: (record: T) => string + /** + * Refresh trigger for the table. + */ + RefreshCount?: number + /** + * Blank record containing default values for the form provided in "Edit Form." + */ + BlankRecord: T + /** + * Determines whether the user has permissions to edit or delete relations + */ + HasPermissions: () => boolean +} + +/** + * A tab for a System Center record for a record's relation (of type T) to other records. + * Includes a sortable and paged table in the tabbed-card layout, with settings for a button to add new relations, + * and also columns that allow for deleting or editing existing relations. + * For example, an Asset Group record might have relation tabs for Meters, Assets, and Asset Groups, + * each of which would feature a sortable and paged table of records in that group. + * If there are special attributes to the relation, or if the user simply wants to edit the records in this context, + * we can provide that functionality with the edit column. + * @param props + * @returns + */ +function GenericRelation({ + RecordType, + Controller, + Filters, + DefaultSort, + Columns, + EditForm, + DeleteColumn, + AddNew, + SetSearchResults, + IsEditable, + GetName, + RefreshCount, + BlankRecord, + ParentID, + HasPermissions +}: IProps) { + const [ascending, setAscending] = React.useState(Controller.Ascending); + const [sortField, setSortField] = React.useState(DefaultSort ?? Controller.DefaultSort); + const [page, setPage] = React.useState(0); + const [hover, setHover] = React.useState<"" | "submit">(""); + const [refreshCount, refreshData] = React.useState(0); + const [showDelete, setShowDelete] = React.useState(false); + const [showEdit, setShowEdit] = React.useState(false); + const [selectedRecord, setSelectedRecord] = React.useState(null); + const [editRecord, setEditRecord] = React.useState(BlankRecord); + const [editRecordErrors, setEditRecordErrors] = React.useState([]); + + const { Data: pagedData, Status: pagedStatus, RecordsPerPage: recordsPerPage, TotalPages: totalPages, TotalRecords: totalRecords } = usePagedSearch(Controller, Filters, page, sortField, ascending, ParentID, refreshCount); + + const getContent = React.useCallback((datum: SC.IColDatum, col: SC.IByCol) => { + if (col?.Content != null) return col.Content(datum); + else if (col.Type === 'boolean') return datum.item[datum.field] ? : <> + else return datum.item[datum.field] ?? <> + }, []); + + // needs to be a use effect? + React.useEffect(() => { + if (SetSearchResults != null) SetSearchResults(pagedData); + }, [pagedData, SetSearchResults]) + + // needs to be a use effect? + React.useEffect(() => { + refreshData(x => x + 1); + }, [RefreshCount]) + + let cardBody; + if (pagedStatus === 'error') { + cardBody = + } else if (pagedStatus === 'loading') { + cardBody = + } else { + cardBody = + <> +
+ + TableClass="table table-hover" + Data={pagedData} + SortKey={sortField.toString()} + Ascending={ascending} + OnSort={(d) => { + if (d.colKey == 'Edit' || d.colKey == 'Delete') return; + if (d.colKey === sortField) + setAscending(!ascending); + else { + setAscending(true); + setSortField(d.colField); + } + }} + TheadStyle={{ fontSize: 'smaller' }} + RowStyle={{ fontSize: 'smaller' }} + Selected={(item) => false} + KeySelector={(item) => item.ID} + > + { + ...Columns.map(col => ( + + Key={col.Field.toString()} + AllowSort={col.AllowSort} + Field={col.Field} + HeaderStyle={{ width: col?.Width ?? 'auto' }} + RowStyle={{ width: col?.Width ?? 'auto' }} + Content={datum => getContent(datum, col)} + >{col.Label} + + )) + } + {DeleteColumn ?? false ? + Key={'Delete'} + AllowSort={false} + HeaderStyle={{ width: '10%' }} + RowStyle={{ width: '10%' }} + Content={({ item }) => ( + (IsEditable != null ? IsEditable(item) : true) ? + : null) + } + >

+ : null} + {EditForm != null ? + Key={'Edit'} + AllowSort={false} + HeaderStyle={{ width: '10%' }} + RowStyle={{ width: '10%' }} + Content={({ item }) => (IsEditable(item) ? + : null) + } + >

+ : null } + +
+
+
+ setPage(p - 1)} + Total={totalPages} + /> +
+
+ + + return ( +
+
+
+
+

{RecordType}s:

+
+
+
+
+

+ {`Displaying ${RecordType}(s) ${totalRecords > 0 ? (recordsPerPage * page + 1) : 0} - ${recordsPerPage * page + pagedData.length} out of ${totalRecords}`} +

+
+
+ +
+ +
+ {cardBody} +
+ +
+ {AddNew != null ? + <> +
+ +
+ +

Your role does not have permission. Please contact your Administrator if you believe this to be in error.

+
+ + : null} +
+ {DeleteColumn ?? false ? + { + if (conf) Controller.DBAction('DELETE', selectedRecord).then(() => { refreshData(x => x+1)}); + setShowDelete(false); + }} /> : null} + {EditForm != null ? + { + if (conf) Controller.DBAction('PATCH', editRecord).then(() => refreshData(x => x + 1)); + setShowEdit(false); + }} + DisableConfirm={editRecordErrors.length > 0} + ShowX={true} + Size={"lg"} + ConfirmShowToolTip={editRecordErrors.length > 0} + ConfirmToolTipContent={ + editRecordErrors.map((t, i) =>

{t}

) + }> + {EditForm(selectedRecord, setEditRecord, setEditRecordErrors)} +
+ : null} +
+ ) + } +} + +export default GenericRelation; \ No newline at end of file diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteAssetTab.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteAssetTab.tsx index 99be7ec96..42463862a 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteAssetTab.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteAssetTab.tsx @@ -23,51 +23,41 @@ import * as React from 'react'; import * as _ from 'lodash'; -import { useAppDispatch, useAppSelector } from '../hooks'; -import { Table, Column, Paging } from '@gpa-gemstone/react-table'; -import { Application, OpenXDA, SystemCenter } from '@gpa-gemstone/application-typings'; -import { RemoteXDAAssetSlice, ByAssetSlice } from '../Store/Store'; -import { LoadingScreen, Modal, Search, ServerErrorIcon, Warning, GenericController } from '@gpa-gemstone/react-interactive'; -import { ToolTip } from '@gpa-gemstone/react-forms'; +import { OpenXDA, SystemCenter } from '@gpa-gemstone/application-typings'; +import { Search, GenericController } from '@gpa-gemstone/react-interactive'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; import { BlankRemoteXDAAsset, RemoteAssetForm } from './RemoteAssetForm'; import AssetSelect from '../Asset/AssetSelect'; +import GenericRelation from '../CommonComponents/GenericRelation'; +import { SystemCenter as SC } from '../global'; import { SelectRoles } from '../Store/UserSettings'; +import { useAppSelector } from '../hooks'; interface IProps { ID: number } +const RemoteXDAAssetController = new GenericController(`${homePath}api/OpenXDA/RemoteXDAAsset`, "LocalAssetName", false); -const RemoteAssetTab = (props: IProps) => { - // Display Remote Assets Consts - const [sortKey, setSortKey] = React.useState('LocalAssetName'); - const [ascending, setAscending] = React.useState(true); - const dispatch = useAppDispatch(); - const remoteAssetStatus = useAppSelector(RemoteXDAAssetSlice.Status); - const [searchResults, setSearchResults] = React.useState([]); - const [searchState, setSearchState] = React.useState('uninitiated'); - - 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 Columns: SC.IByCol[] = [ + { Field: "LocalAssetName", Label: "Local Name", Type: "string" }, + { Field: "LocalAssetKey", Label: "Local Key", Type: "string" }, + { Field: "RemoteAssetName", Label: "Remote Name", Type: "string" }, + { Field: "RemoteAssetKey", Label: "Remote Key", Type: "string" }, + { Field: "Obsfucate", Label: "Obfuscated", Type: "string", Content: ({ item }) => item.Obsfucate ? : null }, + { Field: "Synced", Label: "Synced", Type: "string", Content: ({ item }) => item.Synced ? : null } +] - // Edit and Delete Form Consts - const [newInstErrors, setNewInstErrors] = React.useState([]); - const [remoteAsset, setRemoteAsset] = React.useState(BlankRemoteXDAAsset); - const [selectedAsset, setSelectedAsset] = React.useState(BlankRemoteXDAAsset); - const [showEdit, setShowEdit] = React.useState<(boolean)>(false); - const [showDelete, setShowDelete] = React.useState<(boolean)>(false); - - // Add New Asset Consts - const assetStatus = useAppSelector(ByAssetSlice.Status) as Application.Types.Status; - const [assetList, setAssetList] = React.useState>([]); - const [showAddAssets, setShowAddAssets] = React.useState<(boolean)>(false); +const RemoteAssetTab = (props: IProps) => { + const [assetList, setAssetList] = React.useState([]); + const [showAddAssets, setShowAddAssets] = React.useState(false); + const [refreshCount, refreshData] = React.useState(0); const roles = useAppSelector(SelectRoles); - const [hover, setHover] = React.useState<('submit' | 'clear' | 'none')>('none'); + function hasPermissions(): boolean { + if (roles.indexOf('Administrator') < 0) + return false; + return true; + } - const remoteAssetController = React.useMemo(() => new GenericController(`${homePath}api/OpenXDA/RemoteXDAAsset`, "LocalAssetName", false), []) const filters: Search.IFilter[] = React.useMemo(() => [{ FieldName: 'RemoteXDAInstanceID', SearchText: props.ID.toString(), @@ -76,239 +66,22 @@ const RemoteAssetTab = (props: IProps) => { IsPivotColumn: false }], [props.ID]) - React.useEffect(() => { - if (remoteAssetStatus === 'uninitiated' || remoteAssetStatus === 'changed') - dispatch(RemoteXDAAssetSlice.Fetch()); - }, [dispatch, remoteAssetStatus]); - - React.useEffect(() => { - setSearchState('loading'); - const handle = remoteAssetController.PagedSearch(filters, sortKey, ascending, page); - handle.done((d) => { - setSearchResults(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)); - setSearchState('idle') - }) - handle.fail(() => setSearchState("error")); - return () => { - if (handle != null && handle.abort != null) handle.abort(); - } - }, [filters, sortKey, ascending, page, remoteAssetController, refreshTrigger]) - - React.useEffect(() => { - if (assetStatus === 'uninitiated' || assetStatus === 'changed') - dispatch(ByAssetSlice.Fetch()); - }, [dispatch, assetStatus]); - - function isEditable(item: OpenXDA.Types.RemoteXDAAsset): boolean { - return item.RemoteXDAAssetID <= 0; - } - - function hasPermissions(): boolean { - if (roles.indexOf('Administrator') < 0) - return false; - return true; - } - - let cardBody; - if (remoteAssetStatus === 'error') { - cardBody = - } else if (remoteAssetStatus === 'loading') { - cardBody = - } else { - cardBody = - <> -
- - TableClass="table table-hover" - Data={searchResults} - SortKey={sortKey} - Ascending={ascending} - OnSort={(d) => { - if (d.colKey == 'Edit' || d.colKey == 'Delete') return; - if (d.colKey === sortKey) - setAscending(!ascending); - else { - setAscending(true); - setSortKey(d.colField); - } - }} - TheadStyle={{ fontSize: 'smaller' }} - RowStyle={{ fontSize: 'smaller' }} - Selected={(item) => false} - KeySelector={(item) => item.ID} - > - - Key={'LocalAssetName'} - AllowSort={true} - Field={'LocalAssetName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Local Name - - - Key={'LocalAssetKey'} - AllowSort={true} - Field={'LocalAssetKey'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Local Key - - - Key={'RemoteAssetName'} - AllowSort={true} - Field={'RemoteAssetName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Remote Name - - - Key={'RemoteAssetKey'} - AllowSort={true} - Field={'RemoteAssetKey'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Remote Key - - - Key={'Obsfucate'} - AllowSort={true} - Field={'Obsfucate'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => item.Obsfucate ? : null } - > Obfuscated - - - Key={'Synced'} - AllowSort={true} - Field={'Synced'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => item.Synced ? : null} - > Synced - - - Key={'Edit'} - AllowSort={false} - HeaderStyle={{ width: '10%' }} - RowStyle={{ width: '10%' }} - Content={({ item }) => (isEditable(item) ? - : null) - } - >

- - - Key={'Delete'} - AllowSort={false} - HeaderStyle={{ width: '10%' }} - RowStyle={{ width: '10%' }} - Content={({ item }) => (isEditable(item) ? - : null) - } - >

- - -
-
-
- setPage(p - 1)} - Total={totalPages} - /> -
-
- - } - return ( -
-
-
-
-

Remote openXDA Assets:

-
-
-
-
-

- {searchState === 'error' ? 'Could not complete Search' : - searchState === 'loading' ? 'Loading...' : - `Displaying Asset(s) ${totalRecords > 0 ? (recordsPerPage * page + 1) : 0} - ${recordsPerPage * page + searchResults.length} out of ${totalRecords}`} -

-
-
-
-
- {cardBody} -
-
-
- -
- -

Your role does not have permission. Please contact your Administrator if you believe this to be in error.

-
-
- { - if (conf) remoteAssetController.DBAction('DELETE',selectedAsset).then(() => setRefreshTrigger(val => !val)); - setShowDelete(false); - }} /> - { - if (conf) remoteAssetController.DBAction('PATCH', remoteAsset).then(() => setRefreshTrigger(val => !val)); - setShowEdit(false); - }} - DisableConfirm={newInstErrors.length > 0} - ShowX={true} - Size={"lg"} - ConfirmShowToolTip={newInstErrors.length > 0} - ConfirmToolTipContent={ - newInstErrors.map((t, i) =>

{t}

) - }> - -
+ <> + + Controller={RemoteXDAAssetController} + RecordType={'Remote XDA Asset'} + Columns={Columns} + Filters={filters} + IsEditable={(item) => item.RemoteXDAAssetID <= 0} + DeleteColumn={true} + GetName={(record) => record.LocalAssetName} + BlankRecord={BlankRemoteXDAAsset} + RefreshCount={refreshCount} + AddNew={() => setShowAddAssets(true)} + EditForm={(record, setter, setErrors) => } + HasPermissions={hasPermissions} + /> { @@ -330,13 +103,12 @@ const RemoteAssetTab = (props: IProps) => { RemoteAssetName: "", RemoteAssetKey: "" } - remoteAssetController.DBAction("POST", newRemote).then(() => setRefreshTrigger(val => !val)); + RemoteXDAAssetController.DBAction("POST", newRemote).then(() => refreshData(x => x+1)); }); }} /> -
- ); - - + + ) + } export default RemoteAssetTab; \ No newline at end of file diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteMeterTab.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteMeterTab.tsx index 3f8ff23b3..0dc38c01a 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteMeterTab.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteMeterTab.tsx @@ -23,102 +23,73 @@ import * as React from 'react'; import * as _ from 'lodash'; -import { useAppDispatch, useAppSelector } from '../hooks'; -import { Table, Column, Paging } from '@gpa-gemstone/react-table'; -import { SystemCenter, Application, OpenXDA } from '@gpa-gemstone/application-typings'; -import { RemoteXDAMeterSlice, ByMeterSlice } from '../Store/Store'; -import { LoadingScreen, Modal, Search, ServerErrorIcon, Warning, GenericController } from '@gpa-gemstone/react-interactive'; -import { ToolTip } from '@gpa-gemstone/react-forms'; +import { SystemCenter, OpenXDA } from '@gpa-gemstone/application-typings'; +import { Column } from '@gpa-gemstone/react-table' +import { Modal, Search, GenericController, SearchBar, LoadingScreen } from '@gpa-gemstone/react-interactive'; import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; import { BlankRemoteXDAMeter, RemoteMeterForm } from './RemoteMeterForm'; -import { DefaultSelects } from '@gpa-gemstone/common-pages'; +import GenericRelation from '../CommonComponents/GenericRelation'; +import { SystemCenter as SC } from '../global'; +import ControllerSelectPopup from '../CommonComponents/ControllerSelectPopup' import { SelectRoles } from '../Store/UserSettings'; +import { useAppSelector } from '../hooks'; + interface IProps { ID: number } +const RemoteMeterController = new GenericController(`${homePath}api/OpenXDA/RemoteXDAMeter`, "LocalMeterName", false); +const MeterController = new GenericController(`${homePath}api/OpenXDA/ByMeter`, "Name"); + +const columns: SC.IByCol[] = [ + { Label: "Local Name", Field: "LocalMeterName", Type: "string" }, + { Label: "Local Key", Field: "LocalAssetKey", Type: "string" }, + { Label: "Local Alias", Field: "LocalAlias", Type: "string" }, + { Label: "Remote XDA Name", Field: "RemoteXDAName", Type: "string", Content: ({ item }) => item.Obsfucate ? item.RemoteXDAName : item.LocalMeterName }, + { Label: "Remote Key", Field: "RemoteXDAAssetKey", Type: "string" }, + { Label: "Remote Alias", Field: "RemoteAlias", Type: "string" }, + { Label: "Obfuscated", Field: "Obsfucate", Type: "boolean", Content: ({ item }) => item.Obsfucate ? : null }, + { Label: "Synced", Field: "Synced", Type: "boolean", Content: ({ item }) => item.Synced ? : null } +] + +const defaultSearchcols: Search.IField[] = [ + { label: 'Key', key: 'AssetKey', type: 'string', isPivotField: false }, + { label: 'Name', key: 'Name', type: 'string', isPivotField: false }, + { label: 'Substation Name', key: 'Location', type: 'string', isPivotField: false }, + { label: 'Make', key: 'Make', type: 'string', isPivotField: false }, + { label: 'Model', key: 'Model', type: 'string', isPivotField: false }, + { label: 'Number of Assets', key: 'MappedAssets', type: 'number', isPivotField: false }, + { label: 'Description', key: 'Description', type: 'string', isPivotField: false }, +]; const RemoteMeterTab = (props: IProps) => { - // Display Remote Meters Consts - const [sortKey, setSortKey] = React.useState('LocalMeterName'); - const [ascending, setAscending] = React.useState(true); - const dispatch = useAppDispatch(); - const remoteMeterStatus = useAppSelector(RemoteXDAMeterSlice.Status) as Application.Types.Status; - const [searchResults, setSearchResults] = React.useState([]); - const [searchState, setSearchState] = React.useState('uninitiated'); - - const noSameFilter: Search.IFilter = { - FieldName: 'ID', - SearchText: searchResults.map((r) => r.LocalXDAMeterID).join(','), - Operator: 'NOT IN', - Type: 'number', - IsPivotColumn: false - }; - - 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); - - // Shared Consts - const [selectedMeter, setSelectedMeter] = React.useState(BlankRemoteXDAMeter); - - // Edit and Delete Form Consts - const [newInstErrors, setNewInstErrors] = React.useState([]); - const [remoteMeter, setRemoteMeter] = React.useState(BlankRemoteXDAMeter); - const [showEdit, setShowEdit] = React.useState<(boolean)>(false); - const [showDelete, setShowDelete] = React.useState<(boolean)>(false); - - // Add New Meter Consts - const meterStatus = useAppSelector(ByMeterSlice.Status) as Application.Types.Status; - const [meterList, setMeterList] = React.useState>([]); const [showAddMeters, setShowAddMeters] = React.useState<(boolean)>(false); - - // Add New Assets for Meter - const [showLoading, setShowLoading] = React.useState<(boolean)>(false); + const [selectedMeter, setSelectedMeter] = React.useState(null) + const [meterList, setMeterList] = React.useState([]); + const [refreshCount, refreshData] = React.useState(0); + const [searchResults, setSearchResults] = React.useState([]); const [assetCount, setAssetCount] = React.useState(0); + const [showLoading, setShowLoading] = React.useState(false); const roles = useAppSelector(SelectRoles); - const [hover, setHover] = React.useState<('submit' | 'clear' | 'none')>('none'); - - const remoteMeterController = React.useMemo(() => new GenericController(`${homePath}api/OpenXDA/RemoteXDAMeter`, "LocalMeterName", false), []) - const searchFilters: Search.IFilter[] = React.useMemo(() => [{ FieldName: 'RemoteXDAInstanceID', SearchText: props.ID.toString(), Operator: '=', Type: 'number', IsPivotColumn: false }],[props.ID]) - - React.useEffect(() => { - if (remoteMeterStatus === 'uninitiated' || remoteMeterStatus === 'changed') - dispatch(RemoteXDAMeterSlice.Fetch()); - }, [dispatch, remoteMeterStatus]); - React.useEffect(() => { - if (meterStatus === 'uninitiated' || meterStatus === 'changed') - dispatch(ByMeterSlice.Fetch()); - }, [dispatch, meterStatus]); - - React.useEffect(() => { - setSearchState('loading') - const handle = remoteMeterController.PagedSearch(searchFilters, sortKey, ascending, page); - - handle.done((d) => { - setSearchResults(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)); - setSearchState('idle') - }) + function hasPermissions(): boolean { + if (roles.indexOf('Administrator') < 0) + return false; + return true; + } - handle.fail(() => setSearchState('error')) + const searchFilters: Search.IFilter[] = React.useMemo(() => [{ FieldName: 'RemoteXDAInstanceID', SearchText: props.ID.toString(), Operator: '=', Type: 'number', IsPivotColumn: false }], [props.ID]) - return () => { - if (handle != null && handle.abort != null) handle.abort(); + const noSameFilter: Search.IFilter = React.useMemo(() => { + const filter = { + FieldName: 'ID', + SearchText: searchResults.map((r) => r.LocalXDAMeterID).join(','), + Operator: 'NOT IN' as Search.OperatorType, + Type: 'string' as Search.FieldType, + IsPivotColumn: false } - - }, [ascending, sortKey, remoteMeterController, searchFilters, refreshTrigger]) - - function isEditable(item: OpenXDA.Types.RemoteXDAMeter): boolean { - return item.RemoteXDAMeterID <= 0; - } + return filter; + }, [searchResults]); function getAssociatedAssetCount(meter: OpenXDA.Types.RemoteXDAMeter): JQuery.jqXHR { return $.ajax({ @@ -130,7 +101,6 @@ const RemoteMeterTab = (props: IProps) => { async: true }); } - function addAssociatedAssets(meter: OpenXDA.Types.RemoteXDAMeter): JQuery.jqXHR { return $.ajax({ type: "GET", @@ -142,302 +112,108 @@ const RemoteMeterTab = (props: IProps) => { }); } - function hasPermissions(): boolean { - if (roles.indexOf('Administrator') < 0) - return false; - return true; - } - - let cardBody; - if (remoteMeterStatus === 'error') { - cardBody = - } else if (remoteMeterStatus === 'loading' || showLoading) { - cardBody = - } else { - cardBody = - <> -
- - TableClass="table table-hover" - Data={searchResults} - SortKey={sortKey} - Ascending={ascending} - OnSort={(d) => { - if (d.colKey == 'Edit' || d.colKey == 'Delete') return; - if (d.colKey === sortKey) - setAscending(!ascending); - else { - setAscending(true); - setSortKey(d.colField); - } - }} - TheadStyle={{ fontSize: 'smaller' }} - RowStyle={{ fontSize: 'smaller' }} - Selected={(item) => false} - KeySelector={(item) => item.ID} - > - - Key={'LocalMeterName'} - AllowSort={true} - Field={'LocalMeterName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Local Name - - - Key={'LocalAssetKey'} - AllowSort={true} - Field={'LocalAssetKey'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Local Key - - - Key={'LocalAlias'} - AllowSort={true} - Field={'LocalAlias'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Local Alias - - - Key={'RemoteXDAName'} - AllowSort={true} - Field={'RemoteXDAName'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => item.Obsfucate ? item.RemoteXDAName : item.LocalMeterName} - > Remote Name - - - Key={'RemoteXDAAssetKey'} - AllowSort={true} - Field={'RemoteXDAAssetKey'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Remote Key - - - Key={'RemoteAlias'} - AllowSort={true} - Field={'RemoteAlias'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - > Remote Alias - - - Key={'Obsfucate'} - AllowSort={true} - Field={'Obsfucate'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => item.Obsfucate ? : null } - > Obfuscated - - - Key={'Synced'} - AllowSort={true} - Field={'Synced'} - HeaderStyle={{ width: 'auto' }} - RowStyle={{ width: 'auto' }} - Content={({ item }) => item.Synced ? : null } - > Synced - - - Key={'Edit'} - AllowSort={false} - HeaderStyle={{ width: '10%' }} - RowStyle={{ width: '10%' }} - Content={({ item }) => (isEditable(item) ? - : null) - } - >

- - - Key={'Delete'} - AllowSort={false} - HeaderStyle={{ width: '10%' }} - RowStyle={{ width: '10%' }} - Content={({ item }) => (isEditable(item) ? - : null) - } - >

- - -
-
-
- setPage(p - 1)} - Total={totalPages} - /> -
-
- - } - - return ( -
-
-
-
-

Remote openXDA Meters:

-
-
-
-
-

- {searchState === 'error' ? 'Could not complete Search' : - searchState === 'loading' ? 'Loading...' : - `Displaying Meter(s) ${totalRecords > 0 ? (recordsPerPage * page + 1) : 0} - ${recordsPerPage * page + searchResults.length} out of ${totalRecords}`} -

-
-
- -
-
- {cardBody} -
-
-
- -
- -

Your role does not have permission. Please contact your Administrator if you believe this to be in error.

-
-
- { - if (conf) remoteMeterController.DBAction('DELETE',selectedMeter).then(() => setRefreshTrigger(val => !val)); - setShowDelete(false); - }}/> - { - if (conf) remoteMeterController.DBAction('PATCH', remoteMeter).then(() => setRefreshTrigger(val => !val)); - setShowEdit(false); - }} - DisableConfirm={newInstErrors.length > 0} - ShowX={true} - Size={"lg"} - ConfirmShowToolTip={newInstErrors.length > 0} - ConfirmToolTipContent={ - newInstErrors.map((t, i) =>

{t}

) - }> - -
- 0} Title={'Add Associated Remote Assets?'} - ShowCancel={true} - CallBack={(conf) => { - setAssetCount(0); - if (conf) { - let addAssetHandle = addAssociatedAssets(selectedMeter); - addAssetHandle.then(() => setRefreshTrigger(val => !val)); - return () => { - if (addAssetHandle != null && addAssetHandle.abort != null) { - addAssetHandle.abort(); - } - }; - - } - }} - ShowX={true} Size={"sm"} - ConfirmText={"Yes"} - CancelText={"No"}> -

Add { assetCount } Associated Assets?

-
- { - setShowAddMeters(false); - setMeterList([]); - if (!conf) return; - selected.forEach((meter) => { - setShowLoading(true); - let newRemote: OpenXDA.Types.RemoteXDAMeter = { - ID: -1, - RemoteXDAInstanceID: props.ID, - LocalXDAMeterID: meter.ID, - RemoteXDAMeterID: -1, - RemoteXDAName: "", - RemoteXDAAssetKey: meter.AssetKey, - Obsfucate: false, - Synced: false, - LocalAlias: "", - LocalMeterName: "", - LocalAssetKey: "", - RemoteAlias: "" + return (<> + + + RecordType={"Remote XDA Meter"} + Controller={RemoteMeterController} + Columns={columns} + DefaultSort={'LocalMeterName'} + AddNew={() => { setShowAddMeters(true) }} + SetSearchResults={setSearchResults} + EditForm={(record, setter, setErrors) => { return }} + DeleteColumn={true} + IsEditable={(item) => item.RemoteXDAMeterID <= 0} + Filters={searchFilters} + GetName={(record) => record.LocalMeterName} + RefreshCount={refreshCount} + BlankRecord={BlankRemoteXDAMeter} + HasPermissions={hasPermissions} + /> + 0} Title={'Add Associated Remote Assets?'} + ShowCancel={true} + CallBack={(conf) => { + setAssetCount(0); + if (conf) { + let addAssetHandle = addAssociatedAssets(selectedMeter); + addAssetHandle.then(() => refreshData(x => x + 1)); + return () => { + if (addAssetHandle != null && addAssetHandle.abort != null) { + addAssetHandle.abort(); } - remoteMeterController.DBAction("POST", newRemote); - setSelectedMeter(newRemote); // Technically, this is a race condition with setAssetCount - let fetchAssetHandle = getAssociatedAssetCount(newRemote); - fetchAssetHandle.then((data: number) => { - setRefreshTrigger(val => !val); - setAssetCount(data); - setShowLoading(false); - }); - return () => { - if (fetchAssetHandle != null && fetchAssetHandle.abort != null) { - fetchAssetHandle.abort(); - setShowLoading(false); - } - }; + }; + + } + }} + ShowX={true} Size={"sm"} + ConfirmText={"Yes"} + CancelText={"No"}> +

Add {assetCount} Associated Assets?

+
+ + Controller={MeterController} + Searchbar={(children, setFilters) => ( + + CollumnList={defaultSearchcols} + SetFilter={setFilters} + Direction={'left'} + defaultCollumn={{ label: 'Name', key: 'Name', type: 'string', isPivotField: false }} + Width={'50%'} + Label={'Search'} + > + {children} + )} + Selection={meterList} + OnClose={(selected, conf) => { + setShowAddMeters(false); + setMeterList([]); + if (!conf) return; + selected.forEach((meter) => { + setShowLoading(true); + let newRemote: OpenXDA.Types.RemoteXDAMeter = { + ID: -1, + RemoteXDAInstanceID: props.ID, + LocalXDAMeterID: meter.ID, + RemoteXDAMeterID: -1, + RemoteXDAName: "", + RemoteXDAAssetKey: meter.AssetKey, + Obsfucate: false, + Synced: false, + LocalAlias: "", + LocalMeterName: "", + LocalAssetKey: "", + RemoteAlias: "" + } + RemoteMeterController.DBAction("POST", newRemote); + setSelectedMeter(newRemote); // Technically, this is a race condition with setAssetCount + let fetchAssetHandle = getAssociatedAssetCount(newRemote); + fetchAssetHandle.then((data: number) => { + refreshData(x => x + 1); + setAssetCount(data); + setShowLoading(false); }); - }} - Show={showAddMeters} - Type={'single'} - Title={"Add Meter to Remote openXDA Instance:"} - GetEnum={() => () => { }} - GetAddlFields={() => () => { }} - AddlFilters={[noSameFilter]} - > - Name - Key - Substation -
-
- ); - - + return () => { + if (fetchAssetHandle != null && fetchAssetHandle.abort != null) { + fetchAssetHandle.abort(); + setShowLoading(false); + } + }; + }); + }} + Show={showAddMeters} + Type={'single'} + Title={"Add Meter to Remote openXDA Instance:"} + Filters={[noSameFilter]} + > + Name + Key + Substation + + + ) } export default RemoteMeterTab; \ No newline at end of file diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteXDAInstance.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteXDAInstance.tsx index 7a6fce3b5..4ef0e251a 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteXDAInstance.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteXDAInstance.tsx @@ -23,85 +23,39 @@ import * as React from 'react'; import * as _ from 'lodash'; -import { LoadingScreen, TabSelector, Warning } from '@gpa-gemstone/react-interactive'; -import { useAppDispatch, useAppSelector } from '../hooks'; -import { RemoteXDAInstanceSlice } from '../Store/Store'; import { Application, OpenXDA } from '@gpa-gemstone/application-typings'; import SystemSettingsTab from './SystemSettingsTab' import RemoteAssetTab from './RemoteAssetTab' import RemoteMeterTab from './RemoteMeterTab'; +import GenericRecord, { IRecordTab } from '../CommonComponents/GenericRecord'; declare var homePath: string; declare type Tab = 'systemSettings' | 'remoteMeter' | 'remoteAsset' interface IProps { Roles: Array, ID: number, Tab: Tab } -function RemoteXDAInstance(props: IProps) { - const dispatch = useAppDispatch(); - - const instStatus = useAppSelector(RemoteXDAInstanceSlice.Status) as Application.Types.Status; - const connection = useAppSelector((state) => RemoteXDAInstanceSlice.Datum(state, props.ID)); - - const [tab, setTab] = React.useState(getTab()); - const [loading, setLoading] = React.useState(false); - const [showDelete, setShowDelete] = React.useState(false); - - - React.useEffect(() => { - if (instStatus === 'uninitiated' || instStatus === 'changed') - dispatch(RemoteXDAInstanceSlice.Fetch()); - }, [dispatch, instStatus]); - - React.useEffect(() => { - const saved = getTab(); - if (saved !== tab) - sessionStorage.setItem('RemoteXDAInstance.Tab', JSON.stringify(tab)); - }, [tab]); - - function getTab(): Tab { - if (props.Tab != undefined) return props.Tab; - else if (sessionStorage.hasOwnProperty('RemoteXDAInstance.Tab')) - return JSON.parse(sessionStorage.getItem('RemoteXDAInstance.Tab')); - else - return 'systemSettings'; - } - - function deleteConnection(): void { - setLoading(true); - if (props.Roles.includes('Administrator')) - dispatch(RemoteXDAInstanceSlice.DBAction({ verb: 'DELETE', record: connection })); - setLoading(false); - window.location.href = homePath + 'index.cshtml?name=RemoteXDAInstanceMain'; - }; - - const Tabs = [ - { Id: "systemSettings", Label: "System Settings" }, - { Id: "remoteMeter", Label: "Remote Meters" }, - { Id: "remoteAsset", Label: "Remote Assets" }, - ]; - - if (connection == null) return null; +const tabs: IRecordTab[] = [ + { Id: 'systemSettings', Label: "System Settings", Content: (record, setter, patch, clear, errors, setErrors, warnings) => }, + { Id: "remoteMeter", Label: "Remote Meter", Content: (rec) => }, + { Id: "remoteAsset", Label: "Remote Asset", Content: (rec) => } +] +function RemoteXDAInstance({Roles, ID, Tab }: IProps) { return ( -
-
-
-

{connection.Name}

-
-
- -
-
-
- setTab(t)} Tabs={Tabs} /> - {tab === 'systemSettings' ? : null} - {tab === 'remoteMeter' ? : null} - {tab === 'remoteAsset' ? : null} - - { if (conf) deleteConnection(); setShowDelete(false); }} /> - -
+ + RecordID={ID} + RecordType={"Remote XDA Instance"} + TabLocalStorage={"RemoteXDAInstance.Tab"} + ControllerPath={`${homePath}api/OpenXDA/remoteXDAInstance`} + DefaultSort={"Name"} + GetOnePath={`${homePath}api/OpenXDA/remoteXDAInstance/One/`} + Redirect={`${homePath}index.cshtml?name=RemoteXDAInstanceMain`} + DefaultTab={"systemSettings"} + GetName={(record) => record.Name} + Tabs={tabs} + HasDeletePermission={() => Roles.includes('Administrator')} + HasPatchPermission={() => Roles.includes('Administrator')} + /> ) } - export default RemoteXDAInstance; \ No newline at end of file diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteXDAInstanceForm.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteXDAInstanceForm.tsx index 4f179b137..9120f6f1f 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteXDAInstanceForm.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/RemoteXDAInstanceForm.tsx @@ -45,7 +45,8 @@ const BlankRemoteXDAInstance: OpenXDA.Types.RemoteXDAInstance = { interface IProps { BaseInstance: OpenXDA.Types.RemoteXDAInstance, SetInstance: (instance: OpenXDA.Types.RemoteXDAInstance) => void, - SetErrors?: (e: string[]) => void, RenderPortalId?: string + SetErrors?: (e: string[]) => void, + RenderPortalId?: string } export default function RemoteXDAInstanceForm(props: IProps) { diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/SystemSettingsTab.tsx b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/SystemSettingsTab.tsx index 4589519ab..e63619329 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/SystemSettingsTab.tsx +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/RemoteXDA/SystemSettingsTab.tsx @@ -23,113 +23,35 @@ import * as React from 'react'; import * as _ from 'lodash'; -import { useAppDispatch, useAppSelector } from '../hooks'; -import { Application, OpenXDA } from '@gpa-gemstone/application-typings'; -import { RemoteXDAInstanceSlice } from '../Store/Store'; -import { LoadingScreen, ServerErrorIcon } from '@gpa-gemstone/react-interactive'; -import { ToolTip } from '@gpa-gemstone/react-forms'; -import { ReactIcons } from '@gpa-gemstone/gpa-symbols'; -import { RemoteXDAInstanceForm} from './RemoteXDAInstanceForm'; - -interface IProps { ID: number } - +import { OpenXDA } from '@gpa-gemstone/application-typings'; +import { RemoteXDAInstanceForm } from './RemoteXDAInstanceForm'; +import GenericInfo from '../CommonComponents/GenericInfo'; + +interface IProps { + SelectedRecord: OpenXDA.Types.RemoteXDAInstance + SetSelectedRecord: React.Dispatch> + Errors: string[] + SetErrors: React.Dispatch> + Warnings: string[] + ClearChanges: () => void + Patch: () => void +} const SystemSettingsTab = (props: IProps) => { - const [hover, setHover] = React.useState<('submit' | 'clear' | 'none')>('none'); - - const dispatch = useAppDispatch(); - const instStatus = useAppSelector(RemoteXDAInstanceSlice.Status) as Application.Types.Status; - const connection = useAppSelector((state) => RemoteXDAInstanceSlice.Datum(state, props.ID)); - - const [newInstErrors, setNewInstErrors] = React.useState([]); - const [baseInstance, setBaseInstance] = React.useState(connection); - const [formInstance, setFormInstance] = React.useState(connection); - - - React.useEffect(() => { - if (instStatus === 'uninitiated' || instStatus === 'changed') - dispatch(RemoteXDAInstanceSlice.Fetch()); - }, [dispatch, instStatus]); - - React.useEffect(() => { - setFormInstance(connection); - setBaseInstance(connection); - }, [connection]); - - if (connection == null) - return null; - - let cardBody; - if (instStatus === 'error') { - cardBody = - } else if (instStatus === 'loading') { - cardBody = - } else { - cardBody = - } - return ( -
-
-
-
-

Remote openXDA Instance Connection Information:

-
-
-
-
- {cardBody} -
-
-
- -
- - {newInstErrors.map((t, i) => -

{t}

- )} -
-
- -
- - - {baseInstance.Name != formInstance.Name ?

Changes to Name will be discarded.

: null} - {baseInstance.Address != formInstance.Address ?

Changes to Address will be discarded.

: null} - {baseInstance.Frequency != formInstance.Frequency ?

Changes to Frequency will be discarded.

: null} -
-
- -
- ); - - + + Forms={[(record, setter, setErrors) => ]} + HasPermissions={() => true} + RecordType={'Remote XDA Instance'} + SelectedRecord={props.SelectedRecord} + SetSelectedRecord={props.SetSelectedRecord} + Errors={props.Errors} + SetErrors={props.SetErrors} + Warnings={props.Warnings} + ClearChanges={props.ClearChanges} + Patch={props.Patch} + /> + ) } export default SystemSettingsTab; \ No newline at end of file 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 3bc909fdd..9f19b642b 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/Store.ts +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/Store/Store.ts @@ -31,7 +31,6 @@ 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'; @@ -80,10 +79,6 @@ export const ByAssetSlice = new GenericSlice(" export const ByLocationSlice = new GenericSlice("ByLocation", `${homePath}api/OpenXDA/ByLocation`, "Name", true); export const ByMeterSlice = new GenericSlice("ByMeter", `${homePath}api/OpenXDA/ByMeter`, "Name", true); -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 AssetNoteSlice = new NoteSlice('Asset'); export const MeterNoteSlice = new NoteSlice('Meter'); export const UserNoteSlice = new NoteSlice('User'); @@ -142,9 +137,6 @@ const store = configureStore({ EventType: EventTypeSlice.Reducer, MeasurementCharacteristic: MeasurementCharacteristicSlice.Reducer, ExternalDataBaseTable: ExternalDBTablesSlice.Reducer, - remoteXDAInstance: RemoteXDAInstanceSlice.Reducer, - RemoteXDAAsset: RemoteXDAAssetSlice.Reducer, - RemoteXDAMeter: RemoteXDAMeterSlice.Reducer, ApplicationNode: ApplicationNodeSlice.Reducer, PQApplicationCategory: ApplicationCategorySlice.Reducer, PQApplications: PQApplicationsSlice.Reducer, diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/global.d.ts b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/global.d.ts index 7d4ce734f..fd9af7fea 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/global.d.ts +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/global.d.ts @@ -95,6 +95,7 @@ export namespace SystemCenter { PercentFinished: number RecordsAffected: number } + } // OpenXDA Models diff --git a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/hooks.ts b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/hooks.ts index a6c96a4bd..dce201085 100644 --- a/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/hooks.ts +++ b/Source/Applications/SystemCenter/wwwroot/Scripts/TSX/SystemCenter/hooks.ts @@ -24,13 +24,19 @@ import * as React from 'react'; import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; import { Application } from '@gpa-gemstone/application-typings'; -import { GenericController } from '@gpa-gemstone/react-interactive'; +import { GenericController, Search } from '@gpa-gemstone/react-interactive'; import { AppDispatch, RootState } from './Store/Store'; - export const useAppDispatch: () => AppDispatch = useDispatch; export const useAppSelector: TypedUseSelectorHook = useSelector; +interface IPagedResult { + Data: T[], + NumberOfPages: number, + TotalRecords: number, + RecordsPerPage: number +} + export const useBoundPaging = (currentPage: number, totalPages: number, setPage: (number) => void): void => { React.useEffect(() => { if (currentPage >= totalPages && totalPages > 0) { @@ -43,7 +49,47 @@ export const useBoundPaging = (currentPage: number, totalPages: number, setPage: } -export function useControllerFetch(controller: GenericController, sortField?: keyof T, ascending?: boolean, parentID?: string|number, refreshCount?: number,) { +export function usePagedSearch(controller: GenericController, filters: Search.IFilter[], page: number, sortField?: keyof T, ascending?: boolean, parentID?: string | number, refreshCount?: number) { + + const [pagedData, setPagedData] = React.useState([]); + const [pagedStatus, setPagedStatus] = React.useState('uninitiated'); + const [totalPages, setTotalPages] = React.useState(0); + const [totalRecords, setTotalRecords] = React.useState(0); + const [recordsPerPage, setRecordsPerPage] = React.useState(0); + const sortKey = sortField ?? controller.DefaultSort; + const asc = ascending ?? controller.Ascending; + + const refetchData = React.useCallback(() => { + setPagedStatus('loading'); + + const handle = controller.PagedSearch(filters, sortKey, asc, page, parentID) + .done((data: IPagedResult) => { + setPagedData(JSON.parse(data.Data as unknown as string)); + setTotalPages(data.NumberOfPages); + setTotalRecords(data.TotalRecords); + setRecordsPerPage(data.RecordsPerPage); + setPagedStatus('idle'); + }) + .fail(() => setPagedStatus('error')); + + return () => { if (handle != null && handle.abort != null) handle.abort() } + + }, [controller, filters, sortKey, asc, page, parentID]) + + React.useEffect(() => { + return refetchData(); + }, [refetchData, refreshCount]) + + return { + Data: pagedData, + Status: pagedStatus, + TotalPages: totalPages, + TotalRecords: totalRecords, + RecordsPerPage: recordsPerPage + } +} + +export function useControllerFetch(controller: GenericController, sortField?: keyof T, ascending?: boolean, parentID?: string|number, refreshCount?: number) { const fetchHandle = React.useRef | null>(null); const [fetchData, setFetchData] = React.useState([]); @@ -75,4 +121,41 @@ export function useControllerFetch(controller: GenericController, sortFie Data: fetchData, Status: fetchStatus } -} \ No newline at end of file +} + +export function useGetOne(apiPath: string, recordID: string|number, refreshCount?: number) { + const [record, setRecord] = React.useState(null); + const [recordStatus, setRecordStatus] = React.useState('uninitiated'); + + const refetchData = React.useCallback(() => { + setRecordStatus('loading'); + + const handle = $.ajax({ + type: "GET", + url: apiPath + recordID.toString(), + contentType: "application/json; charset=utf-8", + dataType: 'json', + cache: true, + async: true + }) + .done((record: T) => { + setRecord(record); + setRecordStatus('idle'); + }) + .fail(() => setRecordStatus('error')) + + return () => { + if (handle != null && handle.abort != null) handle.abort(); + } + + }, [apiPath, recordID]) + + React.useEffect(() => { + return refetchData(); + }, [refetchData, refreshCount]) + + return { + Data: record, + Status: recordStatus + } +} \ No newline at end of file