Skip to content

Commit 7545185

Browse files
committed
feat(release): Enable LinkedPackagesTab in edit release and add delete package functionality
Signed-off-by: suhas-SHS <[email protected]>
1 parent 51491b2 commit 7545185

File tree

5 files changed

+652
-185
lines changed

5 files changed

+652
-185
lines changed
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
// Copyright (C) Siemens AG, 2025. Part of the SW360 Frontend Project.
2+
3+
// This program and the accompanying materials are made
4+
// available under the terms of the Eclipse Public License 2.0
5+
// which is available at https://www.eclipse.org/legal/epl-2.0/
6+
7+
// SPDX-License-Identifier: EPL-2.0
8+
// License-Filename: LICENSE
9+
10+
'use client'
11+
12+
import { _, Table } from '@/components/sw360'
13+
import LinkPackagesModal from '@/components/sw360/LinkedPackagesModal/LinkPackagesModal'
14+
import { LinkedPackage, LinkedPackageData, Release, ProjectPayload } from '@/object-types'
15+
import CommonUtils from '@/utils/common.utils'
16+
import { ApiUtils } from '@/utils/index'
17+
import { getSession, signOut } from 'next-auth/react'
18+
import { useTranslations } from 'next-intl'
19+
import { useCallback, useEffect, useState } from 'react'
20+
import { OverlayTrigger, Tooltip } from 'react-bootstrap'
21+
import { FaTrashAlt } from 'react-icons/fa'
22+
import { StatusCodes } from 'http-status-codes'
23+
24+
interface ExtendedRelease extends Release {
25+
linkedPackages?: {
26+
packageId?: string
27+
name?: string
28+
version?: string
29+
licenseIds?: string[]
30+
packageManager?: string
31+
comment?: string
32+
}[]
33+
}
34+
35+
interface Props {
36+
releaseId?: string
37+
releasePayload: ExtendedRelease
38+
setReleasePayload: React.Dispatch<React.SetStateAction<ExtendedRelease>>
39+
}
40+
41+
type RowData = (string | string[] | { comment?: string; key?: string } | undefined)[]
42+
43+
export default function EditLinkedPackages({ releaseId, releasePayload, setReleasePayload }: Props) {
44+
const t = useTranslations('default')
45+
const [tableData, setTableData] = useState<Array<RowData>>([])
46+
const [showLinkedPackagesModal, setShowLinkedPackagesModal] = useState(false)
47+
const [linkedPackageMap, setLinkedPackageMap] = useState<Map<string, LinkedPackageData>>(new Map())
48+
const projectPayloadAdapter: ProjectPayload = { id: '', name: '', packageIds: {} }
49+
const noopSetProjectPayload: React.Dispatch<React.SetStateAction<ProjectPayload>> = () => { }
50+
51+
const extractDataFromMap = (dataMap: Map<string, LinkedPackageData>): Array<RowData> => {
52+
const extractedData: Array<RowData> = []
53+
dataMap.forEach((value) => {
54+
extractedData.push([
55+
[value.name, value.packageId],
56+
value.version,
57+
value.licenseIds,
58+
value.packageManager,
59+
{ comment: value.comment || '', key: value.packageId },
60+
value.packageId,
61+
])
62+
})
63+
return extractedData
64+
}
65+
66+
const handleComments = (packageId: string, updatedComment: string, mapData: Map<string, LinkedPackageData>) => {
67+
if (mapData.has(packageId)) {
68+
const updatedMap = new Map(mapData)
69+
const item = updatedMap.get(packageId)
70+
if (item) item.comment = updatedComment
71+
72+
setLinkedPackageMap(updatedMap)
73+
const updatedPayload = { ...releasePayload, linkedPackages: Array.from(updatedMap.values()) }
74+
setReleasePayload(updatedPayload)
75+
setTableData(extractDataFromMap(updatedMap))
76+
}
77+
}
78+
79+
const handleDeletePackage = (packageId: string) => {
80+
const updatedMap = new Map(linkedPackageMap)
81+
updatedMap.delete(packageId)
82+
83+
const updatedPayload = { ...releasePayload, linkedPackages: Array.from(updatedMap.values()) }
84+
setLinkedPackageMap(updatedMap)
85+
setReleasePayload(updatedPayload)
86+
setTableData(extractDataFromMap(updatedMap))
87+
}
88+
89+
const fetchData = useCallback(async () => {
90+
if (!releaseId) return
91+
const session = await getSession()
92+
if (CommonUtils.isNullOrUndefined(session)) return signOut()
93+
94+
const response = await ApiUtils.GET(`releases/${releaseId}?embed=packages`, session.user.access_token)
95+
if (response.status === StatusCodes.OK) {
96+
const data = await response.json()
97+
const embedded = data?._embedded?.['sw360:packages']
98+
if (!embedded || embedded.length === 0) return
99+
100+
const updatedMap = new Map<string, LinkedPackageData>()
101+
embedded.forEach((item: LinkedPackage) => {
102+
updatedMap.set(item.id, {
103+
packageId: item.id,
104+
name: item.name ?? 'Unnamed',
105+
version: item.version ?? 'N/A',
106+
licenseIds: item.licenseIds ?? [],
107+
packageManager: item.packageManager ?? 'N/A',
108+
comment: releasePayload.linkedPackages?.find((p) => p.packageId === item.id)?.comment || '',
109+
})
110+
})
111+
setLinkedPackageMap(updatedMap)
112+
setTableData(extractDataFromMap(updatedMap))
113+
} else if (response.status === StatusCodes.UNAUTHORIZED) {
114+
signOut()
115+
}
116+
}, [releaseId])
117+
118+
useEffect(() => {
119+
fetchData().catch(console.error)
120+
}, [fetchData])
121+
122+
useEffect(() => {
123+
setTableData(extractDataFromMap(linkedPackageMap))
124+
}, [linkedPackageMap])
125+
126+
const handleModalLinkedPackages = (newMap: Map<string, LinkedPackageData>) => {
127+
const merged = new Map(linkedPackageMap)
128+
newMap.forEach((v, key) => merged.set(key, v))
129+
setLinkedPackageMap(merged)
130+
const updatedPayload = {
131+
...releasePayload, linkedPackages: Array.from(merged.values()).map(v => ({
132+
packageId: v.packageId,
133+
name: v.name,
134+
version: v.version,
135+
licenseIds: v.licenseIds,
136+
packageManager: v.packageManager,
137+
comment: v.comment ?? '',
138+
})),
139+
}
140+
setReleasePayload(updatedPayload)
141+
setTableData(extractDataFromMap(merged))
142+
}
143+
144+
const columns = [
145+
{
146+
id: 'linkedPackagesData.name',
147+
name: t('Package Name'),
148+
sort: true,
149+
formatter: ([name, packageId]: [string, string]) =>
150+
_(
151+
<a
152+
href={`/packages/detail/${packageId}`}
153+
target='_blank'
154+
rel='noopener noreferrer'
155+
>
156+
{name}
157+
</a>,
158+
),
159+
},
160+
{ id: 'linkedPackagesData.version', name: t('Package Version'), sort: true },
161+
{ id: 'linkedPackagesData.licenses', name: t('License'), sort: true },
162+
{ id: 'linkedPackagesData.packageManager', name: t('Package Manager'), sort: true },
163+
{
164+
id: 'linkedPackagesData.comment',
165+
name: t('Comments'),
166+
sort: true,
167+
formatter: ({ comment, key }: { comment: string; key: string }) =>
168+
_(
169+
<div className='col-lg-9'>
170+
<input
171+
key={`comment-${key}-${linkedPackageMap.get(key)?.comment || comment}`}
172+
type='text'
173+
className='form-control'
174+
placeholder='Enter comment'
175+
defaultValue={linkedPackageMap.get(key)?.comment || comment}
176+
onBlur={(e) => handleComments(key, e.target.value, linkedPackageMap)}
177+
/>
178+
</div>,
179+
),
180+
},
181+
{
182+
id: 'linkedPackagesData.delete',
183+
name: t('Actions'),
184+
sort: false,
185+
formatter: (packageId: string) =>
186+
_(
187+
<OverlayTrigger overlay={<Tooltip>{t('Delete Package')}</Tooltip>}>
188+
<span className='d-inline-block'>
189+
<FaTrashAlt
190+
className='btn-icon'
191+
onClick={() => handleDeletePackage(packageId)}
192+
style={{ color: 'gray', fontSize: '18px', cursor: 'pointer' }}
193+
/>
194+
</span>
195+
</OverlayTrigger>,
196+
),
197+
},
198+
]
199+
200+
return (
201+
<>
202+
<LinkPackagesModal
203+
show={showLinkedPackagesModal}
204+
setShow={setShowLinkedPackagesModal}
205+
projectPayload={projectPayloadAdapter}
206+
setProjectPayload={noopSetProjectPayload}
207+
setLinkedPackageData={(m: Map<string, LinkedPackageData>) => handleModalLinkedPackages(m)}
208+
/>
209+
<div className='row mb-4'>
210+
<div className='row header-1'>
211+
<h6
212+
className='fw-medium'
213+
style={{ color: '#5D8EA9', paddingLeft: '0px' }}
214+
>
215+
{t('LINKED PACKAGES')}
216+
<hr
217+
className='my-2 mb-2'
218+
style={{ color: '#5D8EA9' }}
219+
/>
220+
</h6>
221+
</div>
222+
<div style={{ paddingLeft: '0px' }}>
223+
<Table
224+
columns={columns}
225+
data={tableData}
226+
sort={false}
227+
/>
228+
</div>
229+
<div
230+
className='row'
231+
style={{ paddingLeft: '0px', marginTop: '10px' }}
232+
>
233+
<div className='col-lg-4'>
234+
<button
235+
type='button'
236+
className='btn btn-secondary'
237+
onClick={() => setShowLinkedPackagesModal(true)}
238+
>
239+
{t('Add Packages')}
240+
</button>
241+
</div>
242+
</div>
243+
</div>
244+
</>
245+
)
246+
}

src/app/[locale]/components/editRelease/[id]/components/EditRelease.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import { ApiUtils, CommonUtils } from '@/utils'
4545
import DeleteReleaseModal from '../../../detail/[id]/components/DeleteReleaseModal'
4646
import EditClearingDetails from './EditClearingDetails'
4747
import EditECCDetails from './EditECCDetails'
48+
import EditLinkedPackages from './EditLinkedPackages'
4849
import EditSPDXDocument from './EditSPDXDocument'
4950
import ReleaseEditSummary from './ReleaseEditSummary'
5051
import ReleaseEditTabs from './ReleaseEditTabs'
@@ -581,6 +582,16 @@ const EditRelease = ({ releaseId, isSPDXFeatureEnabled }: Props): ReactNode => {
581582
setReleasePayload={setReleasePayload}
582583
/>
583584
</div>
585+
<div
586+
className='row'
587+
hidden={selectedTab !== ReleaseTabIds.LINKED_PACKAGES ? true : false}
588+
>
589+
<EditLinkedPackages
590+
releaseId={releaseId}
591+
releasePayload={releasePayload}
592+
setReleasePayload={setReleasePayload}
593+
/>
594+
</div>
584595
<div
585596
className='row'
586597
hidden={selectedTab !== ReleaseTabIds.CLEARING_DETAILS ? true : false}

src/app/[locale]/components/editRelease/[id]/components/ReleaseEditTabs.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ const WITHOUT_COMMERCIAL_DETAILS_AND_SPDX = [
1919
id: ReleaseTabIds.LINKED_RELEASES,
2020
name: 'Linked Releases',
2121
},
22+
{
23+
id: ReleaseTabIds.LINKED_PACKAGES,
24+
name: 'Linked Packages',
25+
},
2226
{
2327
id: ReleaseTabIds.CLEARING_DETAILS,
2428
name: 'Clearing Details',
@@ -50,6 +54,10 @@ const WITH_COMMERCIAL_DETAILS = [
5054
id: ReleaseTabIds.LINKED_RELEASES,
5155
name: 'Linked Releases',
5256
},
57+
{
58+
id: ReleaseTabIds.LINKED_PACKAGES,
59+
name: 'Linked Packages',
60+
},
5361
{
5462
id: ReleaseTabIds.CLEARING_DETAILS,
5563
name: 'Clearing Details',
@@ -89,6 +97,10 @@ const WITH_SPDX = [
8997
id: ReleaseTabIds.LINKED_RELEASES,
9098
name: 'Linked Releases',
9199
},
100+
{
101+
id: ReleaseTabIds.LINKED_PACKAGES,
102+
name: 'Linked Packages',
103+
},
92104
{
93105
id: ReleaseTabIds.CLEARING_DETAILS,
94106
name: 'Clearing Details',
@@ -124,6 +136,10 @@ const WITH_COMMERCIAL_DETAILS_AND_SPDX = [
124136
id: ReleaseTabIds.LINKED_RELEASES,
125137
name: 'Linked Releases',
126138
},
139+
{
140+
id: ReleaseTabIds.LINKED_PACKAGES,
141+
name: 'Linked Packages',
142+
},
127143
{
128144
id: ReleaseTabIds.CLEARING_DETAILS,
129145
name: 'Clearing Details',

0 commit comments

Comments
 (0)