diff --git a/.docker/Dockerfile b/.docker/Dockerfile index afdc2a7dc..de1c089d3 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -13,4 +13,4 @@ COPY --from=build /app/build /usr/share/nginx/html COPY .docker/nginx/ /etc/nginx/ COPY .docker/scripts/ /etc/scripts/ EXPOSE 5000 -CMD ["sh","/etc/scripts/startup.sh"] +CMD ["sh","/etc/scripts/startup.sh"] \ No newline at end of file diff --git a/.dockerignore b/.dockerignore index 35c96ffba..a95d544f4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,3 @@ node_modules build -junit.xml +junit.xml \ No newline at end of file diff --git a/.eslintignore b/.eslintignore index 0ca596a61..139e61738 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,4 +2,4 @@ /*.js # Ignore storybook build -storybook-static/ +storybook-static/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index ac9156a14..733975b55 100644 --- a/.gitignore +++ b/.gitignore @@ -54,4 +54,4 @@ typings/ #VS Code folder .vscode/ -.vs/slnx.sqlite +.vs/slnx.sqlite \ No newline at end of file diff --git a/radixconfig.yaml b/radixconfig.yaml index c932db73f..a9392919d 100644 --- a/radixconfig.yaml +++ b/radixconfig.yaml @@ -1,17 +1,9 @@ + apiVersion: radix.equinor.com/v1 kind: RadixApplication metadata: name: procosys-frontend spec: - dnsExternalAlias: - - alias: procosystest.equinor.com - component: frontend - environment: test - useCertificateAutomation: true - - alias: procosys.equinor.com - component: frontend - environment: prod - useCertificateAutomation: true environments: - name: dev build: @@ -32,7 +24,7 @@ spec: - environment: dev dockerfileName: Dockerfile.dev variables: - configurationEndpoint: 'https://pcs-config-non-prod-func.azurewebsites.net/api/Frontend' + configurationEndpoint: 'https://pcs-config-non-prod-func.azurewebsites.net/api/Frontend/Auth' configurationScope: 'api://0708e202-b5ad-4d95-9735-a631c715d6a9/Read' - environment: test dockerfileName: Dockerfile.test @@ -43,4 +35,4 @@ spec: dockerfileName: Dockerfile.prod variables: configurationEndpoint: 'https://pcs-config-prod-func.azurewebsites.net/api/Frontend' - configurationScope: 'api://756f2a23-f54d-4643-bb49-62c0db4802ae/Read' + configurationScope: 'api://756f2a23-f54d-4643-bb49-62c0db4802ae/Read' \ No newline at end of file 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 ( - + name.trim() !== '') - .slice(2) - .map(decodeURIComponent); + .split('/') + .filter((name) => name.trim() !== '') + .slice(2) + .map(decodeURIComponent); const checkMounted = () => isMounted; diff --git a/src/core/PlantContext.tsx b/src/core/PlantContext.tsx index 8d745a3de..040a50043 100644 --- a/src/core/PlantContext.tsx +++ b/src/core/PlantContext.tsx @@ -8,7 +8,7 @@ import Loading from '../components/Loading'; import propTypes from 'prop-types'; import { useAnalytics } from './services/Analytics/AnalyticsContext'; import { useCurrentUser } from './UserContext'; -import { useParams } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { useProcosysContext } from './ProcosysContext'; import useRouter from '../hooks/useRouter'; @@ -45,6 +45,7 @@ export const PlantContextProvider: React.FC = ({ children }): JSX.Element => { permissions: true, }); const analytics = useAnalytics(); + const navigate = useNavigate(); // Validate user plants if (!user || !user.plants || user.plants.length === 0) { @@ -55,13 +56,34 @@ export const PlantContextProvider: React.FC = ({ children }): JSX.Element => { } // Validate plant in path - if (!plantInPath || plantInPath === '') { - console.warn('Plant ID is missing in path. Setting default plant.'); - return ; + if (!plantInPath || plantInPath === '' || typeof plantInPath !== 'string') { + console.warn('Invalid plantInPath:', plantInPath); + return ( + + ); } const [currentPlant, setCurrentPlantInContext] = useState(() => { + //TODO: to remove log in the future + if (!user || !Array.isArray(user.plants)) { + const plantsValue = user?.plants + ? `Actual value of 'user.plants': ${JSON.stringify( + user.plants, + null, + 2 + )}` + : "'user.plants' is undefined or null."; + + throw new Error( + `Invalid user object: 'user.plants' is not defined or not an array. ${plantsValue}` + ); + } + const plant = user.plants.filter( (plant) => plant.id === `PCS$${plantInPath}` )[0]; @@ -71,6 +93,7 @@ export const PlantContextProvider: React.FC = ({ children }): JSX.Element => { console.warn( `No plant found for path ID: ${plantInPath}. Using default fallback.` ); + return { id: '', title: '', pathId: plantInPath || 'unknown' }; } return { id: plant.id, title: plant.title, pathId: plantInPath }; @@ -111,7 +134,7 @@ export const PlantContextProvider: React.FC = ({ children }): JSX.Element => { if (!currentPlant || currentPlant.pathId === plantInPath) return; let newPath = `/${currentPlant.pathId}`; newPath = location.pathname.replace(plantInPath, currentPlant.pathId); - history.push(newPath); + navigate(newPath); }, [currentPlant]); // Fetch permissions @@ -154,11 +177,33 @@ export const PlantContextProvider: React.FC = ({ children }): JSX.Element => { } }, [plantInPath]); - // if (!currentPlant || !currentPlant.id) { - if (!currentPlant) { + useEffect(() => { + if (!user || !user.plants) { + console.error( + 'User data is not available. Cannot set current plant.' + ); + return; + } + + if (!plantInPath || typeof plantInPath !== 'string') { + console.warn('Invalid or missing plantInPath:', plantInPath); + return; + } + + try { + console.log('Setting current plant with:', plantInPath); + setCurrentPlant(plantInPath); + } catch (error) { + console.error(`Failed to set current plant: ${error.message}`); + } + }, [plantInPath, user]); + + if (!currentPlant || !currentPlant.id) { // return ; - return ; + // return ; + return ; } + if (isLoading.permissions) { return ; } diff --git a/src/core/ProCoSysSettings.ts b/src/core/ProCoSysSettings.ts index 25f032d04..442163378 100644 --- a/src/core/ProCoSysSettings.ts +++ b/src/core/ProCoSysSettings.ts @@ -240,6 +240,7 @@ class ProCoSysSettings { ); throw error; } + try { this.overrideFromLocalConfiguration(); } catch (error) { diff --git a/src/modules/InvitationForPunchOut/InvitationForPunchOut.tsx b/src/modules/InvitationForPunchOut/InvitationForPunchOut.tsx index ca271378d..1bd3fe0cc 100644 --- a/src/modules/InvitationForPunchOut/InvitationForPunchOut.tsx +++ b/src/modules/InvitationForPunchOut/InvitationForPunchOut.tsx @@ -45,10 +45,13 @@ const InvitationForPunchOut = (): JSX.Element => { /> } /> + } + /> ): JSX.Element => { const data = (row.value as IPO).id.toString(); - return {data}; + return {data}; }; const getProjectNameColumn = (row: TableOptions): JSX.Element => { diff --git a/src/modules/PlantConfig/context/PlantConfigContext.tsx b/src/modules/PlantConfig/context/PlantConfigContext.tsx index a307813bb..77349aa8c 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, { useEffect, 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/types.d.ts b/src/modules/PlantConfig/types.d.ts deleted file mode 100644 index e8c226a6f..000000000 --- a/src/modules/PlantConfig/types.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type ProjectDetails = { - id: number; - name: string; - description: string; -}; diff --git a/src/modules/PlantConfig/views/Library/LibraryTreeview/LibraryTreeview.tsx b/src/modules/PlantConfig/views/Library/LibraryTreeview/LibraryTreeview.tsx index 4decc7838..691c6f30a 100644 --- a/src/modules/PlantConfig/views/Library/LibraryTreeview/LibraryTreeview.tsx +++ b/src/modules/PlantConfig/views/Library/LibraryTreeview/LibraryTreeview.tsx @@ -11,6 +11,7 @@ import { unsavedChangesConfirmationMessage, useDirtyContext, } from '@procosys/core/DirtyContext'; +import { Journey } from '../PreservationJourney/types'; type LibraryTreeviewProps = { forceUpdate: React.DispatchWithoutAction; @@ -23,7 +24,8 @@ type LibraryTreeviewProps = { const LibraryTreeview = (props: LibraryTreeviewProps): JSX.Element => { const { isDirty } = useDirtyContext(); - const { libraryApiClient, preservationApiClient } = usePlantConfigContext(); + const { libraryApiClient, preservationApiClient, projects } = + usePlantConfigContext(); const handleTreeviewClick = ( libraryType: LibraryType, @@ -68,13 +70,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 +101,11 @@ const LibraryTreeview = (props: LibraryTreeviewProps): JSX.Element => { ), }) ); - } - return children; - }); + }, + }; + children.push(projectNode); + }); + return children; } catch (error) { console.error( 'Get preservation journeys failed: ', diff --git a/src/modules/PlantConfig/views/Library/PreservationJourney/PreservationJourney.tsx b/src/modules/PlantConfig/views/Library/PreservationJourney/PreservationJourney.tsx index 6e45eb5d9..321b6a321 100644 --- a/src/modules/PlantConfig/views/Library/PreservationJourney/PreservationJourney.tsx +++ b/src/modules/PlantConfig/views/Library/PreservationJourney/PreservationJourney.tsx @@ -30,6 +30,15 @@ import { ButtonContainerLeft, ButtonContainerRight, } from '../Library.style'; +import { + AutoTransferMethod, + Journey, + Mode, + PreservationJourneyProps, + Step, +} from './types'; +import { ProjectDetails } from '@procosys/modules/Preservation/types'; +import { Autocomplete } from '@equinor/eds-core-react'; const addIcon = ; const upIcon = ; @@ -48,50 +57,7 @@ const WAIT_INTERVAL = 300; const checkboxHeightInGridUnits = 4; -enum AutoTransferMethod { - NONE = 'None', - RFCC = 'OnRfccSign', - RFOC = 'OnRfocSign', -} - -interface Journey { - id: number; - title: string; - isVoided: boolean; - isInUse: boolean; - steps: Step[]; - rowVersion: string; -} - -interface Step { - id: number; - title: string; - autoTransferMethod: string; - isVoided: boolean; - isInUse: boolean; - mode: Mode; - responsible: { - code: string; - title: string; - rowVersion: string; - description?: string; - }; - rowVersion: string; -} - -interface Mode { - id: number; - title: string; - forSupplier: boolean; - isVoided: boolean; - rowVersion: string; -} - -type PreservationJourneyProps = { - forceUpdate: number; - journeyId: number; - setDirtyLibraryType: () => void; -}; +const sharedJourneyBreadcrumb = 'All projects'; const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => { const getInitialJourney = (): Journey => { @@ -108,6 +74,12 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => { const [isEditMode, setIsEditMode] = useState(false); const [isLoading, setIsLoading] = useState(false); const [journey, setJourney] = useState(null); + const [selectedProject, setSelectedProject] = useState< + ProjectDetails | undefined + >(); + const [breadcrumbs, setBreadcrumbs] = useState( + `${baseBreadcrumb} / ${sharedJourneyBreadcrumb}` + ); const [newJourney, setNewJourney] = useState(getInitialJourney); const [mappedModes, setMappedModes] = useState([]); const [modes, setModes] = useState([]); @@ -130,22 +102,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 +164,7 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => { })(); return (): void => { - requestCancellor && requestCancellor(); + requestCanceler && requestCanceler(); }; }, [journey]); @@ -261,6 +242,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 +301,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 +346,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; } @@ -445,7 +438,7 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => { }; const confirmDiscardingChangesIfExist = (): boolean => { - return !isDirty || confirm(unsavedChangesConfirmationMessage); + return !isDirty ?? confirm(unsavedChangesConfirmationMessage); }; const cancel = (): void => { @@ -564,6 +557,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 +796,7 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => { if (isLoading) { return ( - {baseBreadcrumb} / + {breadcrumbs} / ); @@ -807,7 +805,7 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => { if (!isEditMode) { return ( - {baseBreadcrumb} + {breadcrumbs}