diff --git a/src/app/[locale]/components/editRelease/[id]/components/EditLinkedPackages.tsx b/src/app/[locale]/components/editRelease/[id]/components/EditLinkedPackages.tsx new file mode 100644 index 000000000..fdc6d4d88 --- /dev/null +++ b/src/app/[locale]/components/editRelease/[id]/components/EditLinkedPackages.tsx @@ -0,0 +1,246 @@ +// Copyright (C) Siemens AG, 2025. Part of the SW360 Frontend Project. + +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ + +// SPDX-License-Identifier: EPL-2.0 +// License-Filename: LICENSE + +'use client' + +import { _, Table } from '@/components/sw360' +import LinkPackagesModal from '@/components/sw360/LinkedPackagesModal/LinkPackagesModal' +import { LinkedPackage, LinkedPackageData, Release, ProjectPayload } from '@/object-types' +import CommonUtils from '@/utils/common.utils' +import { ApiUtils } from '@/utils/index' +import { getSession, signOut } from 'next-auth/react' +import { useTranslations } from 'next-intl' +import { useCallback, useEffect, useState } from 'react' +import { OverlayTrigger, Tooltip } from 'react-bootstrap' +import { FaTrashAlt } from 'react-icons/fa' +import { StatusCodes } from 'http-status-codes' + +interface ExtendedRelease extends Release { + linkedPackages?: { + packageId?: string + name?: string + version?: string + licenseIds?: string[] + packageManager?: string + comment?: string + }[] +} + +interface Props { + releaseId?: string + releasePayload: ExtendedRelease + setReleasePayload: React.Dispatch> +} + +type RowData = (string | string[] | { comment?: string; key?: string } | undefined)[] + +export default function EditLinkedPackages({ releaseId, releasePayload, setReleasePayload }: Props) { + const t = useTranslations('default') + const [tableData, setTableData] = useState>([]) + const [showLinkedPackagesModal, setShowLinkedPackagesModal] = useState(false) + const [linkedPackageMap, setLinkedPackageMap] = useState>(new Map()) + const projectPayloadAdapter: ProjectPayload = { id: '', name: '', packageIds: {} } + const noopSetProjectPayload: React.Dispatch> = () => { } + + const extractDataFromMap = (dataMap: Map): Array => { + const extractedData: Array = [] + dataMap.forEach((value) => { + extractedData.push([ + [value.name, value.packageId], + value.version, + value.licenseIds, + value.packageManager, + { comment: value.comment || '', key: value.packageId }, + value.packageId, + ]) + }) + return extractedData + } + + const handleComments = (packageId: string, updatedComment: string, mapData: Map) => { + if (mapData.has(packageId)) { + const updatedMap = new Map(mapData) + const item = updatedMap.get(packageId) + if (item) item.comment = updatedComment + + setLinkedPackageMap(updatedMap) + const updatedPayload = { ...releasePayload, linkedPackages: Array.from(updatedMap.values()) } + setReleasePayload(updatedPayload) + setTableData(extractDataFromMap(updatedMap)) + } + } + + const handleDeletePackage = (packageId: string) => { + const updatedMap = new Map(linkedPackageMap) + updatedMap.delete(packageId) + + const updatedPayload = { ...releasePayload, linkedPackages: Array.from(updatedMap.values()) } + setLinkedPackageMap(updatedMap) + setReleasePayload(updatedPayload) + setTableData(extractDataFromMap(updatedMap)) + } + + const fetchData = useCallback(async () => { + if (!releaseId) return + const session = await getSession() + if (CommonUtils.isNullOrUndefined(session)) return signOut() + + const response = await ApiUtils.GET(`releases/${releaseId}?embed=packages`, session.user.access_token) + if (response.status === StatusCodes.OK) { + const data = await response.json() + const embedded = data?._embedded?.['sw360:packages'] + if (!embedded || embedded.length === 0) return + + const updatedMap = new Map() + embedded.forEach((item: LinkedPackage) => { + updatedMap.set(item.id, { + packageId: item.id, + name: item.name ?? 'Unnamed', + version: item.version ?? 'N/A', + licenseIds: item.licenseIds ?? [], + packageManager: item.packageManager ?? 'N/A', + comment: releasePayload.linkedPackages?.find((p) => p.packageId === item.id)?.comment || '', + }) + }) + setLinkedPackageMap(updatedMap) + setTableData(extractDataFromMap(updatedMap)) + } else if (response.status === StatusCodes.UNAUTHORIZED) { + signOut() + } + }, [releaseId]) + + useEffect(() => { + fetchData().catch(console.error) + }, [fetchData]) + + useEffect(() => { + setTableData(extractDataFromMap(linkedPackageMap)) + }, [linkedPackageMap]) + + const handleModalLinkedPackages = (newMap: Map) => { + const merged = new Map(linkedPackageMap) + newMap.forEach((v, key) => merged.set(key, v)) + setLinkedPackageMap(merged) + const updatedPayload = { + ...releasePayload, linkedPackages: Array.from(merged.values()).map(v => ({ + packageId: v.packageId, + name: v.name, + version: v.version, + licenseIds: v.licenseIds, + packageManager: v.packageManager, + comment: v.comment ?? '', + })), + } + setReleasePayload(updatedPayload) + setTableData(extractDataFromMap(merged)) + } + + const columns = [ + { + id: 'linkedPackagesData.name', + name: t('Package Name'), + sort: true, + formatter: ([name, packageId]: [string, string]) => + _( + + {name} + , + ), + }, + { id: 'linkedPackagesData.version', name: t('Package Version'), sort: true }, + { id: 'linkedPackagesData.licenses', name: t('License'), sort: true }, + { id: 'linkedPackagesData.packageManager', name: t('Package Manager'), sort: true }, + { + id: 'linkedPackagesData.comment', + name: t('Comments'), + sort: true, + formatter: ({ comment, key }: { comment: string; key: string }) => + _( +
+ handleComments(key, e.target.value, linkedPackageMap)} + /> +
, + ), + }, + { + id: 'linkedPackagesData.delete', + name: t('Actions'), + sort: false, + formatter: (packageId: string) => + _( + {t('Delete Package')}}> + + handleDeletePackage(packageId)} + style={{ color: 'gray', fontSize: '18px', cursor: 'pointer' }} + /> + + , + ), + }, + ] + + return ( + <> + ) => handleModalLinkedPackages(m)} + /> +
+
+
+ {t('LINKED PACKAGES')} +
+
+
+
+ + +
+
+ +
+
+ + + ) +} diff --git a/src/app/[locale]/components/editRelease/[id]/components/EditRelease.tsx b/src/app/[locale]/components/editRelease/[id]/components/EditRelease.tsx index 42838d4a8..561a70423 100644 --- a/src/app/[locale]/components/editRelease/[id]/components/EditRelease.tsx +++ b/src/app/[locale]/components/editRelease/[id]/components/EditRelease.tsx @@ -45,6 +45,7 @@ import { ApiUtils, CommonUtils } from '@/utils' import DeleteReleaseModal from '../../../detail/[id]/components/DeleteReleaseModal' import EditClearingDetails from './EditClearingDetails' import EditECCDetails from './EditECCDetails' +import EditLinkedPackages from './EditLinkedPackages' import EditSPDXDocument from './EditSPDXDocument' import ReleaseEditSummary from './ReleaseEditSummary' import ReleaseEditTabs from './ReleaseEditTabs' @@ -581,6 +582,16 @@ const EditRelease = ({ releaseId, isSPDXFeatureEnabled }: Props): ReactNode => { setReleasePayload={setReleasePayload} /> +
- - - ) : ( -
-
- )} - + + + { + setShowModal(false) + setSelectedPkgId(null) + setAlert(null) + setDeleting(false) + }} + aria-labelledby='delete-package-modal' + scrollable + > + + {t('Delete Package')} + + + + {alert && {alert.message}} + {!alert &&

{t('Do you really want to delete the package')}?

} +
+ + + {alert ? ( + + ) : ( + <> + + + + )} + +
+ ) } diff --git a/src/components/sw360/LinkedPackagesModal/LinkPackagesModal.tsx b/src/components/sw360/LinkedPackagesModal/LinkPackagesModal.tsx index 2e26efb1c..e721b68cc 100644 --- a/src/components/sw360/LinkedPackagesModal/LinkPackagesModal.tsx +++ b/src/components/sw360/LinkedPackagesModal/LinkPackagesModal.tsx @@ -31,7 +31,7 @@ import MessageService from '@/services/message.service' import { ApiUtils, CommonUtils } from '@/utils' interface Props { - setLinkedPackageData: React.Dispatch>> + setLinkedPackageData: (map: Map) => void projectPayload: ProjectPayload setProjectPayload: React.Dispatch> show: boolean @@ -408,23 +408,23 @@ export default function LinkPackagesModal({