diff --git a/.docker/nginx/conf.d/default.conf b/.docker/nginx/conf.d/default.conf index f5f706f50..efb85a856 100644 --- a/.docker/nginx/conf.d/default.conf +++ b/.docker/nginx/conf.d/default.conf @@ -4,6 +4,9 @@ server { server_name localhost; absolute_redirect off; + gzip on; + gzip_types text/css application/javascript image/svg+xml text/plain; + root /usr/share/nginx/html; @@ -16,4 +19,4 @@ server { location /index.html { add_header Cache-Control "no-cache"; } -} \ No newline at end of file +} diff --git a/.docker/scripts/env-replace.py b/.docker/scripts/env-replace.py deleted file mode 100644 index 3e81ff7d7..000000000 --- a/.docker/scripts/env-replace.py +++ /dev/null @@ -1,30 +0,0 @@ -# -# Replaces #placeholder# values from the build step with values from the system environment -# System variables are set by Docker/kubernetes when deployed -# Filter: Only uses variables that start with 'pcs_' but removes this prefix when searching for replacement -# - -import os -import sys -import glob - -print('Start - Replacing placeholder variables') - -keyVaultSecrets = [] -for key in os.environ: - if (key.startswith('pcs_')): - keyVaultSecrets.append((key.replace('pcs_',''),os.environ.get(key))) - -# Get all files -for filepath in glob.iglob('/frontend/**/*.js', recursive=True): - print("Looking for stuff to replace in: {filepath}".format(**vars())) - s = "" - with open(filepath) as file: - s = file.read() - for placeholderKey,value in keyVaultSecrets: - print("Replacing: #{placeholderKey}# - {value}".format(**vars())) - s = s.replace('#' + placeholderKey + '#', value) - with open(filepath, "w") as file: - file.write(s) - -print('Done - Replacing placeholder variables') diff --git a/.docker/scripts/run-nginx.sh b/.docker/scripts/run-nginx.sh index 5540c0790..7715a0a83 100644 --- a/.docker/scripts/run-nginx.sh +++ b/.docker/scripts/run-nginx.sh @@ -1,3 +1,9 @@ #!/bin/sh echo "Starting NGINX" +envsubst ' + ${CONFIGURATION} + ${AUTH_CONFIG} + ${FEATURE_FLAGS} + ' /usr/share/nginx/html/tmp.html +mv /usr/share/nginx/html/tmp.html /usr/share/nginx/html/index.html nginx -g 'daemon off;' diff --git a/.docker/scripts/startup.sh b/.docker/scripts/startup.sh index b64fa5217..eae036630 100644 --- a/.docker/scripts/startup.sh +++ b/.docker/scripts/startup.sh @@ -1,3 +1,2 @@ #!/bin/sh -python3 /etc/scripts/env-replace.py sh /etc/scripts/run-nginx.sh diff --git a/.gitignore b/.gitignore index ac9156a14..e8b1bd48c 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,8 @@ typings/ #VS Code folder .vscode/ .vs/slnx.sqlite +/.vs/procosys-js-frontend/CopilotIndices/17.12.53.23981 +/.vs/procosys-js-frontend/FileContentIndex +/.vs/ProjectSettings.json +/.vs/slnx.sqlite +/.vs/slnx.sqlite-journal diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite deleted file mode 100644 index d716287f3..000000000 Binary files a/.vs/slnx.sqlite and /dev/null differ diff --git a/Dockerfile.prod b/Dockerfile similarity index 51% rename from Dockerfile.prod rename to Dockerfile index 2b882320d..910f1d0d6 100644 --- a/Dockerfile.prod +++ b/Dockerfile @@ -5,15 +5,6 @@ COPY package*.json ./ COPY . . RUN yarn install -ENV VITE_AUTH_CLIENT="50b15344-28c9-45b5-9616-41da16fc9dcc" -ENV VITE_AUTHORITY="https://login.microsoftonline.com/3aa4a235-b6e2-48d5-9195-7fcf05b459b0" -ENV VITE_CONFIG_SCOPE="api://756f2a23-f54d-4643-bb49-62c0db4802ae/Read" -ENV VITE_BASE_URL_MAIN="https://pcs-main-prod.azurewebsites.net/api" -ENV VITE_CONFIG_ENDPOINT="https://pcs-config-prod-func.azurewebsites.net/api/Frontend/Configuration?" -ENV VITE_WEBAPI_SCOPE="api://47641c40-0135-459b-8ab4-459e68dc8d08/web_api" -ENV VITE_APP_INSIGHTS="ed1e9f1c-5b68-44ca-afec-76ece1f08f80" -ENV VITE_API_VERSION="&api-version=4.1" - RUN yarn build --mode=production # production environment @@ -33,4 +24,4 @@ USER 9999 EXPOSE 5000 -CMD ["sh","/etc/scripts/startup.sh"] \ No newline at end of file +CMD ["sh","/etc/scripts/run-nginx.sh"] diff --git a/Dockerfile.dev b/Dockerfile.dev deleted file mode 100644 index c5e1c8b3f..000000000 --- a/Dockerfile.dev +++ /dev/null @@ -1,36 +0,0 @@ -# build environment -FROM node:20 as build -WORKDIR /app -COPY package*.json ./ -COPY . . -RUN yarn install - -ENV VITE_AUTH_CLIENT="30a25122-c22c-4a5c-a7b8-366d31cb2c46" -ENV VITE_AUTHORITY="https://login.microsoftonline.com/3aa4a235-b6e2-48d5-9195-7fcf05b459b0" -ENV VITE_CONFIG_SCOPE="api://756f2a23-f54d-4643-bb49-62c0db4802ae/Read" -ENV VITE_BASE_URL_MAIN="https://pcs-main-api-dev-pr.azurewebsites.net/api" -ENV VITE_CONFIG_ENDPOINT="https://pcs-config-non-prod-func.azurewebsites.net/api/Frontend/Configuration?" -ENV VITE_WEBAPI_SCOPE="api://47641c40-0135-459b-8ab4-459e68dc8d08/web_api" -ENV VITE_APP_INSIGHTS="cdb49dda-63f9-433d-99f3-c73dff5dc6a1" -ENV VITE_API_VERSION="&api-version=4.1" - -RUN yarn build --mode=production - -# production environment -FROM docker.io/nginxinc/nginx-unprivileged:alpine - -WORKDIR /app -## add permissions for nginx user -COPY --from=build /app/build /usr/share/nginx/html -COPY .docker/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf -COPY .docker/scripts/ /etc/scripts/ - -# Change the user from root to non-root- From now on, all Docker commands are run as non-root user (except for COPY) -USER 0 -RUN chown -R nginx /usr/share/nginx/html \ - && chown -R nginx /etc/nginx/conf.d -USER 9999 - -EXPOSE 5000 - -CMD ["sh","/etc/scripts/startup.sh"] \ No newline at end of file diff --git a/Dockerfile.test b/Dockerfile.test deleted file mode 100644 index 1bde8b1bb..000000000 --- a/Dockerfile.test +++ /dev/null @@ -1,37 +0,0 @@ -# build environment -FROM node:20 as build -WORKDIR /app -COPY package*.json ./ -COPY . . -RUN yarn install - -ENV VITE_AUTH_CLIENT="23d8e04c-9362-4870-bda2-dade6f9d0ffb" -ENV VITE_AUTHORITY="https://login.microsoftonline.com/3aa4a235-b6e2-48d5-9195-7fcf05b459b0" -ENV VITE_CONFIG_SCOPE="api://756f2a23-f54d-4643-bb49-62c0db4802ae/Read" -ENV VITE_BASE_URL_MAIN="https://pcs-main-api-test.azurewebsites.net/api" -ENV VITE_CONFIG_ENDPOINT="https://pcs-config-non-prod-func.azurewebsites.net/api/Frontend/Configuration?" -ENV VITE_WEBAPI_SCOPE="api://47641c40-0135-459b-8ab4-459e68dc8d08/web_api" -ENV VITE_APP_INSIGHTS="2e63710e-308d-45c2-99b0-95a959a3de5a" -ENV VITE_API_VERSION="&api-version=4.1" - -RUN yarn build --mode=production - - -# production environment -FROM docker.io/nginxinc/nginx-unprivileged:alpine - -WORKDIR /app -## add permissions for nginx user -COPY --from=build /app/build /usr/share/nginx/html -COPY .docker/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf -COPY .docker/scripts/ /etc/scripts/ - -# Change the user from root to non-root- From now on, all Docker commands are run as non-root user (except for COPY) -USER 0 -RUN chown -R nginx /usr/share/nginx/html \ - && chown -R nginx /etc/nginx/conf.d -USER 9999 - -EXPOSE 5000 - -CMD ["sh","/etc/scripts/startup.sh"] \ No newline at end of file diff --git a/README.md b/README.md index d1565cf35..f4cbaf65a 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,17 @@ $ docker build --force-rm -t pcs:latest -f .docker/Dockerfile . $ docker run -it -p 80:80 pcs:latest ``` +# How to deploy + +## Deploy to dev +When you are ready to deploy your changes to dev/test create a PR and merge it into the development branch. This will trigger a build and deploy to the dev environment. + +## Deploy to test +When you want your changes to test merge the develop branch into the test branch and your changes will be deployed to test. + +## Deploy to prod +When you are ready to deploy to production merge the develop branch into the master branch and your changes will be deployed to production. You can **NOT** merge test to master because test has different styling than production. + # Libraries ### Microsoft Authentication Library (MSAL) @@ -92,4 +103,4 @@ https://webpack.js.org/ ### Browserslist Used to define which browsers we support, as well as integrate with polyfill loading in CSS and Babel. -https://www.npmjs.com/package/browserslist \ No newline at end of file +https://www.npmjs.com/package/browserslist diff --git a/index.html b/index.html new file mode 100644 index 000000000..6bbcb0c89 --- /dev/null +++ b/index.html @@ -0,0 +1,23 @@ + + + + + + + + + ProCoSys + + + + + + diff --git a/radixconfig.yaml b/radixconfig.yaml index c932db73f..d3ad90d78 100644 --- a/radixconfig.yaml +++ b/radixconfig.yaml @@ -18,29 +18,22 @@ spec: from: develop - name: test build: - from: test + from: feat/auth-silent - name: prod build: from: master components: - name: frontend + variables: + AUTH_CONFIG: "" + CONFIGURATION: "" + FEATURE_FLAGS: "" + publicPort: http ports: - name: http port: 5000 environmentConfig: - environment: dev - dockerfileName: Dockerfile.dev - variables: - configurationEndpoint: 'https://pcs-config-non-prod-func.azurewebsites.net/api/Frontend' - configurationScope: 'api://0708e202-b5ad-4d95-9735-a631c715d6a9/Read' - environment: test - dockerfileName: Dockerfile.test - variables: - configurationEndpoint: 'https://pcs-config-non-prod-func.azurewebsites.net/api/Frontend' - configurationScope: 'api://0708e202-b5ad-4d95-9735-a631c715d6a9/Read' - environment: prod - dockerfileName: Dockerfile.prod - variables: - configurationEndpoint: 'https://pcs-config-prod-func.azurewebsites.net/api/Frontend' - configurationScope: 'api://756f2a23-f54d-4643-bb49-62c0db4802ae/Read' diff --git a/src/auth/AuthService.tsx b/src/auth/AuthService.tsx index 56158507d..ecbbdad89 100644 --- a/src/auth/AuthService.tsx +++ b/src/auth/AuthService.tsx @@ -1,11 +1,9 @@ -import * as msal from '@azure/msal-browser'; import { AccountInfo, AuthenticationResult, AuthError, Configuration, EndSessionRequest, - InteractionRequiredAuthError, LogLevel, PopupRequest, PublicClientApplication, @@ -53,6 +51,7 @@ export default class AuthService implements IAuthService { auth: { clientId: ProCoSysSettings.clientId, authority: ProCoSysSettings.authority, + redirectUri: `${window.location.origin}/auth/Preservation` }, cache: { cacheLocation: 'sessionStorage', // This configures where your cache will be stored @@ -94,7 +93,9 @@ export default class AuthService implements IAuthService { }; this.silentLoginRequest = { - loginHint: this.getAccount()?.username, + loginHint: + new URL(window.location.href).searchParams.get('user_name') ?? + this.getAccount()?.username, }; } @@ -132,13 +133,13 @@ export default class AuthService implements IAuthService { async loadAuthModule(): Promise { await this.myMSALObj.initialize(); // handle auth redired/do all initial setup for msal - await this.myMSALObj.handleRedirectPromise(); + // await this.myMSALObj.handleRedirectPromise(); const acc = this.getAccount(); - + // if (acc) { this.myMSALObj.setActiveAccount(acc); } else { - this.myMSALObj.loginRedirect(); + await this.attemptSsoSilent(); } } @@ -162,23 +163,48 @@ export default class AuthService implements IAuthService { * Calls ssoSilent to attempt silent flow. If it fails due to interaction required error, it will prompt the user to login using popup. * @param request */ - attemptSsoSilent(): void { - this.myMSALObj - .ssoSilent(this.silentLoginRequest) - .then(() => { - this.account = this.getAccount(); - if (this.account) { - this.myMSALObj.setActiveAccount(this.account); - } else { - console.log('No account!'); - } - }) - .catch((error) => { - console.error('Silent Error: ' + error); - if (error instanceof InteractionRequiredAuthError) { - this.login('loginPopup'); - } - }); + async attemptSsoSilent(): Promise { + const goBackTo = `${window.location.origin}${window.location.pathname ?? ""}`; + const acc = this.getAccount(); + if (acc) { + this.myMSALObj.setActiveAccount(acc); + return + } + //Fallback with loginhint + const hint = + new URL(window.location.href).searchParams.get('user_name') ?? + this.getAccount()?.username; + + if (hint) { + console.log('Attempting silent login'); + + const silentResult = await this.myMSALObj + .ssoSilent({ + scopes: ['openid', 'profile', 'User.Read'], + loginHint: + new URL(window.location.href).searchParams.get( + 'user_name' + ) ?? this.getAccount()?.username, + }) + .catch(async (error) => { + await this.myMSALObj.clearCache(); + await this.login(); + window.location.href = goBackTo; + }); + if (silentResult) { + this.myMSALObj.setActiveAccount(silentResult.account); + console.log( + 'User authenticated silently:', + silentResult.account + ); + window.location.href = goBackTo; + } else { + console.error('FAILED TO LOGIN USER THIS SHOULD NOT HAPPEN'); + } + } else { + await this.login(); + window.location.href = goBackTo; + } } /** 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/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}