diff --git a/src/components/Select/index.tsx b/src/components/Select/index.tsx index 2f94d86ad..29c3dedcd 100644 --- a/src/components/Select/index.tsx +++ b/src/components/Select/index.tsx @@ -32,6 +32,7 @@ export type SelectProps = { isVoided?: boolean; maxHeight?: string; title?: string; + style?: React.CSSProperties; }; const KEYCODE_ENTER = 13; @@ -48,6 +49,7 @@ const Select = ({ isVoided = false, maxHeight, title, + style, }: SelectProps): JSX.Element => { const [isOpen, setIsOpen] = useState(false); const containerRef = useRef(null); @@ -148,7 +150,7 @@ const Select = ({ }; return ( - + void; hasUnsavedChanges?: boolean; unsavedChangesConfirmationMessage?: string; + selectedLibraryItem: string; } const TreeView = ({ rootNodes, dirtyNodeId, resetDirtyNode, - hasUnsavedChanges = false, - unsavedChangesConfirmationMessage = 'You have unsaved changes. Are you sure you want to continue?', + selectedLibraryItem, }: TreeViewProps): JSX.Element => { const [treeData, setTreeData] = useState(rootNodes); const [loading, setLoading] = useState(); @@ -56,6 +57,173 @@ const TreeView = ({ const [isNodeExpanded, setIsNodeExpanded] = useState(false); const [executionCount, setExecutionCount] = useState(0); const { pathname } = useLocation(); + const navigate = useNavigate(); + const { newJourney } = useLibraryContext(); + const { saveTriggered, setSaveTriggered } = useLibraryContext(); + const previousProjectId = useRef(null); + + // ----------------------------------------------------------- + const moveNewJourney = async (newJourney: any) => { + const childOldId = newJourney.id; + const childOldJourneyId = `journey_${childOldId}`; + const childOldTitle = newJourney.title; + + const projectName = newJourney.project.name; + const projectDescription = newJourney.project.description; + const parentNewName = `${projectName} ${projectDescription}`; + const parentNewProjectId = `project_${parentNewName}`; + + const childOldNode = treeData.find( + (node) => + node.id === childOldJourneyId && node.name == childOldTitle + ); + + const parentOldNode = treeData.find( + (node) => node.id === childOldNode?.parentId + ); + + const parentNewNode = treeData.find( + (node) => + node.name === parentNewName && node.id === parentNewProjectId + ); + + if (parentNewNode) { + if (typeof parentNewNode.getChildren === 'function') { + try { + const children = await parentNewNode.getChildren(); + + const childNode = children.find( + (child) => + child.id === childOldJourneyId && + child.name === childOldTitle + ); + + if (childNode) { + selectNode(childNode); + + const getBasePath = (pathname: string): string => { + const segments = pathname + .split('/') + .filter(Boolean); + const basePath = `/${segments[0]}/${segments[1]}`; + return basePath; + }; + const baseLibraryPath = getBasePath( + window.location.pathname + ); + + let parentPath = constructPath(parentNewNode, treeData); + + parentPath = parentPath + .split('/') + .filter( + (segment) => !segment.startsWith('project_') + ) + .join('/'); + + const childSegment = `${encodeURIComponent(childOldTitle)}/${encodeURIComponent(childOldJourneyId)}`; + + const fullPath = `${baseLibraryPath}/${parentPath}/${childSegment}`; + + navigate(fullPath, { replace: false }); + + const nodeNames = fullPath + .split('/') + .filter((name) => name.trim() !== '') + .slice(2) + .map(decodeURIComponent); + + expandNodePath( + nodeNames, + treeData, + expandNode, + selectNode, + setIsNodeExpanded, + () => true + ); + } + } catch (error) { + console.error( + `Error fetching children for '${parentNewName}':`, + error + ); + } + } + } + }; + + const resetNewJourney = async (newJourney: any) => { + const childOldId = newJourney.id; + const childOldJourneyId = `journey_${childOldId}`; + const childOldTitle = newJourney.title; + + const childOldNode = treeData.find( + (node) => + node.id === childOldJourneyId && node.name == childOldTitle + ); + + const parentOldNode = treeData.find( + (node) => node.id === childOldNode?.parentId + ); + + if (parentOldNode) { + const getBasePath = (pathname: string): string => { + const segments = pathname.split('/').filter(Boolean); + const basePath = `/${segments[0]}/${segments[1]}`; + return basePath; + }; + const baseLibraryPath = getBasePath(window.location.pathname); + + let parentPath = constructPath(parentOldNode, treeData); + + parentPath = parentPath + .split('/') + .filter((segment) => !segment.startsWith('project_')) + .join('/'); + + const pathSegments = parentPath.split('/'); + pathSegments.pop(); + parentPath = pathSegments.join('/'); + + const intermediateSegment = 'Journey available across projects'; + const childSegment = `${encodeURIComponent(childOldTitle)}/${encodeURIComponent(childOldJourneyId)}`; + const fullPath = `${baseLibraryPath}/${parentPath}/${encodeURIComponent(intermediateSegment)}/${childSegment}`; + + navigate(fullPath, { replace: false }); + + const nodeNames = fullPath + .split('/') + .filter((name) => name.trim() !== '') + .slice(2) + .map(decodeURIComponent); + + // expandNodePath( + // nodeNames, + // treeData, + // expandNode, + // selectNode, + // setIsNodeExpanded, + // () => true + // ); + } + }; + + useEffect(() => { + if ( + newJourney?.project && + (previousProjectId.current !== newJourney.project.id || + previousProjectId.current === null) + ) { + moveNewJourney(newJourney); + } else { + if (!saveTriggered) return; + resetNewJourney(newJourney); + setSaveTriggered(false); + } + previousProjectId.current = newJourney?.project?.id ?? null; + }, [newJourney, treeData, saveTriggered]); + + // ----------------------------------------------------------- const getNodeChildCountAndCollapse = ( parentNodeId: string | number @@ -249,12 +417,6 @@ const TreeView = ({ node.onClick && node.onClick(); }; - const handleOnClick = (node: NodeData): void => { - if (!hasUnsavedChanges || confirm(unsavedChangesConfirmationMessage)) { - selectNode(node); - } - }; - const getParentPath = (node: NodeData, treeData: NodeData[]): any => { if (!node.parentId) { return [node.name]; @@ -296,17 +458,19 @@ const TreeView = ({ hasChildren={node.getChildren ? true : false} isExpanded={node.isExpanded === true} isVoided={node.isVoided === true} - isSelected={node.isSelected === true} + isSelected={ + !!selectedLibraryItem && + node.id.toString().includes(selectedLibraryItem) + } title={node.name} > {node.onClick ? ( { - handleOnClick(node); - }} isSelected={node.isSelected ? true : false} + title={node.name} > {node.name} @@ -319,7 +483,12 @@ const TreeView = ({ return ( {linkContent} diff --git a/src/components/TreeView/style.ts b/src/components/TreeView/style.ts index ad2b493f0..5eabe768c 100644 --- a/src/components/TreeView/style.ts +++ b/src/components/TreeView/style.ts @@ -1,9 +1,11 @@ import styled, { css } from 'styled-components'; import { tokens } from '@equinor/eds-tokens'; +import { NavLink } from 'react-router-dom'; export const TreeContainer = styled.div` display: flex; flex-direction: column; + max-width: 100%; `; interface NodeContainerProps { @@ -17,6 +19,8 @@ export const NodeContainer = styled.div` margin-left: ${(props): string => `calc(var(--grid-unit) * ${props.indentMultiplier} - 4px)`}; + margin-left: ${(props): string => + `calc(var(--grid-unit) * ${props.indentMultiplier} - 4px)`}; `; interface ExpandCollapseIconProps { @@ -81,13 +85,6 @@ export const NodeName = styled.div` opacity: 0.5; `} - ${(props): any => - props.isSelected && - css` - color: ${tokens.colors.interactive.primary__resting.rgba}; - background: ${tokens.colors.ui.background__light.rgba}; - `} - /* add margin to nodes without children, to align with those that do (with expand/collapse icon) */ margin-left: ${(props): string => !props.hasChildren ? 'calc(var(--grid-unit) * 6.5)' : '0'}; @@ -99,8 +96,10 @@ interface NodeLinkProps { isSelected: boolean; } -export const NodeLink = styled.span` +export const NodeLink = styled(NavLink)` cursor: pointer; + text-decoration: none; + color: inherit; ${(props): any => props.isExpanded && @@ -114,12 +113,6 @@ export const NodeLink = styled.span` opacity: 0.5; `} - ${(props): any => - props.isSelected && - css` - color: ${tokens.colors.interactive.primary__resting.rgba}; - background: ${tokens.colors.ui.background__light.rgba}; - `} :hover { color: ${(props): string => diff --git a/src/core/PlantContext.tsx b/src/core/PlantContext.tsx index 040a50043..c89c9d1b6 100644 --- a/src/core/PlantContext.tsx +++ b/src/core/PlantContext.tsx @@ -168,7 +168,6 @@ export const PlantContextProvider: React.FC = ({ children }): JSX.Element => { return; } try { - console.log('Calling setCurrentPlant with:', plantInPath); setCurrentPlant(plantInPath); } catch (error) { console.error(`Failed to set current plant: ${error.message}`); @@ -191,7 +190,6 @@ export const PlantContextProvider: React.FC = ({ children }): JSX.Element => { } try { - console.log('Setting current plant with:', plantInPath); setCurrentPlant(plantInPath); } catch (error) { console.error(`Failed to set current plant: ${error.message}`); diff --git a/src/core/ProCoSysSettings.ts b/src/core/ProCoSysSettings.ts new file mode 100644 index 000000000..6207d4e9c --- /dev/null +++ b/src/core/ProCoSysSettings.ts @@ -0,0 +1,308 @@ +import { IAuthService } from 'src/auth/AuthService'; +import SettingsApiClient from '@procosys/http/SettingsApiClient'; + +import localSettings from '../settings.json'; + +//#region types + +interface FeatureConfig { + url: string; + scope: Array; + version?: string; +} + +interface FeatureFlags { + IPO: boolean; + library: boolean; + preservation: boolean; + main: boolean; + quickSearch: boolean; +} + +interface ConfigResponse { + configuration: { + procosysApi: FeatureConfig; + graphApi: FeatureConfig; + preservationApi: FeatureConfig; + searchApi: FeatureConfig; + ipoApi: FeatureConfig; + libraryApi: FeatureConfig; + instrumentationKey: string; + }; + featureFlags: FeatureFlags; +} + +interface AuthConfigResponse { + clientId: string; + authority: string; + scopes: Array; +} + +export enum AsyncState { + READY, + INITIALIZING, + ERROR, +} +//#endregion types + +class ProCoSysSettings { + private settingsConfigurationApiClient!: SettingsApiClient; + configurationEndpoint!: string; + configurationScope!: string; + + // remoteConfigurationResponse: ConfigResponse; + /** + * Validates the async state + * INITIALIZING - Settings is not done initializing + * ERROR - Settings initializer encountered an error while loading + * READY - Settings are finished loading + */ + configState: AsyncState = AsyncState.INITIALIZING; + + /** + * Validates the async state + * INITIALIZING - Settings is not done initializing + * ERROR - Settings initializer encountered an error while loading + * READY - Settings are finished loading + */ + authConfigState: AsyncState = AsyncState.INITIALIZING; + + /** + * A random number to validate instance + */ + private instanceId!: number; + + /** + * Singleton instance of configuration + */ + private static instance: ProCoSysSettings; + + /** + * Cached response from server + */ + private configurationResponse!: Promise; + + /** + * Cached response from server + */ + private authConfigResponse!: Promise; + + /** + * URI to the login authority + */ + authority!: string; + /** + * Default scopes to ask for concent for when logging in + */ + defaultScopes: Array = []; + + /** + * AAD Client ID + */ + clientId!: string; + + /** + * Application insight instrumentation key + */ + instrumentationKey = ''; + + procosysApi!: FeatureConfig; + graphApi!: FeatureConfig; + preservationApi!: FeatureConfig; + searchApi!: FeatureConfig; + ipoApi!: FeatureConfig; + libraryApi!: FeatureConfig; + + featureFlags!: FeatureFlags; + + /** + * Returns true or false based on if the feature is enabled or disabled. + * @returns TRUE: feature is enabled + * @returns FALSE: feature is disabled + * @default false - if the feature is not configured + * @param feature key for feature + */ + featureIsEnabled(feature: string): boolean { + return this.featureFlags[feature as keyof FeatureFlags] || false; + } + + constructor() { + if (ProCoSysSettings.instance instanceof ProCoSysSettings) { + return ProCoSysSettings.instance; + } + + this.instanceId = Math.floor(Math.random() * 9999); + if ( + !localSettings.configurationEndpoint || + !localSettings.configurationScope + ) { + console.error( + 'Missing local configuration for Config API', + localSettings + ); + throw 'Missing local configuration for Config API'; + } + this.featureFlags = { + IPO: true, + preservation: true, + main: true, + library: true, + quickSearch: true, + }; + this.settingsConfigurationApiClient = new SettingsApiClient( + localSettings.configurationEndpoint + ); + + ProCoSysSettings.instance = this; + } + + async loadAuthConfiguration(): Promise { + this.authConfigState = AsyncState.INITIALIZING; + try { + this.authConfigResponse = + this.settingsConfigurationApiClient.getAuthConfig(); + const response = await this.authConfigResponse; + this.mapFromAuthConfigResponse(response); + this.authConfigState = AsyncState.READY; + } catch (error) { + this.authConfigState = AsyncState.ERROR; + console.error( + 'Failed to load auth configuration from remote source', + error + ); + } + } + + async loadConfiguration(authService: IAuthService): Promise { + this.configurationResponse = + this.settingsConfigurationApiClient.getConfig( + authService, + localSettings.configurationScope + ); + + try { + const configResponse = await this.configurationResponse; + try { + this.mapFromConfigurationResponse(configResponse); + this.configState = AsyncState.READY; + } catch (error) { + this.configState = AsyncState.ERROR; + console.error( + 'Failed to parse configuration from remote source', + error + ); + } + } catch (error) { + this.configState = AsyncState.ERROR; + console.error( + 'Failed to load configuration from remote source', + error + ); + } + } + + private mapFromAuthConfigResponse( + authConfigResponse: AuthConfigResponse + ): void { + this.authority = authConfigResponse.authority; + this.clientId = authConfigResponse.clientId; + this.defaultScopes = authConfigResponse.scopes; + } + + private mapFromConfigurationResponse( + configurationResponse: ConfigResponse + ): void { + try { + this.instrumentationKey = + configurationResponse.configuration.instrumentationKey; + + this.graphApi = configurationResponse.configuration.graphApi; + this.preservationApi = + configurationResponse.configuration.preservationApi; + this.searchApi = configurationResponse.configuration.searchApi; + this.ipoApi = configurationResponse.configuration.ipoApi; + this.libraryApi = configurationResponse.configuration.libraryApi; + this.procosysApi = configurationResponse.configuration.procosysApi; + + // Feature flags + this.featureFlags.IPO = configurationResponse.featureFlags.IPO; + this.featureFlags.main = configurationResponse.featureFlags.main; + this.featureFlags.library = + configurationResponse.featureFlags.library; + this.featureFlags.preservation = + configurationResponse.featureFlags.preservation; + this.featureFlags.quickSearch = + configurationResponse.featureFlags.quickSearch; + } catch (error) { + console.error( + 'Failed to parse Configuration from remote server', + error + ); + throw error; + } + try { + this.overrideFromLocalConfiguration(); + } catch (error) { + console.error('Failed to override with local configuration', error); + throw error; + } + } + + private overrideAuthFromLocalSettings(): void { + // Auth elements + localSettings.clientId && (this.clientId = localSettings.clientId); + localSettings.authority && (this.authority = localSettings.authority); + localSettings.defaultScopes && + (this.defaultScopes = localSettings.defaultScopes); + } + + private overrideFromLocalConfiguration(): void { + if (localSettings.configuration) { + // Configuration elements + console.info( + 'Overriding configuration from settings: ', + localSettings.configuration + ); + localSettings.configuration.instrumentationKey && + (this.instrumentationKey = + localSettings.configuration.instrumentationKey); + localSettings.configuration.graphApi && + (this.graphApi = localSettings.configuration.graphApi); + localSettings.configuration.preservationApi && + (this.preservationApi = + localSettings.configuration.preservationApi); + localSettings.configuration.searchApi && + (this.searchApi = localSettings.configuration.searchApi); + localSettings.configuration.ipoApi && + (this.ipoApi = localSettings.configuration.ipoApi); + localSettings.configuration.libraryApi && + (this.libraryApi = localSettings.configuration.libraryApi); + localSettings.configuration.procosysApi && + (this.procosysApi = localSettings.configuration.procosysApi); + } + + // Feature flags + if (localSettings.featureFlags) { + console.info( + 'Overriding feature flags from settings: ', + localSettings.featureFlags + ); + + localSettings.featureFlags.main != undefined && + (this.featureFlags.main = localSettings.featureFlags.main); + localSettings.featureFlags.IPO != undefined && + (this.featureFlags.IPO = localSettings.featureFlags.IPO); + localSettings.featureFlags.library != undefined && + (this.featureFlags.library = + localSettings.featureFlags.library); + localSettings.featureFlags.preservation != undefined && + (this.featureFlags.preservation = + localSettings.featureFlags.preservation); + localSettings.featureFlags.quickSearch != undefined && + (this.featureFlags.quickSearch = + localSettings.featureFlags.quickSearch); + } + } +} + +export default new ProCoSysSettings(); diff --git a/src/core/services/Analytics/AppInsightsAnalytics.ts b/src/core/services/Analytics/AppInsightsAnalytics.ts index 3d59d01d2..30242d29f 100644 --- a/src/core/services/Analytics/AppInsightsAnalytics.ts +++ b/src/core/services/Analytics/AppInsightsAnalytics.ts @@ -7,6 +7,7 @@ import { ITelemetryPlugin, } from '@microsoft/applicationinsights-web'; import IAnalytics from './IAnalytics'; +import ProCoSysSettings from '@procosys/core/ProCoSysSettings'; import { ReactPlugin } from '@microsoft/applicationinsights-react-js'; class AppInsightsAnalytics implements IAnalytics { @@ -17,7 +18,7 @@ class AppInsightsAnalytics implements IAnalytics { const reactPlugin = new ReactPlugin() as unknown as ITelemetryPlugin; this._service = new ApplicationInsights({ config: { - instrumentationKey: window.INSTRUMENTATION_KEY, + instrumentationKey: ProCoSysSettings.instrumentationKey, extensions: [reactPlugin], extensionConfig: { [reactPlugin.identifier]: { history: history }, diff --git a/src/modules/PlantConfig/context/PlantConfigContext.tsx b/src/modules/PlantConfig/context/PlantConfigContext.tsx index a307813bb..e14f20c69 100644 --- a/src/modules/PlantConfig/context/PlantConfigContext.tsx +++ b/src/modules/PlantConfig/context/PlantConfigContext.tsx @@ -1,13 +1,15 @@ -import React, { useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import LibraryApiClient from '../http/LibraryApiClient'; import propTypes from 'prop-types'; import { useCurrentPlant } from '../../../core/PlantContext'; import { useProcosysContext } from '../../../core/ProcosysContext'; import PreservationApiClient from '@procosys/modules/Preservation/http/PreservationApiClient'; +import { ProjectDetails } from '@procosys/modules/Preservation/types'; type PlantConfigContextProps = { libraryApiClient: LibraryApiClient; preservationApiClient: PreservationApiClient; + projects?: ProjectDetails[]; }; const PlantConfigContext = React.createContext( @@ -17,14 +19,26 @@ const PlantConfigContext = React.createContext( export const PlantConfigContextProvider: React.FC = ({ children, }): JSX.Element => { - const { auth } = useProcosysContext(); + const { auth, procosysApiClient } = useProcosysContext(); const { plant } = useCurrentPlant(); const libraryApiClient = useMemo(() => new LibraryApiClient(auth), [auth]); + const [projects, setProjects] = useState( + undefined + ); const preservationApiClient = useMemo( () => new PreservationApiClient(auth), [auth] ); + useMemo(() => { + const fetchProjects = async () => { + const projects = + await procosysApiClient.getAllProjectsForUserAsync(); + setProjects(projects); + }; + fetchProjects(); + }, [plant]); + useMemo(() => { libraryApiClient.setCurrentPlant(plant.id); preservationApiClient.setCurrentPlant(plant.id); @@ -35,6 +49,7 @@ export const PlantConfigContextProvider: React.FC = ({ value={{ libraryApiClient: libraryApiClient, preservationApiClient: preservationApiClient, + projects: projects, }} > {children} diff --git a/src/modules/PlantConfig/views/Library/Library.tsx b/src/modules/PlantConfig/views/Library/Library.tsx index 6e3952b5a..7306dd318 100644 --- a/src/modules/PlantConfig/views/Library/Library.tsx +++ b/src/modules/PlantConfig/views/Library/Library.tsx @@ -4,10 +4,12 @@ import React, { useEffect, useReducer, useState } from 'react'; import { Helmet } from 'react-helmet'; //import withAccessControl from '../../../../core/security/withAccessControl'; import LibraryItemDetails from './LibraryItemDetails'; -import LibraryTreeview from './LibraryTreeview/LibraryTreeview'; +import LibraryTreeview, { + LibraryProvider, +} from './LibraryTreeview/LibraryTreeview'; import { hot } from 'react-hot-loader'; -import { Route, Routes } from 'react-router-dom'; +import { Route, Routes, useLocation } from 'react-router-dom'; export enum LibraryType { TAG_FUNCTION = 'TagFunction', @@ -33,6 +35,7 @@ const Library = (): JSX.Element => { const [selectedLibraryType, setSelectedLibraryType] = useState(''); const [selectedLibraryItem, setSelectedLibraryItem] = useState(''); const [dirtyLibraryType, setDirtyLibraryType] = useState(''); + const { pathname } = useLocation(); const [update, forceUpdate] = useReducer((x) => x + 1, 0); // Used to force an update on library content pane for top level tree nodes useEffect(() => { @@ -54,7 +57,7 @@ const Library = (): JSX.Element => { extractAndSetItemId(segmentsAfterLibraryV2); } - }, []); + }, [pathname]); const mapSegmentsToLibraryType = (segments: string[]): string => { const firstSegment = segments[0]?.toLowerCase().replace(/%20/g, ' '); @@ -111,43 +114,51 @@ const Library = (): JSX.Element => { }; return ( - - {selectedLibraryType && ( - - {` - ${selectedLibraryType}`} - - )} - setDirtyLibraryType('')} - /> - - - - - - - } + + + {selectedLibraryType && ( + + {` - ${selectedLibraryType}`} + + )} + setDirtyLibraryType('')} /> - - + + + + + + + } + /> + + + ); }; diff --git a/src/modules/PlantConfig/views/Library/LibraryTreeview/LibraryTreeview.style.ts b/src/modules/PlantConfig/views/Library/LibraryTreeview/LibraryTreeview.style.ts index b9467b8b3..2ef506aed 100644 --- a/src/modules/PlantConfig/views/Library/LibraryTreeview/LibraryTreeview.style.ts +++ b/src/modules/PlantConfig/views/Library/LibraryTreeview/LibraryTreeview.style.ts @@ -7,4 +7,5 @@ export const Container = styled.div` padding-top: var(--margin-module--top); padding-right: calc(var(--grid-unit) * 5); padding-left: calc(var(--grid-unit) * 5); + width: 20vw; `; diff --git a/src/modules/PlantConfig/views/Library/LibraryTreeview/LibraryTreeview.tsx b/src/modules/PlantConfig/views/Library/LibraryTreeview/LibraryTreeview.tsx index 4decc7838..9c253b05d 100644 --- a/src/modules/PlantConfig/views/Library/LibraryTreeview/LibraryTreeview.tsx +++ b/src/modules/PlantConfig/views/Library/LibraryTreeview/LibraryTreeview.tsx @@ -4,13 +4,14 @@ import TreeView, { import { Container } from './LibraryTreeview.style'; import { LibraryType } from '../Library'; -import React from 'react'; +import React, { createContext, useContext, useState } from 'react'; import { showSnackbarNotification } from '../../../../../core/services/NotificationService'; import { usePlantConfigContext } from '../../../context/PlantConfigContext'; import { unsavedChangesConfirmationMessage, useDirtyContext, } from '@procosys/core/DirtyContext'; +import { Journey } from '../PreservationJourney/types/Journey'; type LibraryTreeviewProps = { forceUpdate: React.DispatchWithoutAction; @@ -18,12 +19,14 @@ type LibraryTreeviewProps = { setSelectedLibraryItem: (libraryItem: string) => void; dirtyLibraryType: string; resetDirtyLibraryType: () => void; + selectedLibraryItem: string; }; const LibraryTreeview = (props: LibraryTreeviewProps): JSX.Element => { const { isDirty } = useDirtyContext(); - const { libraryApiClient, preservationApiClient } = usePlantConfigContext(); + const { libraryApiClient, preservationApiClient, projects } = + usePlantConfigContext(); const handleTreeviewClick = ( libraryType: LibraryType, @@ -68,13 +71,28 @@ const LibraryTreeview = (props: LibraryTreeviewProps): JSX.Element => { const getPresJourneyTreeNodes = async (): Promise => { const children: TreeViewNode[] = []; try { - return await preservationApiClient - .getJourneys(true) - .then((response) => { - if (response) { - response.forEach((journey) => - children.push({ - id: 'journey_' + journey.id, + const journeys = await preservationApiClient.getJourneys(true); + const groupedJourneys = journeys.reduce( + (acc: { [key: string]: Journey[] }, journey) => { + const projectDescription = journey.project + ? `${journey.project.name} ${journey.project.description}` + : 'Journey available across projects'; + if (!acc[projectDescription]) { + acc[projectDescription] = []; + } + acc[projectDescription].push(journey); + return acc; + }, + {} as { [key: string]: Journey[] } + ); + Object.keys(groupedJourneys).forEach((projectDescription) => { + const projectNode: TreeViewNode = { + id: `project_${projectDescription}`, + name: projectDescription, + getChildren: async (): Promise => { + return groupedJourneys[projectDescription].map( + (journey) => ({ + id: `journey_${journey.id}`, name: journey.title, isVoided: journey.isVoided, onClick: (): void => @@ -84,9 +102,11 @@ const LibraryTreeview = (props: LibraryTreeviewProps): JSX.Element => { ), }) ); - } - return children; - }); + }, + }; + children.push(projectNode); + }); + return children; } catch (error) { console.error( 'Get preservation journeys failed: ', @@ -270,6 +290,7 @@ const LibraryTreeview = (props: LibraryTreeviewProps): JSX.Element => { { }; export default LibraryTreeview; + +interface LibraryContextType { + newJourney: Journey; + setNewJourney: (journey: Journey) => void; + saveTriggered: boolean; + setSaveTriggered: (value: boolean) => void; +} + +const LibraryContext = createContext(undefined); + +export const LibraryProvider: React.FC<{ children: React.ReactNode }> = ({ + children, +}) => { + const [newJourney, setNewJourney] = useState({ + id: -1, + title: '', + isVoided: false, + isInUse: false, + steps: [], + rowVersion: '', + }); + + const [saveTriggered, setSaveTriggered] = useState(false); + + return ( + + {children} + + ); +}; + +export const useLibraryContext = (): LibraryContextType => { + const context = useContext(LibraryContext); + if (!context) { + throw new Error( + 'useLibraryContext must be used within a LibraryProvider' + ); + } + return context; +}; diff --git a/src/modules/PlantConfig/views/Library/PreservationJourney/PreservationJourney.tsx b/src/modules/PlantConfig/views/Library/PreservationJourney/PreservationJourney.tsx index 6e45eb5d9..b6eb597e3 100644 --- a/src/modules/PlantConfig/views/Library/PreservationJourney/PreservationJourney.tsx +++ b/src/modules/PlantConfig/views/Library/PreservationJourney/PreservationJourney.tsx @@ -30,23 +30,22 @@ import { ButtonContainerLeft, ButtonContainerRight, } from '../Library.style'; - -const addIcon = ; -const upIcon = ; -const downIcon = ; -const deleteIcon = ; -const duplicateIcon = ; -const voidIcon = ; -const unvoidIcon = ; - -const saveTitle = - 'If you have changes to save, check that all fields are filled in, no titles are identical, and if you have a supplier step it must be the first step.'; -const baseBreadcrumb = 'Library / Preservation journeys'; -const moduleName = 'PreservationJourneyForm'; - -const WAIT_INTERVAL = 300; - -const checkboxHeightInGridUnits = 4; +// import { +// AutoTransferMethod, +// Journey, +// Mode, +// PreservationJourneyProps, +// Step, +// } from './types'; +// import { ProjectDetails } from '@procosys/modules/Preservation/types'; +import { Autocomplete } from '@equinor/eds-core-react'; +import { useLibraryContext } from '../LibraryTreeview/LibraryTreeview'; + +type ProjectDetails = { + id: number; + name: string; + description: string; +}; enum AutoTransferMethod { NONE = 'None', @@ -61,6 +60,7 @@ interface Journey { isInUse: boolean; steps: Step[]; rowVersion: string; + project?: ProjectDetails; } interface Step { @@ -93,6 +93,25 @@ type PreservationJourneyProps = { setDirtyLibraryType: () => void; }; +const addIcon = ; +const upIcon = ; +const downIcon = ; +const deleteIcon = ; +const duplicateIcon = ; +const voidIcon = ; +const unvoidIcon = ; + +const saveTitle = + 'If you have changes to save, check that all fields are filled in, no titles are identical, and if you have a supplier step it must be the first step.'; +const baseBreadcrumb = 'Library / Preservation journeys'; +const moduleName = 'PreservationJourneyForm'; + +const WAIT_INTERVAL = 300; + +const checkboxHeightInGridUnits = 4; + +const sharedJourneyBreadcrumb = 'All projects'; + const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => { const getInitialJourney = (): Journey => { return { @@ -108,7 +127,13 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => { const [isEditMode, setIsEditMode] = useState(false); const [isLoading, setIsLoading] = useState(false); const [journey, setJourney] = useState(null); - const [newJourney, setNewJourney] = useState(getInitialJourney); + const [selectedProject, setSelectedProject] = useState< + ProjectDetails | undefined + >(); + const [breadcrumbs, setBreadcrumbs] = useState( + `${baseBreadcrumb} / ${sharedJourneyBreadcrumb}` + ); + const { newJourney, setNewJourney } = useLibraryContext(); const [mappedModes, setMappedModes] = useState([]); const [modes, setModes] = useState([]); const [mappedResponsibles, setMappedResponsibles] = useState( @@ -121,6 +146,7 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => { const [filterForResponsibles, setFilterForResponsibles] = useState(''); const { setDirtyStateFor, unsetDirtyStateFor } = useDirtyContext(); + const { saveTriggered, setSaveTriggered } = useLibraryContext(); useEffect(() => { setIsEditMode(false); @@ -130,22 +156,31 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => { return JSON.stringify(journey) != JSON.stringify(newJourney); }, [journey, newJourney]); - const { preservationApiClient, libraryApiClient } = usePlantConfigContext(); + const { preservationApiClient, libraryApiClient, projects } = + usePlantConfigContext(); const cloneJourney = (journey: Journey): Journey => { return JSON.parse(JSON.stringify(journey)); }; + useEffect(() => { + setBreadcrumbs( + `${baseBreadcrumb} / ${ + selectedProject?.description ?? sharedJourneyBreadcrumb + }` + ); + }, [selectedProject]); + /** * Get Modes */ useEffect(() => { - let requestCancellor: Canceler | null = null; + let requestCanceler: Canceler | null = null; (async (): Promise => { try { const modes = await preservationApiClient.getModes( false, - (cancel: Canceler) => (requestCancellor = cancel) + (cancel: Canceler) => (requestCanceler = cancel) ); const mappedModes: SelectItem[] = []; modes.forEach((mode) => @@ -183,7 +218,7 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => { })(); return (): void => { - requestCancellor && requestCancellor(); + requestCanceler && requestCanceler(); }; }, [journey]); @@ -231,21 +266,17 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => { const getJourney = async (journeyId: number): Promise => { setIsLoading(true); try { - await preservationApiClient - .getJourney(journeyId, true) - .then((response) => { - setJourney(response); - setNewJourney(cloneJourney(response)); - }); + const response = await preservationApiClient.getJourney( + journeyId, + true + ); + setNewJourney(response); } catch (error) { console.error( 'Get preservation journey failed: ', error.message, error.data ); - if (error instanceof ProCoSysApiError) { - if (error.isCancel) return; - } showSnackbarNotification(error.message); } setIsLoading(false); @@ -261,6 +292,14 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => { } }, [props.journeyId]); + useEffect(() => { + if (journey || newJourney) { + setSelectedProject(newJourney.project ?? journey?.project); + } else { + setSelectedProject(undefined); + } + }, [journey?.project?.id, newJourney.project?.id]); + const saveNewStep = async ( journeyId: number, step: Step @@ -312,7 +351,8 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => { await preservationApiClient.updateJourney( newJourney.id, newJourney.title, - newJourney.rowVersion + newJourney.rowVersion, + newJourney.project?.name ); props.setDirtyLibraryType(); return true; @@ -356,7 +396,10 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => { setIsLoading(true); let saveOk = true; let noChangesToSave = true; - if (journey && journey.title != newJourney.title) { + if ( + journey?.title != newJourney.title || + journey?.project?.id != newJourney.project?.id + ) { saveOk = await updateJourney(); noChangesToSave = false; } @@ -434,9 +477,8 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => { }; const handleSave = (): void => { - if (!inputIsComplete()) { - return; - } + if (!inputIsComplete()) return; + setSaveTriggered(true); if (newJourney.id === -1) { saveNewJourney(); } else { @@ -564,6 +606,11 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => { setNewJourney(cloneJourney(newJourney)); }; + const setProjectIdValue = (value: ProjectDetails): void => { + newJourney.project = value; + setNewJourney(cloneJourney(newJourney)); + }; + const setResponsibleValue = ( event: React.MouseEvent, stepIndex: number, @@ -798,7 +845,7 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => { if (isLoading) { return ( - {baseBreadcrumb} / + {breadcrumbs} / ); @@ -807,7 +854,7 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => { if (!isEditMode) { return ( - {baseBreadcrumb} + {breadcrumbs}