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 (
-
+
{label}
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}
{addIcon} New preservation journey
@@ -820,7 +818,7 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => {
return (
- {baseBreadcrumb} / {newJourney.title}
+ {breadcrumbs} / {newJourney.title}
{newJourney.isVoided && (
{
Save
@@ -896,8 +894,13 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => {
{
}}
placeholder="Write here"
disabled={newJourney.isVoided}
+ style={{
+ minWidth: '250px',
+ width: 'fit-content',
+ }}
/>
+
+
+ `${p.name} - ${p.description}`
+ }
+ onOptionsChange={(options: any): void => {
+ setProjectIdValue(options.selectedItems[0]);
+ }}
+ initialSelectedOptions={[selectedProject]}
+ key={selectedProject?.id}
+ label={'Project'}
+ placeholder={'Select project'}
+ style={{
+ minWidth: '300px',
+ width: 'fit-content',
+ }}
+ >
@@ -927,7 +952,7 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => {
);
return (
-
+
{
data={mappedModes}
label={'Mode'}
disabled={
- newJourney.isVoided ||
+ newJourney.isVoided ??
step.isVoided
}
>
- {(modeSelectItem &&
- modeSelectItem.text) ||
+ {modeSelectItem?.text ??
'Select mode'}
@@ -952,17 +976,17 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => {
{
(respItem, filtRespIndex) => {
return (
@@ -1018,7 +1042,7 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => {
}
placeholder="Write here"
disabled={
- newJourney.isVoided ||
+ newJourney.isVoided ??
step.isVoided
}
/>
@@ -1056,7 +1080,7 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => {
AutoTransferMethod.RFCC
}
disabled={
- newJourney.isVoided ||
+ newJourney.isVoided ??
step.isVoided
}
onChange={(
@@ -1091,7 +1115,7 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => {
AutoTransferMethod.RFOC
}
disabled={
- newJourney.isVoided ||
+ newJourney.isVoided ??
step.isVoided
}
onChange={(
@@ -1128,9 +1152,9 @@ const PreservationJourney = (props: PreservationJourneyProps): JSX.Element => {
<>
{
void;
+};
diff --git a/src/modules/PlantConfig/views/Library/PreservationJourney/types/Step.ts b/src/modules/PlantConfig/views/Library/PreservationJourney/types/Step.ts
new file mode 100644
index 000000000..cb95e60a9
--- /dev/null
+++ b/src/modules/PlantConfig/views/Library/PreservationJourney/types/Step.ts
@@ -0,0 +1,17 @@
+import { Mode } from './Mode';
+
+export 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;
+}
diff --git a/src/modules/PlantConfig/views/Library/PreservationJourney/types/index.ts b/src/modules/PlantConfig/views/Library/PreservationJourney/types/index.ts
new file mode 100644
index 000000000..f13e05e39
--- /dev/null
+++ b/src/modules/PlantConfig/views/Library/PreservationJourney/types/index.ts
@@ -0,0 +1,5 @@
+export { AutoTransferMethod } from './AutoTransferMethod';
+export { Journey } from './Journey';
+export { Mode } from './Mode';
+export { Step } from './Step';
+export { PreservationJourneyProps } from './PreservationJourneyProps';
diff --git a/src/modules/Preservation/http/PreservationApiClient.ts b/src/modules/Preservation/http/PreservationApiClient.ts
index 6bfad265d..cf8fd4003 100644
--- a/src/modules/Preservation/http/PreservationApiClient.ts
+++ b/src/modules/Preservation/http/PreservationApiClient.ts
@@ -6,6 +6,7 @@ import { ProCoSysApiError } from '../../../core/ProCoSysApiError';
import ProCoSysSettings from '../../../core/ProCoSysSettings';
import Qs from 'qs';
import { RequestCanceler } from '../../../http/HttpClient';
+import { ProjectDetails } from '../types';
interface PreservedTagResponse {
maxAvailable: number;
@@ -208,6 +209,7 @@ interface JourneyResponse {
},
];
rowVersion: string;
+ project?: ProjectDetails;
}
interface RequirementTypeResponse {
@@ -1295,12 +1297,14 @@ class PreservationApiClient extends ApiClient {
*/
async getJourneys(
includeVoided: boolean,
- setRequestCanceller?: RequestCanceler
+ setRequestCanceller?: RequestCanceler,
+ projectName?: string
): Promise {
const endpoint = '/Journeys';
const settings: AxiosRequestConfig = {
params: {
includeVoided: includeVoided,
+ projectName: projectName,
},
};
this.setupRequestCanceler(settings, setRequestCanceller);
@@ -1376,6 +1380,7 @@ class PreservationApiClient extends ApiClient {
journeyId: number,
title: string,
rowVersion: string,
+ name?: string,
setRequestCanceller?: RequestCanceler
): Promise {
const endpoint = `/Journeys/${journeyId}`;
@@ -1386,8 +1391,9 @@ class PreservationApiClient extends ApiClient {
await this.client.put(
endpoint,
{
- title: title,
- rowVersion: rowVersion,
+ title,
+ rowVersion,
+ projectName: name,
},
settings
);
diff --git a/src/modules/Preservation/views/AddScope/AddScope.tsx b/src/modules/Preservation/views/AddScope/AddScope.tsx
index 173ea823c..469265ccc 100644
--- a/src/modules/Preservation/views/AddScope/AddScope.tsx
+++ b/src/modules/Preservation/views/AddScope/AddScope.tsx
@@ -188,7 +188,8 @@ const AddScope = (): JSX.Element => {
try {
const data = await apiClient.getJourneys(
false,
- (cancel: Canceler) => (requestCancellor = cancel)
+ (cancel: Canceler) => (requestCancellor = cancel),
+ project.name
);
setJourneys(data);
} catch (error) {
@@ -354,7 +355,12 @@ const AddScope = (): JSX.Element => {
error.message,
error.data
);
- showSnackbarNotification(error.message, 5000);
+ showSnackbarNotification(
+ error.data?.data?.length > 0
+ ? error.data.data[0]
+ : error.message,
+ 10000
+ );
}
setIsSubmittingScope(false);
return Promise.resolve();
diff --git a/src/modules/Preservation/views/ScopeOverview/Dialogs/UpdateJourneyDialog.tsx b/src/modules/Preservation/views/ScopeOverview/Dialogs/UpdateJourneyDialog.tsx
index a9e32bceb..6129b9a9d 100644
--- a/src/modules/Preservation/views/ScopeOverview/Dialogs/UpdateJourneyDialog.tsx
+++ b/src/modules/Preservation/views/ScopeOverview/Dialogs/UpdateJourneyDialog.tsx
@@ -128,7 +128,7 @@ const UpdateJourneyDialog = ({
const [tagJourneyAndStepValid, setTagJourneyAndStepValid] =
useState(false);
const { setDirtyStateFor, unsetDirtyStateFor } = useDirtyContext();
- const { apiClient } = usePreservationContext();
+ const { apiClient, project } = usePreservationContext();
const [journeys, setJourneys] = useState([]);
const [step, setStep] = useState(null);
const [poTag, setPoTag] = useState(false);
@@ -177,7 +177,8 @@ const UpdateJourneyDialog = ({
try {
const data = await apiClient.getJourneys(
true,
- (cancel: Canceler) => (requestCancellor = cancel)
+ (cancel: Canceler) => (requestCancellor = cancel),
+ project.name
);
setJourneys(data);
} catch (error) {
diff --git a/src/modules/Preservation/views/ScopeOverview/ScopeOverview.tsx b/src/modules/Preservation/views/ScopeOverview/ScopeOverview.tsx
index 864d9a0c8..f35501325 100644
--- a/src/modules/Preservation/views/ScopeOverview/ScopeOverview.tsx
+++ b/src/modules/Preservation/views/ScopeOverview/ScopeOverview.tsx
@@ -972,8 +972,6 @@ const ScopeOverview: React.FC = (): JSX.Element => {
);
}
}
- // clear parameters in browser url
- navigate('/');
}, [location]);
const navigateToOldPreservation = (): void => {
diff --git a/webpack.development.config.js b/webpack.development.config.js
index 735a91a97..791314734 100644
--- a/webpack.development.config.js
+++ b/webpack.development.config.js
@@ -106,13 +106,6 @@ module.exports = {
new webpack.ProvidePlugin({
process: 'process/browser',
}),
- /* Automatically creates our index.html page */
- new HtmlWebpackPlugin({
- title: 'ProCoSys',
- meta: {
- viewport: 'width=device-width, height=device-height, initial-scale=1'
- }
- }),
/* Deletes our build directory when building */
new CleanWebpackPlugin()
]
diff --git a/webpack.production.config.js b/webpack.production.config.js
index 3a836eeae..74bf566f8 100644
--- a/webpack.production.config.js
+++ b/webpack.production.config.js
@@ -104,13 +104,6 @@ module.exports = {
new webpack.ProvidePlugin({
process: 'process/browser',
}),
- /* Automatically creates our index.html page */
- new HtmlWebpackPlugin({
- title: 'ProCoSys',
- meta: {
- viewport: 'width=device-width, height=device-height, initial-scale=1'
- }
- }),
/* Deletes our build directory when building */
new CleanWebpackPlugin()
]
diff --git a/yarn.lock b/yarn.lock
index 87fd256dd..b4e8a8f6f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -13043,16 +13043,7 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
-"string-width-cjs@npm:string-width@^4.2.0":
- version "4.2.3"
- resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.1"
-
-"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
+"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -14450,7 +14441,7 @@ worker-rpc@^0.1.0:
dependencies:
microevent.ts "~0.1.1"
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -14468,15 +14459,6 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
-wrap-ansi@^7.0.0:
- version "7.0.0"
- resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz"