diff --git a/src/assets/locales/de/main.json b/src/assets/locales/de/main.json index 4c34a1faf..b566cf13b 100644 --- a/src/assets/locales/de/main.json +++ b/src/assets/locales/de/main.json @@ -2046,9 +2046,22 @@ }, "companyData": { "label": "Company Data", + "csvUploadBtn": "Upload CSV", "statusInfo": { "title": "Status Information" }, + "upload": { + "title": "Upload CSV", + "fileSizeError": "Uploaded file is too big. Maximum 1MB is allowed", + "successDescription": "CSV Upload is success. Please close the modal to see updated list", + "note": "Note: Please upload only CSV files with maximum 1 MB.", + "errorDescription": "CSV Upload failed with the following erros", + "templateBtn": "Download CSV Template", + "emptyError": "Uploaded CSV is empty. Kindly check and upload again", + "copy": "Copy Error to clipboard", + "copySuccess": "Error messages copied successfully", + "downloadSuccess": "CSV template download successfully" + }, "companyInfo": { "title": "Company Name", "legalEntityName": "Legal Entity Name", @@ -2066,7 +2079,9 @@ "location": "Location", "search": "search address / site", "noRowsMsg": "No Data yet", - "type": "Type" + "type": "Type", + "status": "Status", + "details": "Details" }, "site": { "title": "Create new site", diff --git a/src/assets/locales/en/main.json b/src/assets/locales/en/main.json index 86952ed00..aef08c23b 100644 --- a/src/assets/locales/en/main.json +++ b/src/assets/locales/en/main.json @@ -2013,9 +2013,22 @@ }, "companyData": { "label": "Company Data", + "csvUploadBtn": "Upload CSV", "statusInfo": { "title": "Status Information" }, + "upload": { + "title": "Upload CSV", + "fileSizeError": "Uploaded file is too big. Maximum 1MB is allowed", + "successDescription": "CSV Upload is success. Please close the modal to see updated list", + "note": "Note: Please upload only CSV files with maximum 1 MB.", + "errorDescription": "CSV Upload failed with the following erros", + "templateBtn": "Download CSV Template", + "emptyError": "Uploaded CSV is empty. Kindly check and upload again", + "copy": "Copy Error to clipboard", + "copySuccess": "Error messages copied successfully", + "downloadSuccess": "CSV template download successfully" + }, "companyInfo": { "title": "Company Name", "legalEntityName": "Legal Entity Name", @@ -2033,7 +2046,9 @@ "location": "Location", "search": "search address / site", "noRowsMsg": "No Data yet", - "type": "Type" + "type": "Type", + "status": "Status", + "details": "Details" }, "site": { "title": "Create new site", diff --git a/src/components/overlays/CSVUploadOverlay/index.tsx b/src/components/overlays/CSVUploadOverlay/index.tsx new file mode 100644 index 000000000..d2a514718 --- /dev/null +++ b/src/components/overlays/CSVUploadOverlay/index.tsx @@ -0,0 +1,291 @@ +/******************************************************************************** + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + Dialog, + Button, + DialogActions, + DialogContent, + DialogHeader, + DropArea, + type DropAreaProps, + LoadingButton, + Typography, +} from '@catena-x/portal-shared-components' +import { useUploadCSVMutation } from 'features/companyData/companyDataApiSlice' +import { Dropzone } from 'components/shared/basic/Dropzone' +import { useDispatch } from 'react-redux' +import { closeOverlay } from 'features/control/overlay' +import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined' +import UserService from 'services/UserService' +import { getBpdmGateApiBase } from 'services/EnvironmentService' +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline' +import CheckCircleOutlinedIcon from '@mui/icons-material/CheckCircleOutlined' +import WarningAmberOutlinedIcon from '@mui/icons-material/WarningAmberOutlined' +import { success as CopySuccess } from 'services/NotifyService' + +export default function CSVUploadOverlay(): JSX.Element { + const { t } = useTranslation() + const dispatch = useDispatch() + const [uploadedFile, setUploadedFile] = useState() + const [loading, setLoading] = useState(false) + const [uploadCSV] = useUploadCSVMutation() + const [success, setSuccess] = useState(false) + const [error, setError] = useState(false) + const [errorResponse, setErrorResponse] = useState([]) + + const renderDropArea = (props: DropAreaProps): JSX.Element => { + return + } + + const handleSubmit = async (): Promise => { + setLoading(true) + try { + if (uploadedFile != null) { + await uploadCSV(uploadedFile) + .unwrap() + .then((response) => { + if (response.length > 0) { + setSuccess(true) + } else { + setError(true) + setErrorResponse([]) + } + }) + setLoading(false) + } + // Add an ESLint exception until there is a solution + // eslint-disable-next-line + } catch (err: any) { + setLoading(false) + setErrorResponse(err?.data?.error ?? []) + setError(true) + } + } + + const handleClose = () => { + dispatch(closeOverlay()) + } + + const downloadTemplate = () => { + const url = `${getBpdmGateApiBase()}/input/partner-upload-template` + return fetch(url, { + method: 'GET', + headers: { + authorization: `Bearer ${UserService.getToken()}`, + }, + }) + .then((res) => res.blob()) + .then((blob) => { + const url = window.URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = 'upload_template.csv' + document.body.appendChild(a) + a.click() + a.remove() + CopySuccess(t('content.companyData.upload.downloadSuccess')) + }) + } + + const getIcon = () => { + if (success) + return + else if (error) + return + return <> + } + + return ( + + { + handleClose() + }, + iconComponent: getIcon(), + }} + /> + {success && ( + +
+ + {t('content.companyData.upload.successDescription')} + +
+
+ )} + {error && ( + +
+ + {t('content.companyData.upload.errorDescription')} + +
+ {errorResponse?.length > 0 ? ( + <> + {errorResponse.map((text, i) => ( + + {text} + + ))} + + ) : ( + + {t('content.companyData.upload.emptyError')} + + )} +
+
+ +
+
+
+ )} + {!success && !error && ( + +
+ +
+ { + setUploadedFile(file) + }} + errorText={t('content.companyData.upload.fileSizeError')} + DropStatusHeader={false} + DropArea={renderDropArea} + /> +
+ + {t('content.companyData.upload.note')} + +
+
+ )} + {success && ( + + + + )} + {!success && !error && ( + + + + {loading ? ( + { + // do nothing + }} + sx={{ marginLeft: '10px' }} + /> + ) : ( + + )} + + )} +
+ ) +} diff --git a/src/components/pages/CompanyData/components/CompanyAddressList.tsx b/src/components/pages/CompanyData/components/CompanyAddressList.tsx index a2e4ec14f..816ae07fe 100644 --- a/src/components/pages/CompanyData/components/CompanyAddressList.tsx +++ b/src/components/pages/CompanyData/components/CompanyAddressList.tsx @@ -19,7 +19,7 @@ import { Chip, IconButton, Table } from '@catena-x/portal-shared-components' import { useEffect, useState } from 'react' -import { useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { Box } from '@mui/material' import { useTranslation } from 'react-i18next' import { @@ -29,6 +29,7 @@ import { useFetchOutputCompanyBusinessPartnersMutation, useFetchSharingStateQuery, AddressType, + type SharingStateType, } from 'features/companyData/companyDataApiSlice' import HourglassBottomIcon from '@mui/icons-material/HourglassBottom' import WarningAmberIcon from '@mui/icons-material/WarningAmber' @@ -37,32 +38,34 @@ import ArrowForwardIcon from '@mui/icons-material/ArrowForward' import { type GridCellParams } from '@mui/x-data-grid' import DetailsOverlay from './DetailsOverlay' import { + companyRefetch, + setCompanyPageRefetch, setSelectedCompanyData, setSelectedCompanyStatus, setSharingStateInfo, } from 'features/companyData/slice' -import LoadingProgress from 'components/shared/basic/LoadingProgress' import { statusColorMap } from 'utils/dataMapper' export const CompanyAddressList = ({ handleButtonClick, handleSecondButtonClick, - refetch = false, handleConfirm, }: { handleButtonClick: () => void handleSecondButtonClick: () => void - refetch: boolean handleConfirm: () => void }) => { const { t } = useTranslation() + const [page, setPage] = useState(0) const { data, - refetch: refreshSharingData, isFetching, error: sharingStateError, - } = useFetchSharingStateQuery() - const sharingStates = data?.content + refetch: refetchSharingState, + } = useFetchSharingStateQuery({ + page, + }) + const [sharingStates, setSharingStates] = useState([]) const [outputRequest, { isLoading: isOutputLoading, error: outputError }] = useFetchOutputCompanyBusinessPartnersMutation() const [inputRequest, { isLoading: isInputLoading, error: inputError }] = @@ -71,6 +74,7 @@ export const CompanyAddressList = ({ const [inputs, setInputs] = useState([]) const [details, setDetails] = useState(false) const dispatch = useDispatch() + const refetch = useSelector(companyRefetch) const getInputItems = async () => { const params = sharingStates @@ -113,14 +117,24 @@ export const CompanyAddressList = ({ useEffect(() => { if (refetch) { - refreshSharingData() setInputs([]) setOutputs([]) + setPage(0) + refetchSharingState() + dispatch(setCompanyPageRefetch(false)) } getInputItems() getOutputItems() }, [sharingStates, refetch]) + useEffect(() => { + if (data) { + setSharingStates((i) => + page === 0 ? data.content : i.concat(data.content) + ) + } + }, [data]) + const getStatus = (id: string) => sharingStates?.filter((state) => id === state.externalId)[0] .sharingStateType @@ -164,8 +178,14 @@ export const CompanyAddressList = ({ return ( <> - {!isFetching && !isOutputLoading && !isInputLoading ? ( + {sharingStates.length > 0 && ( page + 1} + nextPage={() => { + setPage((i) => i + 1) + }} + hideFooterPagination={true} autoFocus={false} onButtonClick={handleButtonClick} rowsCount={inputs.length + outputs.length} @@ -181,7 +201,7 @@ export const CompanyAddressList = ({ getRowId={(row: { [key: string]: string }) => row.createdAt} rows={inputs.concat(outputs)} onCellClick={onRowClick} - error={errorObj} + error={errorObj.status === 0 ? null : errorObj} columns={[ { field: 'site', @@ -197,7 +217,7 @@ export const CompanyAddressList = ({ headerAlign: 'left', align: 'left', headerName: t('content.companyData.table.location'), - flex: 2.5, + flex: 2, valueGetter: ({ row }: { row: CompanyDataType }) => row.address ? `${row.address.name ?? ''} ${row.address.physicalPostalAddress.street?.name ?? ''} ${row.address.physicalPostalAddress.street?.houseNumber ?? ''} ${row.address.physicalPostalAddress.city ?? ''} ${row.address.physicalPostalAddress.postalCode ?? ''} ${row.address.physicalPostalAddress.country ?? ''}` @@ -216,7 +236,7 @@ export const CompanyAddressList = ({ }, { field: 'status', - headerName: '', + headerName: t('content.companyData.table.status'), align: 'left', flex: 1, renderCell: ({ row }: { row: CompanyDataType }) => { @@ -252,9 +272,9 @@ export const CompanyAddressList = ({ }, { field: 'details', - headerName: '', + headerName: t('content.companyData.table.details'), align: 'left', - flex: 0.5, + flex: 1, renderCell: () => { return ( - ) : ( - - - )} {details && ( { + setSharingStates([]) + handleConfirm() + }} /> )} diff --git a/src/components/pages/CompanyData/index.tsx b/src/components/pages/CompanyData/index.tsx index 8088f3180..167888a1d 100644 --- a/src/components/pages/CompanyData/index.tsx +++ b/src/components/pages/CompanyData/index.tsx @@ -21,13 +21,20 @@ import { CompanyAddressList } from './components/CompanyAddressList' import { useState } from 'react' import MyCompanyInfoComponent from '../Organization/MyCompanyInfoComponent' import EditForm from './components/EditForm' +import { useDispatch } from 'react-redux' +import { setCompanyPageRefetch } from 'features/companyData/slice' +import { Button } from '@catena-x/portal-shared-components' +import { useTranslation } from 'react-i18next' +import { show } from 'features/control/overlay' +import { OVERLAYS } from 'types/Constants' export default function CompanyData() { + const { t } = useTranslation() const [showOverlay, setShowOverlay] = useState({ address: false, site: false, }) - const [refetch, setRefetch] = useState(0) + const dispatch = useDispatch() const updateOverlay = () => { setShowOverlay((old) => { @@ -41,6 +48,16 @@ export default function CompanyData() {
+
+ +
{ @@ -55,9 +72,8 @@ export default function CompanyData() { return { ...old } }) }} - refetch={refetch !== 0} handleConfirm={() => { - setRefetch(Date.now()) + dispatch(setCompanyPageRefetch(true)) }} /> { - setRefetch(Date.now()) + dispatch(setCompanyPageRefetch(true)) }} />
diff --git a/src/features/companyData/companyDataApiSlice.tsx b/src/features/companyData/companyDataApiSlice.tsx index 6dfe4cc7a..a3efef5c0 100644 --- a/src/features/companyData/companyDataApiSlice.tsx +++ b/src/features/companyData/companyDataApiSlice.tsx @@ -215,15 +215,28 @@ export interface ReadyStateRequestBody { externalIds: string[] } +interface SharingStateRequest { + page: number +} + +enum TAGS { + SHARING = 'sharing', +} + export const apiSlice = createApi({ reducerPath: 'rtk/companyData', baseQuery: fetchBaseQuery(apiBpdmGateQuery()), + tagTypes: [TAGS.SHARING], endpoints: (builder) => ({ - fetchSharingState: builder.query({ - query: () => ({ - url: '/sharing-state?page=0&size=100', - }), - }), + fetchSharingState: builder.query( + { + query: (obj) => ({ + url: `/sharing-state?page=${obj.page}&size=100`, + }), + keepUnusedDataFor: 5, + providesTags: [TAGS.SHARING], + } + ), fetchInputCompanyBusinessPartners: builder.mutation< CompanyDataResponse, string[] | void @@ -264,6 +277,22 @@ export const apiSlice = createApi({ body: data, }), }), + uploadCSV: builder.mutation({ + query: (body) => { + const formData = new FormData() + formData.append('file', body) + formData.append('type', body.type) + return { + url: '/input/partner-upload-process', + method: 'POST', + body: formData, + } + }, + invalidatesTags: [TAGS.SHARING], + }), + downloadCsv: builder.query({ + query: () => '/input/partner-upload-template', + }), }), }) @@ -273,4 +302,6 @@ export const { useFetchOutputCompanyBusinessPartnersMutation, useUpdateCompanySiteAndAddressMutation, useUpdateCompanyStatusToReadyMutation, + useUploadCSVMutation, + useDownloadCsvQuery, } = apiSlice diff --git a/src/features/companyData/slice.ts b/src/features/companyData/slice.ts index 6e1d507a2..d50ebf0f1 100644 --- a/src/features/companyData/slice.ts +++ b/src/features/companyData/slice.ts @@ -31,6 +31,7 @@ export interface CompanyDataState { row: CompanyDataType status: string sharingStateInfo: SharingStateType + refetchState: boolean } export const companyDataInitialData: CompanyDataType = { @@ -158,6 +159,7 @@ export const initialState: CompanyDataState = { sharingProcessStarted: '', taskId: '', }, + refetchState: false, } const companyDataSlice = createSlice({ @@ -176,6 +178,10 @@ const companyDataSlice = createSlice({ ...state, sharingStateInfo: actions.payload, }), + setCompanyPageRefetch: (state, actions) => ({ + ...state, + refetchState: actions.payload, + }), }, }) @@ -188,10 +194,14 @@ export const statusSelector = (state: RootState): string => export const sharingStateInfoSelector = (state: RootState): SharingStateType => state.companyData.sharingStateInfo +export const companyRefetch = (state: RootState): boolean => + state.companyData.refetchState + export const { setSelectedCompanyData, setSelectedCompanyStatus, setSharingStateInfo, + setCompanyPageRefetch, } = companyDataSlice.actions export default companyDataSlice diff --git a/src/services/AccessService.tsx b/src/services/AccessService.tsx index 50bbd0967..69e5549cd 100644 --- a/src/services/AccessService.tsx +++ b/src/services/AccessService.tsx @@ -70,6 +70,7 @@ import CompanyCertificateDetails from 'components/overlays/CompanyCertificateDet import DeleteCompanyCertificateConfirmationOverlay from 'components/overlays/CompanyCertificateDetails/DeleteCompanyCertificateConfirmationOverlay' import { DisableManagedIDP } from 'components/overlays/EnableIDP/DisableManagedIdp' import { DeleteManagedIDP } from 'components/overlays/IDPDelete/DeleteManagedIdp' +import CSVUploadOverlay from 'components/overlays/CSVUploadOverlay' let pageMap: { [page: string]: IPage } let actionMap: { [action: string]: IAction } @@ -209,6 +210,8 @@ export const getOverlay = (overlay: OverlayState) => { title={overlay.title ?? ''} /> ) + case OVERLAYS.CSV_UPLOAD_OVERLAY: + return default: return } diff --git a/src/types/Config.tsx b/src/types/Config.tsx index 02a48c5bf..041a1f142 100644 --- a/src/types/Config.tsx +++ b/src/types/Config.tsx @@ -726,6 +726,9 @@ export const ALL_OVERLAYS: IOverlay[] = [ { name: OVERLAYS.COMPANY_CERTIFICATE_CONFIRM_DELETE, }, + { + name: OVERLAYS.CSV_UPLOAD_OVERLAY, + }, ] export const ALL_ACTIONS: IAction[] = [ diff --git a/src/types/Constants.ts b/src/types/Constants.ts index 6fb5b4605..4e1b491c6 100644 --- a/src/types/Constants.ts +++ b/src/types/Constants.ts @@ -156,6 +156,7 @@ export enum OVERLAYS { UPDATE_CERTIFICATE = 'updateCertificate', COMPANY_CERTIFICATE_DETAILS = 'companyCertificateDetails', COMPANY_CERTIFICATE_CONFIRM_DELETE = 'companyCertificateConfirmDelete', + CSV_UPLOAD_OVERLAY = 'csvUploadOverlay', } export enum ACTIONS {