Repository: opossum-tool/OpossumUI
Pull request #2443 (2023-12-21 16:06:20)

Fix integration of @yarnpkg/plugin-spdx
- Add spdx file to .gitignore in order to avoid accidentally checking the file in when creating it locally.
- Remove verification of name in SPDX file from CI workflow. If the document creation does not work correctly, the plugin should throw an error. So, there is no need to test that here. Besides, this test would fail if the app name would be changed in the package.json.

### Summary of changes

- Add spdx file to .gitignore
- Remove verification of name in SPDX file from CI workflow

### Context and reason for change

- We add the spdx file to .gitignore in order to avoid accidentally checking the file in when creating it locally.
- If the document creation does not work correctly, the plugin should throw an error. So, there is no need to test that in the CI workflow. Besides, this test would fail if the app name would be changed in the package.json.

### How can the changes be tested

- Create an SPDX file with `yarn spdx` and verify that it's not checked in.
- Run the CI workflow.

Note: Please review the [guidelines for contributing](https://github.com/opossum-tool/OpossumUI/blob/main/CONTRIBUTING.md) to this repository.



Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index 07baf3a5..99d12884 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -37,8 +37,6 @@ const sharedRules = {
     { allowBoolean: true, allowNumber: true, allowNever: true },
   ],
   '@typescript-eslint/switch-exhaustiveness-check': 'error',
-  'jest/consistent-test-it': 'error',
-  'jest/prefer-spy-on': 'error',
   'filenames/match-regex': [
     'error',
     '^[a-zA-Z0-9\\-]+(.d|.test|.style)?$',
@@ -116,7 +114,6 @@ module.exports = {
     'plugin:jest/recommended',
     'plugin:jest/style',
     'plugin:jest-dom/recommended',
-    'plugin:@tanstack/eslint-plugin-query/recommended',
   ],
   globals: {
     Atomics: 'readonly',
diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml
index 323e64e4..77e5aeee 100644
--- a/.github/workflows/publish-release.yml
+++ b/.github/workflows/publish-release.yml
@@ -51,4 +51,4 @@ jobs:
       - name: Upload SPDX document
         uses: softprops/action-gh-release@v1
         with:
-          files: opossum-up.spdx.json
+          files: opossum-ui.spdx.json
diff --git a/.gitignore b/.gitignore
index 6986a28b..28c72c9f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,6 +24,7 @@ playwright-report
 # release
 /dist-electron
 /release
+opossum-ui.spdx.json
 
 # misc
 .DS_Store
diff --git a/example-files/opossum_input.json b/example-files/opossum_input.json
index 5964eb52..bfcef1ce 100644
--- a/example-files/opossum_input.json
+++ b/example-files/opossum_input.json
@@ -161,8 +161,7 @@
       },
       "attributionConfidence": 100,
       "comment": "TypeScript. \nhttps://www.npmjs.com/package/typescript",
-      "packageName": "typescript",
-      "packageNamespace": "microsoft",
+      "packageName": "TypeScript",
       "preSelected": true,
       "criticality": "medium",
       "packageVersion": "3.9.6",
@@ -179,8 +178,7 @@
       },
       "attributionConfidence": 100,
       "comment": "TypeScript. \nhttps://www.npmjs.com/package/typescript",
-      "packageName": "typescript",
-      "packageNamespace": "microsoft",
+      "packageName": "TypeScript",
       "preSelected": true,
       "criticality": "medium",
       "packageVersion": "3.9.6",
@@ -196,8 +194,7 @@
       },
       "attributionConfidence": 100,
       "comment": "TypeScript. \nhttps://www.npmjs.com/package/typescript",
-      "packageName": "typescript",
-      "packageNamespace": "microsoft",
+      "packageName": "TypeScript",
       "preSelected": true,
       "criticality": "medium",
       "packageVersion": "3.9.6",
@@ -213,8 +210,7 @@
       },
       "attributionConfidence": 100,
       "comment": "TypeScript. \nhttps://www.npmjs.com/package/typescript",
-      "packageName": "typescript",
-      "packageNamespace": "microsoft",
+      "packageName": "TypeScript",
       "packageType": "github",
       "preSelected": true,
       "criticality": "medium",
diff --git a/jest.config.js b/jest.config.js
index b3ab3471..c7df668c 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -9,8 +9,6 @@ module.exports = {
   transformIgnorePatterns: ['/node_modules/(?!d3-*|internmap|axios)'],
   setupFilesAfterEnv: ['./src/shared/setupTests.ts'],
   watchAll: false,
-  clearMocks: true,
-  setupFiles: ['whatwg-fetch'],
   roots: ['<rootDir>/src/Frontend', '<rootDir>/src/ElectronBackend'],
   modulePathIgnorePatterns: ['<rootDir>/build/'],
   watchPlugins: [
diff --git a/package.json b/package.json
index 80ec2b6d..5a2ad376 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
   "private": true,
   "homepage": "./",
   "dependencies": {
+    "@babel/preset-react": "^7.23.3",
     "@emotion/react": "^11.11.1",
     "@emotion/styled": "^11.11.0",
     "@mui/icons-material": "^5.15.1",
@@ -15,7 +16,6 @@
     "@tanstack/react-query": "^5.14.2",
     "axios": "^1.6.2",
     "buffer": "^6.0.3",
-    "compare-versions": "^6.1.0",
     "dayjs": "^1.11.10",
     "electron-devtools-installer": "^3.2.0",
     "electron-is-dev": "^2.0.0",
@@ -28,6 +28,7 @@
     "lodash": "^4.17.21",
     "object-hash": "^3.0.0",
     "packageurl-js": "^1.2.1",
+    "prop-types": "^15.8.1",
     "proxy-memoize": "^2.0.4",
     "re-resizable": "^6.9.11",
     "react": "^18.2.0",
@@ -45,11 +46,9 @@
   "devDependencies": {
     "@babel/core": "^7.23.6",
     "@babel/preset-env": "^7.23.6",
-    "@babel/preset-react": "^7.23.3",
     "@babel/preset-typescript": "^7.23.3",
     "@faker-js/faker": "^8.3.1",
     "@playwright/test": "^1.40.1",
-    "@tanstack/eslint-plugin-query": "^5.12.1",
     "@testing-library/dom": "^9.3.3",
     "@testing-library/jest-dom": "^6.1.5",
     "@testing-library/react": "^14.1.2",
@@ -104,8 +103,7 @@
     "vite": "^5.0.10",
     "vite-plugin-electron": "^0.15.5",
     "vite-plugin-svgr": "^4.2.0",
-    "vite-tsconfig-paths": "^4.2.2",
-    "whatwg-fetch": "^3.6.19"
+    "vite-tsconfig-paths": "^4.2.2"
   },
   "scripts": {
     "start": "run-script-os",
diff --git a/renovate.json5 b/renovate.json5
index 400eb075..c034695b 100644
--- a/renovate.json5
+++ b/renovate.json5
@@ -21,13 +21,13 @@
       matchPackageNames: [
         '@faker-js/faker',
         '@playwright/test',
+        '@tanstack/react-query',
         '@trivago/prettier-plugin-sort-imports',
         'axios',
         'axios-mock-adapter',
         'babel-jest',
         'babel-plugin-transform-import-meta',
         'buffer',
-        'compare-versions',
         'cross-env',
         'dayjs',
         'electron-devtools-installer',
@@ -48,6 +48,7 @@
         'nunjucks',
         'object-hash',
         'prettier',
+        'prop-types',
         'proxy-memoize',
         'run-script-os',
         'spdx-license-ids',
@@ -82,11 +83,6 @@
       groupName: 'Emotion dependencies',
       groupSlug: 'emotion',
     },
-    {
-      matchPackagePrefixes: ['@tanstack/'],
-      groupName: 'Tanstack dependencies',
-      groupSlug: 'tanstack',
-    },
     {
       matchPackageNames: ['react', 'react-dom'],
       groupName: 'React dependencies',
diff --git a/src/Frontend/Components/AppContainer/AppContainer.tsx b/src/Frontend/Components/AppContainer/AppContainer.tsx
index 41c69021..9052adbe 100644
--- a/src/Frontend/Components/AppContainer/AppContainer.tsx
+++ b/src/Frontend/Components/AppContainer/AppContainer.tsx
@@ -15,9 +15,7 @@ import { App } from '../App/App';
 dayjs.extend(localizedFormat);
 
 const store = createAppStore();
-const queryClient = new QueryClient({
-  defaultOptions: { queries: { refetchOnWindowFocus: false, retry: false } },
-});
+const queryClient = new QueryClient();
 
 export function AppContainer(): ReactElement {
   return (
diff --git a/src/Frontend/Components/AttributionColumn/AttributionColumn.tsx b/src/Frontend/Components/AttributionColumn/AttributionColumn.tsx
index ed1e728a..a44e9350 100644
--- a/src/Frontend/Components/AttributionColumn/AttributionColumn.tsx
+++ b/src/Frontend/Components/AttributionColumn/AttributionColumn.tsx
@@ -4,7 +4,6 @@
 //
 // SPDX-License-Identifier: Apache-2.0
 import MuiBox from '@mui/material/Box';
-import MuiDialogContentText from '@mui/material/DialogContentText';
 import MuiDivider from '@mui/material/Divider';
 import MuiToggleButton from '@mui/material/ToggleButton';
 import MuiToggleButtonGroup from '@mui/material/ToggleButtonGroup';
@@ -33,18 +32,13 @@ import {
   ResetStateListener,
   useIpcRenderer,
 } from '../../util/use-ipc-renderer';
-import {
-  ConfirmationDialog,
-  useConfirmationDialog,
-} from '../ConfirmationDialog/ConfirmationDialog';
-import { WasPreferredIcon } from '../Icons/Icons';
+import { TextFieldStack } from '../TextFieldStack/TextFieldStack';
 import {
   getResolvedToggleHandler,
   selectedPackagesAreResolved,
 } from './attribution-column-helpers';
 import { AuditingOptions } from './AuditingOptions';
 import { ButtonRow } from './ButtonRow';
-import { CommentStack } from './CommentStack';
 import { CopyrightSubPanel } from './CopyrightSubPanel';
 import { LicenseSubPanel } from './LicenseSubPanel';
 import { PackageSubPanel } from './PackageSubPanel';
@@ -105,10 +99,6 @@ export function AttributionColumn(props: AttributionColumnProps): ReactElement {
   const temporaryDisplayPackageInfo = useAppSelector(
     getTemporaryDisplayPackageInfo,
   );
-  const [confirmEditWasPreferredRef, confirmEditWasPreferred] =
-    useConfirmationDialog({
-      skip: !temporaryDisplayPackageInfo.wasPreferred,
-    });
   const packageInfoWereModified = useAppSelector(
     wereTemporaryDisplayPackageInfoModified,
   );
@@ -145,71 +135,64 @@ export function AttributionColumn(props: AttributionColumnProps): ReactElement {
   ]);
 
   return (
-    <>
-      <MuiBox aria-label={'attribution column'} sx={classes.root}>
-        <MuiBox sx={classes.panelsContainer}>
-          <AuditingOptions
-            packageInfo={temporaryDisplayPackageInfo}
-            isEditable={props.isEditable}
-          />
-          <MuiDivider variant={'middle'}>
-            <MuiTypography>
-              {text.attributionColumn.packageCoordinates}
-            </MuiTypography>
-          </MuiDivider>
-          <PackageSubPanel
-            displayPackageInfo={temporaryDisplayPackageInfo}
-            isEditable={props.isEditable}
-            showHighlight={showHighlight}
-            confirmEditWasPreferred={confirmEditWasPreferred}
-          />
-          <MuiDivider variant={'middle'}>
-            <MuiTypography>
-              {text.attributionColumn.legalInformation}
-            </MuiTypography>
-          </MuiDivider>
-          {renderAttributionType()}
-          {temporaryDisplayPackageInfo.firstParty ? null : (
-            <>
-              <CopyrightSubPanel
-                isEditable={props.isEditable}
-                displayPackageInfo={temporaryDisplayPackageInfo}
-                showHighlight={showHighlight}
-                confirmEditWasPreferred={confirmEditWasPreferred}
-              />
-              <LicenseSubPanel
-                displayPackageInfo={temporaryDisplayPackageInfo}
-                isEditable={props.isEditable}
-                showHighlight={showHighlight}
-                confirmEditWasPreferred={confirmEditWasPreferred}
-              />
-            </>
-          )}
-          <CommentStack
-            isEditable={props.isEditable}
-            displayPackageInfo={temporaryDisplayPackageInfo}
-            confirmEditWasPreferred={confirmEditWasPreferred}
-          />
-        </MuiBox>
-        <MuiBox sx={classes.buttonsContainer}>
-          {props.showHideButton ? (
-            renderShowHideButton()
-          ) : (
-            <ButtonRow
-              areButtonsHidden={props.areButtonsHidden}
+    <MuiBox aria-label={'attribution column'} sx={classes.root}>
+      <MuiBox sx={classes.panelsContainer}>
+        <AuditingOptions
+          packageInfo={temporaryDisplayPackageInfo}
+          isEditable={props.isEditable}
+        />
+        <MuiDivider variant={'middle'}>
+          <MuiTypography>
+            {text.attributionColumn.packageCoordinates}
+          </MuiTypography>
+        </MuiDivider>
+        <PackageSubPanel
+          displayPackageInfo={temporaryDisplayPackageInfo}
+          isEditable={props.isEditable}
+          showHighlight={showHighlight}
+        />
+        <MuiDivider variant={'middle'}>
+          <MuiTypography>
+            {text.attributionColumn.legalInformation}
+          </MuiTypography>
+        </MuiDivider>
+        {renderAttributionType()}
+        {temporaryDisplayPackageInfo.firstParty ? null : (
+          <>
+            <CopyrightSubPanel
+              isEditable={props.isEditable}
+              displayPackageInfo={temporaryDisplayPackageInfo}
+              showHighlight={showHighlight}
+            />
+            <LicenseSubPanel
               displayPackageInfo={temporaryDisplayPackageInfo}
-              hideDeleteButtons={props.hideDeleteButtons}
-              onDeleteButtonClick={props.onDeleteButtonClick}
-              onDeleteGloballyButtonClick={props.onDeleteGloballyButtonClick}
-              onSaveButtonClick={props.onSaveButtonClick}
-              onSaveGloballyButtonClick={props.onSaveGloballyButtonClick}
-              showSaveGloballyButton={props.showSaveGloballyButton}
+              isEditable={props.isEditable}
+              showHighlight={showHighlight}
             />
-          )}
-        </MuiBox>
+          </>
+        )}
+        <TextFieldStack
+          isEditable={props.isEditable}
+          comments={temporaryDisplayPackageInfo.comments || []}
+        />
       </MuiBox>
-      {renderConfirmationDialog()}
-    </>
+      <MuiBox sx={classes.buttonsContainer}>
+        {props.showHideButton ? (
+          renderShowHideButton()
+        ) : (
+          <ButtonRow
+            areButtonsHidden={props.areButtonsHidden}
+            displayPackageInfo={temporaryDisplayPackageInfo}
+            hideDeleteButtons={props.hideDeleteButtons}
+            onDeleteButtonClick={props.onDeleteButtonClick}
+            onDeleteGloballyButtonClick={props.onDeleteGloballyButtonClick}
+            onSaveButtonClick={props.onSaveButtonClick}
+            onSaveGloballyButtonClick={props.onSaveGloballyButtonClick}
+            showSaveGloballyButton={props.showSaveGloballyButton}
+          />
+        )}
+      </MuiBox>
+    </MuiBox>
   );
 
   function renderAttributionType() {
@@ -217,17 +200,14 @@ export function AttributionColumn(props: AttributionColumnProps): ReactElement {
       <MuiToggleButtonGroup
         value={temporaryDisplayPackageInfo.firstParty || false}
         exclusive
-        onChange={(_, newValue) =>
-          confirmEditWasPreferred(() =>
-            dispatch(
-              setTemporaryDisplayPackageInfo({
-                ...temporaryDisplayPackageInfo,
-                firstParty: newValue,
-                wasPreferred: undefined,
-              }),
-            ),
-          )
-        }
+        onChange={(_, newValue) => {
+          dispatch(
+            setTemporaryDisplayPackageInfo({
+              ...temporaryDisplayPackageInfo,
+              firstParty: newValue,
+            }),
+          );
+        }}
         size={'small'}
         fullWidth
       >
@@ -258,22 +238,4 @@ export function AttributionColumn(props: AttributionColumnProps): ReactElement {
       </MuiToggleButton>
     );
   }
-
-  function renderConfirmationDialog() {
-    return (
-      <ConfirmationDialog
-        ref={confirmEditWasPreferredRef}
-        message={
-          <MuiDialogContentText
-            style={{ display: 'flex', alignItems: 'center' }}
-          >
-            {text.modifyWasPreferredPopup.message}
-            <WasPreferredIcon />
-            {'.'}
-          </MuiDialogContentText>
-        }
-        title={text.modifyWasPreferredPopup.header}
-      />
-    );
-  }
 }
diff --git a/src/Frontend/Components/AttributionColumn/CommentStack.tsx b/src/Frontend/Components/AttributionColumn/CommentStack.tsx
deleted file mode 100644
index ddbc81e2..00000000
--- a/src/Frontend/Components/AttributionColumn/CommentStack.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-import MuiBox from '@mui/material/Box';
-import { ReactElement } from 'react';
-
-import { DisplayPackageInfo } from '../../../shared/shared-types';
-import { setTemporaryDisplayPackageInfo } from '../../state/actions/resource-actions/all-views-simple-actions';
-import { useAppDispatch } from '../../state/hooks';
-import { Confirm } from '../ConfirmationDialog/ConfirmationDialog';
-import { TextBox } from '../InputElements/TextBox';
-import { attributionColumnClasses } from './shared-attribution-column-styles';
-
-interface Props {
-  isEditable: boolean;
-  displayPackageInfo: DisplayPackageInfo;
-  confirmEditWasPreferred: Confirm;
-}
-
-export function CommentStack({
-  confirmEditWasPreferred,
-  displayPackageInfo,
-  isEditable,
-}: Props): ReactElement {
-  const dispatch = useAppDispatch();
-  const filteredComments = (displayPackageInfo.comments || [])?.filter(
-    (comment) => comment.replace(/\s/g, '') !== '',
-  );
-
-  if (filteredComments.length === 0) {
-    filteredComments.push('');
-  }
-
-  return (
-    <MuiBox sx={attributionColumnClasses.panel}>
-      {filteredComments.map((comment, index) => (
-        <TextBox
-          key={index}
-          isEditable={isEditable}
-          title={`Comment ${
-            filteredComments.length === 1 ? '' : index + 1
-          }`.trim()}
-          text={comment}
-          minRows={3}
-          maxRows={10}
-          multiline={true}
-          handleChange={({ target: { value } }) =>
-            confirmEditWasPreferred(() =>
-              dispatch(
-                setTemporaryDisplayPackageInfo({
-                  ...displayPackageInfo,
-                  comments: [value],
-                  wasPreferred: undefined,
-                }),
-              ),
-            )
-          }
-        />
-      ))}
-    </MuiBox>
-  );
-}
diff --git a/src/Frontend/Components/AttributionColumn/CopyrightSubPanel.tsx b/src/Frontend/Components/AttributionColumn/CopyrightSubPanel.tsx
index fead7cb5..e2373a1a 100644
--- a/src/Frontend/Components/AttributionColumn/CopyrightSubPanel.tsx
+++ b/src/Frontend/Components/AttributionColumn/CopyrightSubPanel.tsx
@@ -6,10 +6,8 @@ import MuiBox from '@mui/material/Box';
 import { ReactElement } from 'react';
 
 import { DisplayPackageInfo } from '../../../shared/shared-types';
-import { setTemporaryDisplayPackageInfo } from '../../state/actions/resource-actions/all-views-simple-actions';
-import { useAppDispatch } from '../../state/hooks';
 import { isImportantAttributionInformationMissing } from '../../util/is-important-attribution-information-missing';
-import { Confirm } from '../ConfirmationDialog/ConfirmationDialog';
+import { usePackageInfoChangeHandler } from '../../util/use-package-info-change-handler';
 import { TextBox } from '../InputElements/TextBox';
 import { attributionColumnClasses } from './shared-attribution-column-styles';
 
@@ -17,43 +15,27 @@ interface CopyrightSubPanelProps {
   isEditable: boolean;
   displayPackageInfo: DisplayPackageInfo;
   showHighlight?: boolean;
-  confirmEditWasPreferred: Confirm;
 }
 
-export function CopyrightSubPanel({
-  confirmEditWasPreferred,
-  displayPackageInfo,
-  isEditable,
-  showHighlight,
-}: CopyrightSubPanelProps): ReactElement {
-  const dispatch = useAppDispatch();
+export function CopyrightSubPanel(props: CopyrightSubPanelProps): ReactElement {
+  const handleChange = usePackageInfoChangeHandler();
 
   return (
     <MuiBox sx={attributionColumnClasses.panel}>
       <TextBox
-        isEditable={isEditable}
+        isEditable={props.isEditable}
         sx={attributionColumnClasses.textBox}
         title={'Copyright'}
-        text={displayPackageInfo.copyright}
+        text={props.displayPackageInfo.copyright}
         minRows={3}
         maxRows={10}
         multiline={true}
-        handleChange={({ target: { value } }) =>
-          confirmEditWasPreferred(() =>
-            dispatch(
-              setTemporaryDisplayPackageInfo({
-                ...displayPackageInfo,
-                copyright: value,
-                wasPreferred: undefined,
-              }),
-            ),
-          )
-        }
+        handleChange={handleChange('copyright')}
         isHighlighted={
-          showHighlight &&
+          props.showHighlight &&
           isImportantAttributionInformationMissing(
             'copyright',
-            displayPackageInfo,
+            props.displayPackageInfo,
           )
         }
       />
diff --git a/src/Frontend/Components/AttributionColumn/LicenseSubPanel.tsx b/src/Frontend/Components/AttributionColumn/LicenseSubPanel.tsx
index ea864e05..29d5bf4d 100644
--- a/src/Frontend/Components/AttributionColumn/LicenseSubPanel.tsx
+++ b/src/Frontend/Components/AttributionColumn/LicenseSubPanel.tsx
@@ -11,16 +11,12 @@ import MuiInputAdornment from '@mui/material/InputAdornment';
 import { sortBy } from 'lodash';
 import { ReactElement, useMemo, useState } from 'react';
 
-import {
-  AutocompleteSignal,
-  DisplayPackageInfo,
-} from '../../../shared/shared-types';
+import { DisplayPackageInfo, PackageInfo } from '../../../shared/shared-types';
 import { text } from '../../../shared/text';
 import { OpossumColors } from '../../shared-styles';
-import { setTemporaryDisplayPackageInfo } from '../../state/actions/resource-actions/all-views-simple-actions';
-import { useAppDispatch, useAppSelector } from '../../state/hooks';
+import { useAppSelector } from '../../state/hooks';
 import { getFrequentLicensesNameOrder } from '../../state/selectors/all-views-resource-selectors';
-import { Confirm } from '../ConfirmationDialog/ConfirmationDialog';
+import { usePackageInfoChangeHandler } from '../../util/use-package-info-change-handler';
 import { TextBox } from '../InputElements/TextBox';
 import { getLicenseTextLabelText } from './attribution-column-helpers';
 import { PackageAutocomplete } from './PackageAutocomplete';
@@ -69,24 +65,15 @@ interface LicenseSubPanelProps {
   isEditable: boolean;
   displayPackageInfo: DisplayPackageInfo;
   showHighlight?: boolean;
-  confirmEditWasPreferred: Confirm;
 }
 
-export function LicenseSubPanel({
-  confirmEditWasPreferred,
-  displayPackageInfo,
-  isEditable,
-  showHighlight,
-}: LicenseSubPanelProps): ReactElement {
-  const dispatch = useAppDispatch();
+export function LicenseSubPanel(props: LicenseSubPanelProps): ReactElement {
   const [expanded, setExpanded] = useState(false);
   const frequentLicensesNames = useAppSelector(getFrequentLicensesNameOrder);
   const defaultLicenses = useMemo(
     () =>
       sortBy(
-        frequentLicensesNames.map<AutocompleteSignal>(({ fullName }) => ({
-          attributionIds: [],
-          default: true,
+        frequentLicensesNames.map<PackageInfo>(({ fullName }) => ({
           licenseName: fullName,
           source: {
             documentConfidence: 100,
@@ -98,6 +85,8 @@ export function LicenseSubPanel({
     [frequentLicensesNames],
   );
 
+  const handleChange = usePackageInfoChangeHandler();
+
   return (
     <MuiBox sx={classes.panel}>
       <MuiAccordion
@@ -121,11 +110,10 @@ export function LicenseSubPanel({
           <PackageAutocomplete
             attribute={'licenseName'}
             title={text.attributionColumn.licenseName}
-            disabled={!isEditable}
-            showHighlight={showHighlight}
-            confirmEditWasPreferred={confirmEditWasPreferred}
+            disabled={!props.isEditable}
+            showHighlight={props.showHighlight}
             endAdornment={
-              displayPackageInfo.licenseText ? (
+              props.displayPackageInfo.licenseText ? (
                 <MuiInputAdornment position="end" sx={classes.endAdornment}>
                   {text.attributionColumn.licenseTextModified}
                 </MuiInputAdornment>
@@ -136,28 +124,18 @@ export function LicenseSubPanel({
         </MuiAccordionSummary>
         <MuiAccordionDetails sx={classes.expansionPanelDetails}>
           <TextBox
-            isEditable={isEditable}
+            isEditable={props.isEditable}
             sx={classes.licenseText}
             minRows={3}
             maxRows={10}
             multiline={true}
             title={getLicenseTextLabelText(
-              displayPackageInfo.licenseName,
-              isEditable,
+              props.displayPackageInfo.licenseName,
+              props.isEditable,
               frequentLicensesNames,
             )}
-            text={displayPackageInfo.licenseText}
-            handleChange={({ target: { value } }) =>
-              confirmEditWasPreferred(() =>
-                dispatch(
-                  setTemporaryDisplayPackageInfo({
-                    ...displayPackageInfo,
-                    licenseText: value,
-                    wasPreferred: undefined,
-                  }),
-                ),
-              )
-            }
+            text={props.displayPackageInfo.licenseText}
+            handleChange={handleChange('licenseText')}
           />
         </MuiAccordionDetails>
       </MuiAccordion>
diff --git a/src/Frontend/Components/AttributionColumn/PackageAutocomplete.tsx b/src/Frontend/Components/AttributionColumn/PackageAutocomplete.tsx
index d5e85885..1556bd2f 100644
--- a/src/Frontend/Components/AttributionColumn/PackageAutocomplete.tsx
+++ b/src/Frontend/Components/AttributionColumn/PackageAutocomplete.tsx
@@ -4,40 +4,33 @@
 // SPDX-License-Identifier: Apache-2.0
 import AddIcon from '@mui/icons-material/Add';
 import ExploreIcon from '@mui/icons-material/Explore';
-import OpenInNewIcon from '@mui/icons-material/OpenInNew';
 import StarIcon from '@mui/icons-material/Star';
-import { createFilterOptions, styled } from '@mui/material';
+import { styled } from '@mui/material';
 import MuiBadge from '@mui/material/Badge';
 import MuiChip from '@mui/material/Chip';
 import MuiIconButton from '@mui/material/IconButton';
 import MuiTooltip from '@mui/material/Tooltip';
 import { compact } from 'lodash';
-import { useEffect, useMemo, useState } from 'react';
+import { useMemo } from 'react';
 
-import {
-  AutocompleteSignal,
-  DisplayPackageInfo,
-  PackageInfo,
-} from '../../../shared/shared-types';
+import { PackageInfo, SignalWithCount } from '../../../shared/shared-types';
 import { text } from '../../../shared/text';
-import { baseIcon, clickableIcon, OpossumColors } from '../../shared-styles';
+import { baseIcon, OpossumColors } from '../../shared-styles';
 import { setTemporaryDisplayPackageInfo } from '../../state/actions/resource-actions/all-views-simple-actions';
+import { savePackageInfo } from '../../state/actions/resource-actions/save-actions';
 import { useAppDispatch, useAppSelector } from '../../state/hooks';
 import {
+  getAttributionIdOfDisplayedPackageInManualPanel,
   getExternalAttributionSources,
   getTemporaryDisplayPackageInfo,
 } from '../../state/selectors/all-views-resource-selectors';
+import { getSelectedResourceId } from '../../state/selectors/audit-view-resource-selectors';
 import { isAuditViewSelected } from '../../state/selectors/view-selector';
 import { generatePurl } from '../../util/handle-purl';
 import { isImportantAttributionInformationMissing } from '../../util/is-important-attribution-information-missing';
-import { omit, pick } from '../../util/lodash-extension-utils';
 import { maybePluralize } from '../../util/maybe-pluralize';
-import { openUrl } from '../../util/open-url';
-import { PackageSearchHooks } from '../../util/package-search-hooks';
 import { useAutocompleteSignals } from '../../web-workers/use-signals-worker';
 import { Autocomplete } from '../Autocomplete/Autocomplete';
-import { Confirm } from '../ConfirmationDialog/ConfirmationDialog';
-import { IconButton } from '../IconButton/IconButton';
 
 type AutocompleteAttribute = Extract<
   keyof PackageInfo,
@@ -54,10 +47,9 @@ interface Props {
   attribute: AutocompleteAttribute;
   highlight?: 'default' | 'dark';
   endAdornment?: React.ReactElement;
-  defaults?: Array<AutocompleteSignal>;
+  defaults?: Array<SignalWithCount>;
   disabled: boolean;
   showHighlight: boolean | undefined;
-  confirmEditWasPreferred: Confirm;
 }
 
 const Badge = styled(MuiBadge)({
@@ -68,31 +60,24 @@ const Badge = styled(MuiBadge)({
   },
 });
 
-const AddIconButton = styled(MuiIconButton)({
-  backgroundColor: 'rgba(0, 0, 0, 0.04)',
-  marginLeft: '8px',
-  '&:hover': { backgroundColor: 'rgba(0, 0, 0, 0.1)' },
-});
-
 export function PackageAutocomplete({
   attribute,
   title,
-  highlight = 'default',
+  highlight,
   endAdornment,
   defaults = [],
   disabled,
   showHighlight,
-  confirmEditWasPreferred,
 }: Props) {
   const dispatch = useAppDispatch();
   const temporaryPackageInfo = useAppSelector(getTemporaryDisplayPackageInfo);
-  const attributeValue = temporaryPackageInfo[attribute] || '';
-  const [inputValue, setInputValue] = useState(attributeValue);
+  const resourceId = useAppSelector(getSelectedResourceId);
+  const attributionIdOfSelectedPackageInManualPanel = useAppSelector(
+    getAttributionIdOfDisplayedPackageInManualPanel,
+  );
   const sources = useAppSelector(getExternalAttributionSources);
   const isAuditView = useAppSelector(isAuditViewSelected);
 
-  const { enrichPackageInfo } = PackageSearchHooks.useEnrichPackageInfo();
-
   const autocompleteSignals = useAutocompleteSignals();
   const filteredSignals = useMemo(
     () =>
@@ -100,16 +85,10 @@ export function PackageAutocomplete({
         ? autocompleteSignals
             .filter((signal) => !['', undefined].includes(signal[attribute]))
             .concat(defaults)
-        : defaults,
+        : [],
     [isAuditView, autocompleteSignals, defaults, attribute],
   );
 
-  useEffect(() => {
-    if (attributeValue !== inputValue) {
-      setInputValue(attributeValue);
-    }
-  }, [attributeValue, inputValue]);
-
   return (
     <Autocomplete
       title={title}
@@ -117,7 +96,6 @@ export function PackageAutocomplete({
       autoHighlight
       disableClearable
       freeSolo
-      inputValue={inputValue}
       highlight={
         showHighlight &&
         isImportantAttributionInformationMissing(
@@ -143,13 +121,7 @@ export function PackageAutocomplete({
       }
       renderOptionStartIcon={renderOptionStartIcon}
       renderOptionEndIcon={renderOptionEndIcon}
-      value={temporaryPackageInfo}
-      filterOptions={createFilterOptions({
-        stringify: (option) =>
-          attribute === 'packageName'
-            ? `${option.packageName || ''}${option.packageNamespace || ''}`
-            : option[attribute] || '',
-      })}
+      value={temporaryPackageInfo as SignalWithCount}
       isOptionEqualToValue={(option, value) =>
         option[attribute] === value[attribute]
       }
@@ -158,59 +130,34 @@ export function PackageAutocomplete({
           ? sources[option.source.name]?.name || option.source.name
           : text.attributionColumn.manualAttributions
       }
-      groupProps={{
-        icon: () => (
-          <ExploreIcon
-            sx={{
-              ...baseIcon,
-              color: `${OpossumColors.black} !important`,
-            }}
-          />
-        ),
-        action: ({ name }) => (
-          <IconButton
-            hidden={name !== text.attributionColumn.openSourceInsights}
-            onClick={() => openUrl('https://www.deps.dev')}
-            icon={<OpenInNewIcon sx={clickableIcon} />}
-          />
-        ),
-      }}
+      groupIcon={
+        <ExploreIcon
+          sx={{
+            ...baseIcon,
+            color: `${OpossumColors.black} !important`,
+          }}
+        />
+      }
       optionText={{
-        primary: (option) => {
-          if (typeof option === 'string') {
-            return option;
-          }
-
-          const optionValue = option[attribute];
-
-          if (!optionValue) {
-            return '';
-          }
-
-          return `${optionValue} ${option.suffix || ''}`.trim();
-        },
+        primary: (option) =>
+          typeof option === 'string' ? option : option[attribute],
         secondary: (option) =>
           typeof option === 'string' ? option : generatePurl(option),
       }}
-      onInputChange={(event, value) =>
-        event &&
+      onInputChange={(_, value) => {
         temporaryPackageInfo[attribute] !== value &&
-        confirmEditWasPreferred(() => {
           dispatch(
             setTemporaryDisplayPackageInfo({
               ...temporaryPackageInfo,
               [attribute]: value,
-              wasPreferred: undefined,
             }),
           );
-          setInputValue(value);
-        })
-      }
+      }}
       endAdornment={endAdornment}
     />
   );
 
-  function renderOptionStartIcon(option: AutocompleteSignal) {
+  function renderOptionStartIcon(option: SignalWithCount) {
     if (!option.count) {
       return null;
     }
@@ -227,12 +174,11 @@ export function PackageAutocomplete({
           showStar
             ? `${baseTitle} (${
                 option.preferred
-                  ? text.auditingOptions.currentlyPreferred.toLowerCase()
-                  : text.auditingOptions.previouslyPreferred.toLowerCase()
+                  ? text.auditingOptions.currentlyPreferred
+                  : text.auditingOptions.previouslyPreferred
               })`
             : baseTitle
         }
-        enterDelay={500}
       >
         <Badge
           badgeContent={
@@ -259,7 +205,7 @@ export function PackageAutocomplete({
   }
 
   function renderOptionEndIcon(
-    option: AutocompleteSignal,
+    { preSelected, ...option }: PackageInfo,
     { closePopper }: { closePopper: () => void },
   ) {
     if (!option.packageName || !option.packageType) {
@@ -269,31 +215,18 @@ export function PackageAutocomplete({
     return (
       <MuiTooltip
         title={text.attributionColumn.useAutocompleteSuggestion}
-        enterDelay={1000}
         disableInteractive
       >
-        <AddIconButton
-          onClick={async (event) => {
+        <MuiIconButton
+          onClick={(event) => {
             event.stopPropagation();
-            const merged: DisplayPackageInfo = {
-              ...omit(option, [
-                'count',
-                'default',
-                'preSelected',
-                'source',
-                'suffix',
-              ]),
-              ...pick(temporaryPackageInfo, [
-                'attributionConfidence',
-                'excludeFromNotice',
-                'followUp',
-                'needsReview',
-                'preferred',
-              ]),
-            };
             dispatch(
-              setTemporaryDisplayPackageInfo(
-                (option.default && (await enrichPackageInfo(merged))) || merged,
+              savePackageInfo(
+                resourceId,
+                attributionIdOfSelectedPackageInManualPanel,
+                option,
+                undefined,
+                true,
               ),
             );
             closePopper();
@@ -301,7 +234,7 @@ export function PackageAutocomplete({
           size={'small'}
         >
           <AddIcon fontSize={'inherit'} color={'primary'} />
-        </AddIconButton>
+        </MuiIconButton>
       </MuiTooltip>
     );
   }
diff --git a/src/Frontend/Components/AttributionColumn/PackageSubPanel.tsx b/src/Frontend/Components/AttributionColumn/PackageSubPanel.tsx
index 57a6d11f..48f8b891 100644
--- a/src/Frontend/Components/AttributionColumn/PackageSubPanel.tsx
+++ b/src/Frontend/Components/AttributionColumn/PackageSubPanel.tsx
@@ -10,54 +10,35 @@ import MuiBox from '@mui/material/Box';
 import { styled } from '@mui/system';
 import { useMemo } from 'react';
 
-import {
-  AutocompleteSignal,
-  DisplayPackageInfo,
-} from '../../../shared/shared-types';
+import { DisplayPackageInfo, PackageInfo } from '../../../shared/shared-types';
 import { text } from '../../../shared/text';
+import { PopupType } from '../../enums/enums';
 import { clickableIcon } from '../../shared-styles';
 import { setTemporaryDisplayPackageInfo } from '../../state/actions/resource-actions/all-views-simple-actions';
+import { openPopup } from '../../state/actions/view-actions/view-actions';
 import { useAppDispatch } from '../../state/hooks';
 import { generatePurl, parsePurl } from '../../util/handle-purl';
 import { openUrl } from '../../util/open-url';
-import { PackageSearchHooks } from '../../util/package-search-hooks';
-import { useDebouncedInput } from '../../util/use-debounced-input';
-import { Confirm } from '../ConfirmationDialog/ConfirmationDialog';
+import { FetchLicenseInformationButton } from '../FetchLicenseInformationButton/FetchLicenseInformationButton';
 import { IconButton } from '../IconButton/IconButton';
+import { SearchPackagesIcon } from '../Icons/Icons';
 import { TextBox } from '../InputElements/TextBox';
 import { PackageAutocomplete } from './PackageAutocomplete';
 import { attributionColumnClasses } from './shared-attribution-column-styles';
 
-/** https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst */
 const COMMON_PACKAGE_TYPES = [
   'bitbucket',
   'cargo',
-  'composer',
-  'conan',
-  'conda',
-  'cran',
   'deb',
   'docker',
   'gem',
-  'generic',
   'github',
-  'gitlab',
   'golang',
-  'hackage',
-  'hex',
-  'huggingface',
   'maven',
-  'mlflow',
   'npm',
   'nuget',
-  'oci',
-  'pub',
   'pypi',
-  'qpkg',
   'rpm',
-  'rpm',
-  'swid',
-  'swift',
 ];
 
 const DisplayRow = styled('div')({
@@ -69,16 +50,13 @@ interface PackageSubPanelProps {
   displayPackageInfo: DisplayPackageInfo;
   isEditable: boolean;
   showHighlight?: boolean;
-  confirmEditWasPreferred: Confirm;
 }
 
 export function PackageSubPanel(props: PackageSubPanelProps) {
   const dispatch = useAppDispatch();
   const defaultPackageTypes = useMemo(
     () =>
-      COMMON_PACKAGE_TYPES.map<AutocompleteSignal>((packageType) => ({
-        attributionIds: [],
-        default: true,
+      COMMON_PACKAGE_TYPES.map<PackageInfo>((packageType) => ({
         packageType,
         source: {
           name: text.attributionColumn.commonEcosystems,
@@ -88,15 +66,6 @@ export function PackageSubPanel(props: PackageSubPanelProps) {
     [],
   );
 
-  const debouncedPackageInfo = useDebouncedInput(props.displayPackageInfo);
-
-  const { packageNames } =
-    PackageSearchHooks.usePackageNames(debouncedPackageInfo);
-  const { packageNamespaces } =
-    PackageSearchHooks.usePackageNamespaces(debouncedPackageInfo);
-  const { packageVersions } =
-    PackageSearchHooks.usePackageVersions(debouncedPackageInfo);
-
   return (
     <MuiBox sx={attributionColumnClasses.panel}>
       <DisplayRow>
@@ -120,8 +89,17 @@ export function PackageSubPanel(props: PackageSubPanelProps) {
         highlight={'dark'}
         disabled={!props.isEditable}
         showHighlight={props.showHighlight}
-        defaults={packageNames}
-        confirmEditWasPreferred={props.confirmEditWasPreferred}
+        endAdornment={
+          <IconButton
+            tooltipTitle={
+              text.attributionColumn.packageSubPanel.searchForPackage
+            }
+            tooltipPlacement="right"
+            onClick={() => dispatch(openPopup(PopupType.PackageSearchPopup))}
+            hidden={!props.isEditable}
+            icon={<SearchPackagesIcon sx={clickableIcon} />}
+          />
+        }
       />
     );
   }
@@ -134,8 +112,6 @@ export function PackageSubPanel(props: PackageSubPanelProps) {
         highlight={'dark'}
         disabled={!props.isEditable}
         showHighlight={props.showHighlight}
-        defaults={packageNamespaces}
-        confirmEditWasPreferred={props.confirmEditWasPreferred}
       />
     );
   }
@@ -147,8 +123,6 @@ export function PackageSubPanel(props: PackageSubPanelProps) {
         title={text.attributionColumn.packageSubPanel.packageVersion}
         disabled={!props.isEditable}
         showHighlight={props.showHighlight}
-        defaults={packageVersions}
-        confirmEditWasPreferred={props.confirmEditWasPreferred}
       />
     );
   }
@@ -162,7 +136,6 @@ export function PackageSubPanel(props: PackageSubPanelProps) {
         disabled={!props.isEditable}
         showHighlight={props.showHighlight}
         defaults={defaultPackageTypes}
-        confirmEditWasPreferred={props.confirmEditWasPreferred}
       />
     );
   }
@@ -231,20 +204,28 @@ export function PackageSubPanel(props: PackageSubPanelProps) {
         title={text.attributionColumn.packageSubPanel.repositoryUrl}
         disabled={!props.isEditable}
         showHighlight={props.showHighlight}
-        confirmEditWasPreferred={props.confirmEditWasPreferred}
         endAdornment={
-          <IconButton
-            tooltipTitle={
-              text.attributionColumn.packageSubPanel.openLinkInBrowser
-            }
-            tooltipPlacement="right"
-            onClick={(): void => {
-              props.displayPackageInfo.url &&
-                openUrl(props.displayPackageInfo.url);
-            }}
-            hidden={!props.displayPackageInfo.url}
-            icon={<OpenInNewIcon aria-label={'Url icon'} sx={clickableIcon} />}
-          />
+          <>
+            <FetchLicenseInformationButton
+              url={props.displayPackageInfo.url}
+              version={props.displayPackageInfo.packageVersion}
+              disabled={!props.isEditable}
+            />
+            <IconButton
+              tooltipTitle={
+                text.attributionColumn.packageSubPanel.openLinkInBrowser
+              }
+              tooltipPlacement="right"
+              onClick={(): void => {
+                props.displayPackageInfo.url &&
+                  openUrl(props.displayPackageInfo.url);
+              }}
+              hidden={!props.displayPackageInfo.url}
+              icon={
+                <OpenInNewIcon aria-label={'Url icon'} sx={clickableIcon} />
+              }
+            />
+          </>
         }
       />
     );
diff --git a/src/Frontend/Components/AttributionDetailsViewer/AttributionDetailsViewer.tsx b/src/Frontend/Components/AttributionDetailsViewer/AttributionDetailsViewer.tsx
index b0ae2a0d..75772358 100644
--- a/src/Frontend/Components/AttributionDetailsViewer/AttributionDetailsViewer.tsx
+++ b/src/Frontend/Components/AttributionDetailsViewer/AttributionDetailsViewer.tsx
@@ -64,7 +64,9 @@ export function AttributionDetailsViewer(): ReactElement | null {
   const dispatch = useAppDispatch();
 
   const saveFileRequestListener = useCallback(() => {
-    if (
+    if (temporaryDisplayPackageInfo.wasPreferred) {
+      dispatch(openPopup(PopupType.ModifyWasPreferredAttributionPopup));
+    } else if (
       didPreferredFieldChange &&
       resourceIdsOfSelectedAttributionId.length > 1
     ) {
@@ -87,7 +89,9 @@ export function AttributionDetailsViewer(): ReactElement | null {
   ]);
 
   const dispatchSavePackageInfoOrOpenWasPreferredPopup = useCallback(() => {
-    if (
+    if (temporaryDisplayPackageInfo.wasPreferred) {
+      dispatch(openPopup(PopupType.ModifyWasPreferredAttributionPopup));
+    } else if (
       didPreferredFieldChange &&
       resourceIdsOfSelectedAttributionId.length > 1
     ) {
diff --git a/src/Frontend/Components/Autocomplete/Autocomplete.tsx b/src/Frontend/Components/Autocomplete/Autocomplete.tsx
index c1bbcac4..636965bc 100644
--- a/src/Frontend/Components/Autocomplete/Autocomplete.tsx
+++ b/src/Frontend/Components/Autocomplete/Autocomplete.tsx
@@ -9,7 +9,6 @@ import { IconButtonProps as MuiIconButtonProps } from '@mui/material/IconButton'
 import { InputBaseComponentProps as MuiInputBaseComponentProps } from '@mui/material/InputBase';
 import useMuiAutocomplete, {
   AutocompleteHighlightChangeReason,
-  AutocompleteInputChangeReason,
   UseAutocompleteProps as MuiUseAutocompleteProps,
 } from '@mui/material/useAutocomplete';
 import { compact } from 'lodash';
@@ -33,12 +32,12 @@ type AutocompleteProps<
   FreeSolo extends boolean | undefined,
 > = Omit<
   MuiUseAutocompleteProps<Value, Multiple, DisableClearable, FreeSolo>,
-  'onClose' | 'onOpen' | 'open' | 'onHighlightChange' | 'onInputChange'
+  'onClose' | 'onOpen' | 'open' | 'onHighlightChange'
 > &
   Pick<
     ListboxProps<Value, FreeSolo>,
     | 'getOptionKey'
-    | 'groupProps'
+    | 'groupIcon'
     | 'optionText'
     | 'renderOptionEndIcon'
     | 'renderOptionStartIcon'
@@ -46,11 +45,6 @@ type AutocompleteProps<
     endAdornment?: React.ReactNode;
     highlight?: 'default' | 'dark';
     inputProps?: MuiInputBaseComponentProps;
-    onInputChange?: (
-      event: React.SyntheticEvent | undefined,
-      value: string,
-      reason: AutocompleteInputChangeReason,
-    ) => void;
     sx?: SxProps;
     title: string;
   };
@@ -69,9 +63,9 @@ export function Autocomplete<
   getOptionKey,
   getOptionLabel,
   groupBy,
-  groupProps,
+  groupIcon,
   highlight,
-  inputProps: customInputProps,
+  inputProps,
   multiple,
   optionText,
   renderOptionEndIcon,
@@ -147,13 +141,10 @@ export function Autocomplete<
   const hasClearButton = !disableClearable && !disabled && containsValue;
   const isPopupOpen = !!groupedOptions.length && popupOpen;
 
-  const { ref, color, ...inputProps } = getInputProps();
-
   return (
     <>
       <Container {...getRootProps()} sx={sx} ref={setAnchorEl}>
         <Input
-          {...inputProps}
           disabled={disabled}
           label={title}
           highlight={highlight}
@@ -162,8 +153,7 @@ export function Autocomplete<
           }
           size={'small'}
           InputLabelProps={getInputLabelProps()}
-          inputRef={ref}
-          inputProps={customInputProps}
+          inputProps={{ ...getInputProps(), ...inputProps }}
           InputProps={{
             startAdornment: renderStartAdornment(),
             endAdornment: renderEndAdornment(),
@@ -194,7 +184,6 @@ export function Autocomplete<
           key={key}
           label={label}
           {...tagProps}
-          onMouseDown={(event) => event.stopPropagation()}
           data-testid={`tag-${label}`}
         />
       );
@@ -234,7 +223,7 @@ export function Autocomplete<
               optionText={optionText}
               options={groupedOptions as Array<Value>}
               groupBy={groupBy}
-              groupProps={groupProps}
+              groupIcon={groupIcon}
               getOptionKey={getOptionKey}
               getOptionProps={getOptionProps}
               renderOptionStartIcon={renderOptionStartIcon}
diff --git a/src/Frontend/Components/Autocomplete/Listbox/Listbox.tsx b/src/Frontend/Components/Autocomplete/Listbox/Listbox.tsx
index 116605c3..9e4641db 100644
--- a/src/Frontend/Components/Autocomplete/Listbox/Listbox.tsx
+++ b/src/Frontend/Components/Autocomplete/Listbox/Listbox.tsx
@@ -22,10 +22,7 @@ export type ListboxProps<
 > = React.HTMLAttributes<HTMLElement> & {
   virtuosoRef: React.RefObject<VirtuosoHandle>;
   options: Array<Value>;
-  groupProps?: {
-    icon?: React.FC<{ name: string }>;
-    action?: React.FC<{ name: string }>;
-  };
+  groupIcon?: React.ReactNode;
   closePopper: () => void;
   groupBy?: (option: Value) => string;
   getOptionKey?: (
@@ -67,7 +64,7 @@ export const Listbox = forwardRef(
       getOptionKey,
       getOptionProps,
       groupBy,
-      groupProps,
+      groupIcon,
       optionText,
       options,
       renderOptionEndIcon,
@@ -118,7 +115,7 @@ export const Listbox = forwardRef(
       return (
         <GroupedVirtuoso
           ref={virtuosoRef}
-          style={{ ...styles.virtuoso, height: height + groupNames.length }}
+          style={{ ...styles.virtuoso, height: height + groupNames.length - 1 }}
           overscan={4}
           increaseViewportBy={2}
           initialTopMostItemIndex={
@@ -129,22 +126,16 @@ export const Listbox = forwardRef(
           }
           totalListHeightChanged={setHeight}
           groupCounts={groupCounts}
-          groupContent={(index) => {
-            const IconComp = groupProps?.icon || (() => null);
-            const ActionComp = groupProps?.action || (() => null);
-
-            return (
-              <GroupContainer role={'group'}>
-                <IconComp name={groupNames[index]} />
-                <MuiTypography
-                  sx={{ ...styles.overflowEllipsis, paddingTop: '2px' }}
-                >
-                  {groupNames[index]}
-                </MuiTypography>
-                <ActionComp name={groupNames[index]} />
-              </GroupContainer>
-            );
-          }}
+          groupContent={(index) => (
+            <GroupContainer role={'group'}>
+              {groupIcon}
+              <MuiTypography
+                sx={{ ...styles.overflowEllipsis, paddingTop: '2px' }}
+              >
+                {groupNames[index]}
+              </MuiTypography>
+            </GroupContainer>
+          )}
           itemContent={(index) =>
             renderOption({ index, option: options[index] })
           }
diff --git a/src/Frontend/Components/ConfirmationDialog/ConfirmationDialog.tsx b/src/Frontend/Components/ConfirmationDialog/ConfirmationDialog.tsx
deleted file mode 100644
index e642f22d..00000000
--- a/src/Frontend/Components/ConfirmationDialog/ConfirmationDialog.tsx
+++ /dev/null
@@ -1,133 +0,0 @@
-// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-import {
-  forwardRef,
-  useCallback,
-  useImperativeHandle,
-  useRef,
-  useState,
-} from 'react';
-
-import { ButtonText } from '../../enums/enums';
-import { NotificationPopup } from '../NotificationPopup/NotificationPopup';
-
-export interface ConfirmOptions {
-  onCancel?: () => unknown | Promise<unknown>;
-  skip?: boolean;
-}
-
-export type Confirm = (
-  onConfirm: () => unknown | Promise<unknown>,
-  options?: ConfirmOptions,
-) => Promise<boolean>;
-
-/**
- * Hook to use the confirmation dialog.
- * @param options Options to be used for all confirm calls.
- * @returns A tuple containing a ref to the confirmation dialog and the confirm function.
- * @throws Error if the ref is not connected to a confirmation dialog.
- * @example
- * const [confirmRef, confirm] = useConfirmationDialog();
- * // ...
- * <button onClick={confirm(() => console.log('confirmed'))}>
- *    "Click to open confirmation dialog"
- * </button>
- * // ...
- * <ConfirmationDialog ref={confirmRef} title="Confirm" message="Are you sure?" />
- */
-export function useConfirmationDialog(
-  options?: ConfirmOptions,
-): [React.RefObject<Confirm>, Confirm] {
-  const confirmRef = useRef<Confirm>(null);
-
-  const confirm = useCallback<Confirm>(
-    (onConfirm, localOptions) => {
-      if (!confirmRef.current) {
-        throw new Error(
-          'Confirmation dialog ref is not connected to a confirmation dialog.',
-        );
-      }
-
-      return confirmRef.current(onConfirm, {
-        onCancel: localOptions?.onCancel || options?.onCancel,
-        skip: localOptions?.skip || options?.skip,
-      });
-    },
-    [options?.onCancel, options?.skip],
-  );
-
-  return [confirmRef, confirm];
-}
-
-export interface ConfirmationDialogProps {
-  title: string;
-  message: React.ReactElement | string;
-}
-
-/**
- * A confirmation dialog that can be used with the useConfirmationDialog hook.
- * @param props The props for the confirmation dialog.
- * @returns A confirmation dialog.
- * @example
- * const [confirmRef, confirm] = useConfirmationDialog();
- * // ...
- * <button onClick={confirm(() => console.log('confirmed'))}>
- *    "Click to open confirmation dialog"
- * </button>
- * // ...
- * <ConfirmationDialog ref={confirmRef} title="Confirm" message="Are you sure?" />
- */
-export const ConfirmationDialog = forwardRef<Confirm, ConfirmationDialogProps>(
-  ({ message, title }, ref) => {
-    const [open, setOpen] = useState(false);
-    const resolveRef = useRef<(value: boolean) => void>();
-
-    useImperativeHandle(
-      ref,
-      () =>
-        async (onConfirm, { onCancel, skip } = {}) => {
-          if (skip) {
-            void onConfirm?.();
-            return true;
-          }
-
-          setOpen(true);
-
-          const result = await new Promise<boolean>((resolve) => {
-            resolveRef.current = resolve;
-          });
-
-          if (result) {
-            void onConfirm?.();
-          } else {
-            void onCancel?.();
-          }
-
-          setOpen(false);
-
-          return result;
-        },
-      [],
-    );
-
-    return (
-      <NotificationPopup
-        content={message}
-        header={title}
-        isOpen={open}
-        rightButtonConfig={{
-          buttonText: ButtonText.Cancel,
-          color: 'secondary',
-          onClick: () => resolveRef.current?.(false),
-        }}
-        centerLeftButtonConfig={{
-          buttonText: ButtonText.Ok,
-          onClick: () => resolveRef.current?.(true),
-        }}
-        aria-label={'confirmation dialog'}
-      />
-    );
-  },
-);
diff --git a/src/Frontend/Components/EditAttributionPopup/EditAttributionPopup.tsx b/src/Frontend/Components/EditAttributionPopup/EditAttributionPopup.tsx
index 4d9aba6b..19e66bdb 100644
--- a/src/Frontend/Components/EditAttributionPopup/EditAttributionPopup.tsx
+++ b/src/Frontend/Components/EditAttributionPopup/EditAttributionPopup.tsx
@@ -4,13 +4,16 @@
 // SPDX-License-Identifier: Apache-2.0
 import { ReactElement, useCallback } from 'react';
 
-import { ButtonText } from '../../enums/enums';
+import { ButtonText, PopupType } from '../../enums/enums';
 import { closeEditAttributionPopupOrOpenUnsavedPopup } from '../../state/actions/popup-actions/popup-actions';
 import {
   savePackageInfo,
   savePackageInfoIfSavingIsNotDisabled,
 } from '../../state/actions/resource-actions/save-actions';
-import { closePopup } from '../../state/actions/view-actions/view-actions';
+import {
+  closePopup,
+  openPopup,
+} from '../../state/actions/view-actions/view-actions';
 import { useAppDispatch, useAppSelector } from '../../state/hooks';
 import { getTemporaryDisplayPackageInfo } from '../../state/selectors/all-views-resource-selectors';
 import { getPopupAttributionId } from '../../state/selectors/view-selector';
@@ -27,24 +30,42 @@ export function EditAttributionPopup(): ReactElement {
   );
 
   const saveFileRequestListener = useCallback(() => {
-    dispatch(
-      savePackageInfoIfSavingIsNotDisabled(
-        null,
-        popupAttributionId,
-        convertDisplayPackageInfoToPackageInfo(temporaryDisplayPackageInfo),
-      ),
-    );
+    if (temporaryDisplayPackageInfo.wasPreferred && popupAttributionId) {
+      dispatch(
+        openPopup(
+          PopupType.ModifyWasPreferredAttributionPopup,
+          popupAttributionId,
+        ),
+      );
+    } else {
+      dispatch(
+        savePackageInfoIfSavingIsNotDisabled(
+          null,
+          popupAttributionId,
+          convertDisplayPackageInfoToPackageInfo(temporaryDisplayPackageInfo),
+        ),
+      );
+    }
   }, [dispatch, popupAttributionId, temporaryDisplayPackageInfo]);
 
   const dispatchSavePackageInfoOrOpenWasPreferredPopup = useCallback(() => {
-    dispatch(
-      savePackageInfo(
-        null,
-        popupAttributionId,
-        convertDisplayPackageInfoToPackageInfo(temporaryDisplayPackageInfo),
-      ),
-    );
-    dispatch(closePopup());
+    if (temporaryDisplayPackageInfo.wasPreferred && popupAttributionId) {
+      dispatch(
+        openPopup(
+          PopupType.ModifyWasPreferredAttributionPopup,
+          popupAttributionId,
+        ),
+      );
+    } else {
+      dispatch(
+        savePackageInfo(
+          null,
+          popupAttributionId,
+          convertDisplayPackageInfoToPackageInfo(temporaryDisplayPackageInfo),
+        ),
+      );
+      dispatch(closePopup());
+    }
   }, [dispatch, popupAttributionId, temporaryDisplayPackageInfo]);
 
   function checkForModifiedPackageInfoBeforeClosing(): void {
diff --git a/src/Frontend/Components/FetchLicenseInformationButton/FetchLicenseInformationButton.tsx b/src/Frontend/Components/FetchLicenseInformationButton/FetchLicenseInformationButton.tsx
new file mode 100644
index 00000000..aa522dd1
--- /dev/null
+++ b/src/Frontend/Components/FetchLicenseInformationButton/FetchLicenseInformationButton.tsx
@@ -0,0 +1,215 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import CachedIcon from '@mui/icons-material/Cached';
+import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
+import axios from 'axios';
+import { ReactElement, useEffect, useState } from 'react';
+
+import { PackageInfo } from '../../../shared/shared-types';
+import { text } from '../../../shared/text';
+import {
+  baseIcon,
+  clickableIcon,
+  disabledIcon,
+  OpossumColors,
+} from '../../shared-styles';
+import { setTemporaryDisplayPackageInfo } from '../../state/actions/resource-actions/all-views-simple-actions';
+import { useAppDispatch, useAppSelector } from '../../state/hooks';
+import { getTemporaryDisplayPackageInfo } from '../../state/selectors/all-views-resource-selectors';
+import { getSelectedResourceId } from '../../state/selectors/audit-view-resource-selectors';
+import { doNothing } from '../../util/do-nothing';
+import { IconButton } from '../IconButton/IconButton';
+import {
+  getLicenseFetchingInformation,
+  LicenseFetchingInformation,
+} from './license-fetching-helpers';
+
+const classes = {
+  clickableIcon,
+  disabledIcon,
+  baseIcon,
+  errorIcon: {
+    color: OpossumColors.red,
+  },
+  successfulIcon: {
+    color: OpossumColors.green,
+  },
+  spinningIcon: {
+    color: OpossumColors.darkBlue,
+    animationName: '$spin',
+    animationDuration: '1000ms',
+    animationIterationCount: 'infinite',
+    animationTimingFunction: 'linear',
+  },
+  '@keyframes spin': {
+    from: {
+      transform: 'rotate(0deg)',
+    },
+    to: {
+      transform: 'rotate(-360deg)',
+    },
+  },
+};
+
+export enum FetchStatus {
+  Idle = 'Idle',
+  Success = 'Success',
+  Error = 'Error',
+  InFlight = 'InFlight',
+}
+
+export function useFetchPackageInfo(props: LicenseFetchingInformation): {
+  fetchStatus: FetchStatus;
+  errorMessage: string;
+  fetchData: () => void;
+} {
+  const dispatch = useAppDispatch();
+  const temporaryDisplayPackageInfo = useAppSelector(
+    getTemporaryDisplayPackageInfo,
+  );
+  const selectedResourceId = useAppSelector(getSelectedResourceId);
+  const [fetchStatus, setFetchStatus] = useState<FetchStatus>(FetchStatus.Idle);
+  const [errorMessage, setErrorMessage] = useState<string>('');
+  const [fetchedPackageInfo, setFetchedPackageInfo] = useState<{
+    selectedResourceId: string;
+    packageInfo: PackageInfo;
+  }>();
+
+  function fetchData(): void {
+    setFetchStatus(FetchStatus.InFlight);
+    axios
+      .get(props.url)
+      .then((res) => {
+        setFetchedPackageInfo({
+          selectedResourceId,
+          packageInfo: props.convertPayload(res.data),
+        });
+        setFetchStatus(FetchStatus.Success);
+      })
+      .catch((error: Error) => {
+        setErrorMessage(error.message);
+        setFetchStatus(FetchStatus.Error);
+      });
+  }
+
+  useEffect(() => {
+    if (
+      fetchStatus === FetchStatus.Success &&
+      fetchedPackageInfo?.packageInfo &&
+      selectedResourceId === fetchedPackageInfo?.selectedResourceId
+    ) {
+      dispatch(
+        setTemporaryDisplayPackageInfo({
+          ...temporaryDisplayPackageInfo,
+          ...fetchedPackageInfo.packageInfo,
+        }),
+      );
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [fetchStatus, fetchedPackageInfo, dispatch, selectedResourceId]);
+
+  useEffect(() => {
+    setErrorMessage('');
+    setFetchedPackageInfo(undefined);
+    setFetchStatus(FetchStatus.Idle);
+  }, [props.url]);
+
+  return {
+    fetchStatus,
+    errorMessage,
+    fetchData,
+  };
+}
+
+interface DisabledFetchLicenseInformationButtonProps {
+  tooltipText: string;
+}
+function DisabledFetchLicenseInformationButton(
+  props: DisabledFetchLicenseInformationButtonProps,
+): ReactElement {
+  return (
+    <IconButton
+      tooltipTitle={props.tooltipText}
+      tooltipPlacement="right"
+      disabled={true}
+      onClick={doNothing}
+      icon={<CachedIcon sx={classes.disabledIcon} />}
+    />
+  );
+}
+
+export function EnabledFetchLicenseInformationButton(
+  props: LicenseFetchingInformation,
+): ReactElement {
+  const { fetchStatus, errorMessage, fetchData } = useFetchPackageInfo(props);
+
+  function getIcon(): ReactElement {
+    switch (fetchStatus) {
+      case FetchStatus.InFlight:
+        return (
+          <CachedIcon sx={{ ...classes.baseIcon, ...classes.spinningIcon }} />
+        );
+      case FetchStatus.Error:
+        return (
+          <ErrorOutlineIcon
+            sx={{ ...classes.baseIcon, ...classes.errorIcon }}
+          />
+        );
+      case FetchStatus.Success:
+        return (
+          <CachedIcon sx={{ ...classes.baseIcon, ...classes.successfulIcon }} />
+        );
+      default:
+        return <CachedIcon sx={classes.clickableIcon} />;
+    }
+  }
+
+  function getTooltip(): string {
+    switch (fetchStatus) {
+      case FetchStatus.InFlight:
+        return text.attributionColumn.packageSubPanel.fetching;
+      case FetchStatus.Error:
+        return errorMessage;
+      default:
+        return text.attributionColumn.packageSubPanel.fetchPackageInfo;
+    }
+  }
+
+  return (
+    <IconButton
+      tooltipTitle={getTooltip()}
+      tooltipPlacement="right"
+      onClick={fetchData}
+      icon={getIcon()}
+    />
+  );
+}
+
+export function FetchLicenseInformationButton(props: {
+  url?: string;
+  version?: string;
+  disabled: boolean;
+}): React.ReactNode {
+  if (props.disabled) {
+    return null;
+  }
+
+  const licenseFetchingInformation = getLicenseFetchingInformation(
+    props.url,
+    props.version,
+  );
+  return licenseFetchingInformation ? (
+    <EnabledFetchLicenseInformationButton
+      url={licenseFetchingInformation.url}
+      convertPayload={licenseFetchingInformation.convertPayload}
+    />
+  ) : (
+    <DisabledFetchLicenseInformationButton
+      tooltipText={
+        text.attributionColumn.packageSubPanel.fetchPackageInfoNotPossible
+      }
+    />
+  );
+}
diff --git a/src/Frontend/Components/FetchLicenseInformationButton/github-fetching-helpers.ts b/src/Frontend/Components/FetchLicenseInformationButton/github-fetching-helpers.ts
new file mode 100644
index 00000000..f259310d
--- /dev/null
+++ b/src/Frontend/Components/FetchLicenseInformationButton/github-fetching-helpers.ts
@@ -0,0 +1,76 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { Buffer } from 'buffer';
+import { Schema, Validator } from 'jsonschema';
+
+import { PackageInfo } from '../../../shared/shared-types';
+
+const jsonSchemaValidator = new Validator();
+
+export function getGithubAPIUrl(url: string): string {
+  const { namespace, packageName } =
+    getPackageNamespaceAndPackageNameFromGithubURL(url);
+
+  return `https://api.github.com/repos/${namespace}/${packageName}/license`;
+}
+
+function getPackageNamespaceAndPackageNameFromGithubURL(url: string): {
+  namespace: string;
+  packageName: string;
+} {
+  const urlSuffix = url
+    .replace(new RegExp('^https://(www.)?github.com/'), '')
+    .split('/');
+  return {
+    namespace: urlSuffix[0],
+    packageName: urlSuffix[1],
+  };
+}
+
+const GITHUB_SCHEMA: Schema = {
+  type: 'object',
+  properties: {
+    html_url: { type: 'string' },
+    content: { type: 'string' },
+    license: {
+      type: 'object',
+      properties: { spdx_id: { type: 'string' } },
+      required: ['spdx_id'],
+    },
+  },
+  required: ['license', 'html_url'],
+};
+
+interface Payload {
+  html_url: string;
+  content?: string;
+  license: {
+    spdx_id: string;
+  };
+}
+
+export function convertGithubPayload(payload: unknown): PackageInfo {
+  jsonSchemaValidator.validate(payload, GITHUB_SCHEMA, {
+    throwError: true,
+  });
+
+  const validatedPayload = payload as Payload;
+
+  const { namespace, packageName } =
+    getPackageNamespaceAndPackageNameFromGithubURL(validatedPayload.html_url);
+
+  const licenseText = validatedPayload.content
+    ? Buffer.from(validatedPayload.content, 'base64').toString()
+    : undefined;
+
+  return {
+    licenseName: validatedPayload.license.spdx_id,
+    licenseText,
+    packageType: 'github',
+    packageNamespace: namespace,
+    packageName,
+    packagePURLAppendix: undefined,
+  };
+}
diff --git a/src/Frontend/Components/FetchLicenseInformationButton/license-fetching-helpers.ts b/src/Frontend/Components/FetchLicenseInformationButton/license-fetching-helpers.ts
new file mode 100644
index 00000000..0ab933c6
--- /dev/null
+++ b/src/Frontend/Components/FetchLicenseInformationButton/license-fetching-helpers.ts
@@ -0,0 +1,57 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { PackageInfo } from '../../../shared/shared-types';
+import {
+  convertGithubPayload,
+  getGithubAPIUrl,
+} from './github-fetching-helpers';
+import { convertNpmPayload, getNpmAPIUrl } from './npm-fetching-helpers';
+import { convertPypiPayload, getPypiAPIUrl } from './pypi-fetching-helpers';
+
+const PYPI_REGEX = new RegExp(
+  '^https://(www.)?pypi.org/(pypi|project)/[\\w-+,_]+/?$',
+);
+const NPM_REGEX = new RegExp(
+  '^https://(www.)?npmjs.com/(package/)?[\\w-+,_@/]+/?$',
+);
+const GITHUB_REGEX = new RegExp('^https://(www.)?github.com/[^/]+/[^/]+');
+
+export interface LicenseFetchingInformation {
+  url: string;
+  convertPayload: (payload: Response) => PackageInfo;
+}
+
+export function getLicenseFetchingInformation(
+  url?: string,
+  version?: string,
+): LicenseFetchingInformation | null {
+  if (!url) {
+    return null;
+  }
+
+  try {
+    new URL(url);
+  } catch {
+    return null;
+  }
+
+  if (PYPI_REGEX.test(url)) {
+    return {
+      url: getPypiAPIUrl(url),
+      convertPayload: convertPypiPayload,
+    };
+  } else if (NPM_REGEX.test(url)) {
+    return {
+      url: getNpmAPIUrl(url, version),
+      convertPayload: convertNpmPayload,
+    };
+  } else if (GITHUB_REGEX.test(url)) {
+    return {
+      url: getGithubAPIUrl(url),
+      convertPayload: convertGithubPayload,
+    };
+  }
+  return null;
+}
diff --git a/src/Frontend/Components/FetchLicenseInformationButton/npm-fetching-helpers.ts b/src/Frontend/Components/FetchLicenseInformationButton/npm-fetching-helpers.ts
new file mode 100644
index 00000000..b4b19ba9
--- /dev/null
+++ b/src/Frontend/Components/FetchLicenseInformationButton/npm-fetching-helpers.ts
@@ -0,0 +1,48 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { Schema, Validator } from 'jsonschema';
+
+import { PackageInfo } from '../../../shared/shared-types';
+
+export function getNpmAPIUrl(url: string, version?: string): string {
+  const packageName = url
+    .replace(new RegExp('^https://(www.)?npmjs.com/(package/)?'), '')
+    .replace(new RegExp('/$'), '');
+  return `https://registry.npmjs.org/${packageName}${
+    version ? `/${version}` : ''
+  }`;
+}
+
+const jsonSchemaValidator = new Validator();
+
+const NPM_SCHEMA: Schema = {
+  type: 'object',
+  properties: {
+    name: { type: 'string' },
+    license: { type: 'string' },
+  },
+  required: ['name', 'license'],
+};
+
+interface Payload {
+  name: string;
+  license: string;
+}
+
+export function convertNpmPayload(payload: unknown): PackageInfo {
+  jsonSchemaValidator.validate(payload, NPM_SCHEMA, {
+    throwError: true,
+  });
+
+  const validatedPayload = payload as Payload;
+
+  return {
+    licenseName: validatedPayload.license,
+    packageName: validatedPayload.name,
+    packageType: 'npm',
+    packageNamespace: undefined,
+    packagePURLAppendix: undefined,
+  };
+}
diff --git a/src/Frontend/Components/FetchLicenseInformationButton/pypi-fetching-helpers.ts b/src/Frontend/Components/FetchLicenseInformationButton/pypi-fetching-helpers.ts
new file mode 100644
index 00000000..7c7f7e69
--- /dev/null
+++ b/src/Frontend/Components/FetchLicenseInformationButton/pypi-fetching-helpers.ts
@@ -0,0 +1,60 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { Schema, Validator } from 'jsonschema';
+
+import { PackageInfo } from '../../../shared/shared-types';
+
+export function getPypiAPIUrl(url: string): string {
+  const packageName = url
+    .split('/')
+    .filter((x) => x)
+    .slice(-1)[0];
+
+  return `https://pypi.org/pypi/${packageName}/json`;
+}
+
+const jsonSchemaValidator = new Validator();
+
+const PYPI_SCHEMA: Schema = {
+  type: 'object',
+  properties: {
+    info: {
+      type: 'object',
+      properties: {
+        license: {
+          type: 'string',
+        },
+        name: {
+          type: 'string',
+        },
+      },
+      required: ['license', 'name'],
+    },
+  },
+  required: ['info'],
+};
+
+interface Payload {
+  info: {
+    license: string;
+    name: string;
+  };
+}
+
+export function convertPypiPayload(payload: unknown): PackageInfo {
+  jsonSchemaValidator.validate(payload, PYPI_SCHEMA, {
+    throwError: true,
+  });
+
+  const validatedPayload = payload as Payload;
+
+  return {
+    licenseName: validatedPayload.info.license,
+    packageName: validatedPayload.info.name,
+    packageType: 'pypi',
+    packageNamespace: undefined,
+    packagePURLAppendix: undefined,
+  };
+}
diff --git a/src/Frontend/Components/GlobalPopup/GlobalPopup.tsx b/src/Frontend/Components/GlobalPopup/GlobalPopup.tsx
index a4f17477..352fd0f2 100644
--- a/src/Frontend/Components/GlobalPopup/GlobalPopup.tsx
+++ b/src/Frontend/Components/GlobalPopup/GlobalPopup.tsx
@@ -19,7 +19,9 @@ import { FileSearchPopup } from '../FileSearchPopup/FileSearchPopup';
 import { FileSupportDotOpossumAlreadyExistsPopup } from '../FileSupportDotOpossumAlreadyExistsPopup/FileSupportDotOpossumAlreadyExistsPopup';
 import { FileSupportPopup } from '../FileSupportPopup/FileSupportPopup';
 import { LocatorPopup } from '../LocatorPopup/LocatorPopup';
+import { ModifyWasPreferredAttributionPopup } from '../ModifyWasPreferredAttributionPopup/ModifyWasPreferredAttributionPopup';
 import { NotSavedPopup } from '../NotSavedPopup/NotSavedPopup';
+import { PackageSearchPopup } from '../PackageSearchPopup/PackageSearchPopup';
 import { ProjectMetadataPopup } from '../ProjectMetadataPopup/ProjectMetadataPopup';
 import { ProjectStatisticsPopup } from '../ProjectStatisticsPopup/ProjectStatisticsPopup';
 import { ReplaceAttributionPopup } from '../ReplaceAttributionPopup/ReplaceAttributionPopup';
@@ -49,6 +51,8 @@ function getPopupComponent(popupType: PopupType | null): ReactElement | null {
       return <ConfirmMultiSelectDeletionPopup />;
     case PopupType.EditAttributionPopup:
       return <EditAttributionPopup />;
+    case PopupType.PackageSearchPopup:
+      return <PackageSearchPopup />;
     case PopupType.ChangedInputFilePopup:
       return <ChangedInputFilePopup />;
     case PopupType.FileSupportPopup:
@@ -59,6 +63,8 @@ function getPopupComponent(popupType: PopupType | null): ReactElement | null {
       return <UpdateAppPopup />;
     case PopupType.LocatorPopup:
       return <LocatorPopup />;
+    case PopupType.ModifyWasPreferredAttributionPopup:
+      return <ModifyWasPreferredAttributionPopup />;
     case PopupType.ChangePreferredStatusGloballyPopup:
       return <ChangePreferredStatusGloballyPopup />;
     default:
diff --git a/src/Frontend/Components/ModifyWasPreferredAttributionPopup/ModifyWasPreferredAttributionPopup.tsx b/src/Frontend/Components/ModifyWasPreferredAttributionPopup/ModifyWasPreferredAttributionPopup.tsx
new file mode 100644
index 00000000..2171a951
--- /dev/null
+++ b/src/Frontend/Components/ModifyWasPreferredAttributionPopup/ModifyWasPreferredAttributionPopup.tsx
@@ -0,0 +1,96 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import MuiDialogContentText from '@mui/material/DialogContentText';
+import { ReactElement } from 'react';
+
+import { ButtonText, View } from '../../enums/enums';
+import {
+  closePopupAndUnsetTargets,
+  removeWasPreferred,
+  saveTemporaryDisplayPackageInfoAndNavigateToTargetViewIfSavingIsNotDisabled,
+  unlinkAttributionAndSavePackageInfoAndNavigateToTargetViewIfSavingIsNotDisabled,
+} from '../../state/actions/popup-actions/popup-actions';
+import { useAppDispatch, useAppSelector } from '../../state/hooks';
+import {
+  getCurrentAttributionId,
+  getIsGlobalSavingDisabled,
+  getManualAttributionsToResources,
+} from '../../state/selectors/all-views-resource-selectors';
+import { getSelectedView } from '../../state/selectors/view-selector';
+import { hasAttributionMultipleResources } from '../../util/has-attribution-multiple-resources';
+import { WasPreferredIcon } from '../Icons/Icons';
+import { NotificationPopup } from '../NotificationPopup/NotificationPopup';
+
+export function ModifyWasPreferredAttributionPopup(): ReactElement {
+  const dispatch = useAppDispatch();
+  const currentAttributionId = useAppSelector(getCurrentAttributionId);
+  const attributionsToResources = useAppSelector(
+    getManualAttributionsToResources,
+  );
+  const view = useAppSelector(getSelectedView);
+  const isGlobalSavingDisabled = useAppSelector(getIsGlobalSavingDisabled);
+  const showSaveGloballyButton =
+    view === View.Audit &&
+    hasAttributionMultipleResources(
+      currentAttributionId,
+      attributionsToResources,
+    );
+
+  function handleSaveClick(): void {
+    dispatch(removeWasPreferred());
+    dispatch(
+      unlinkAttributionAndSavePackageInfoAndNavigateToTargetViewIfSavingIsNotDisabled(),
+    );
+  }
+
+  function handleSaveGloballyClick(): void {
+    dispatch(removeWasPreferred());
+    dispatch(
+      saveTemporaryDisplayPackageInfoAndNavigateToTargetViewIfSavingIsNotDisabled(),
+    );
+  }
+
+  function handleCancelClick(): void {
+    dispatch(closePopupAndUnsetTargets());
+  }
+
+  const content = (
+    <MuiDialogContentText style={{ display: 'flex', alignItems: 'center' }}>
+      You are about to modify an attribution that was preferred in the past. Are
+      you sure you want to continue? The attribution will no longer be marked
+      with a <WasPreferredIcon />.
+    </MuiDialogContentText>
+  );
+
+  return (
+    <NotificationPopup
+      content={content}
+      header={'Warning'}
+      centerLeftButtonConfig={{
+        onClick: showSaveGloballyButton
+          ? handleSaveClick
+          : handleSaveGloballyClick,
+        buttonText: ButtonText.Save,
+      }}
+      centerRightButtonConfig={
+        showSaveGloballyButton
+          ? {
+              onClick: handleSaveGloballyClick,
+              buttonText: ButtonText.SaveGlobally,
+              disabled: isGlobalSavingDisabled,
+              color: 'secondary',
+            }
+          : undefined
+      }
+      rightButtonConfig={{
+        onClick: handleCancelClick,
+        buttonText: ButtonText.Cancel,
+        color: 'secondary',
+      }}
+      isOpen={true}
+      aria-label={'modify was preferred attribution popup'}
+    />
+  );
+}
diff --git a/src/Frontend/Components/NotSavedPopup/NotSavedPopup.tsx b/src/Frontend/Components/NotSavedPopup/NotSavedPopup.tsx
index c02e0d9c..6206f0e3 100644
--- a/src/Frontend/Components/NotSavedPopup/NotSavedPopup.tsx
+++ b/src/Frontend/Components/NotSavedPopup/NotSavedPopup.tsx
@@ -6,10 +6,10 @@ import { ReactElement } from 'react';
 
 import { ButtonText, View } from '../../enums/enums';
 import {
-  checkIfPreferredStatusChangedAndShowWarningOrSave,
+  checkIfWasPreferredAndShowWarningOrUnlinkAndSave,
+  checkIfWasPreferredOrPreferredStatusChangedAndShowWarningOrSave,
   closePopupAndUnsetTargets,
   navigateToTargetResourceOrAttributionOrOpenFileDialog,
-  unlinkAttributionAndSavePackageInfoAndNavigateToTargetViewIfSavingIsNotDisabled,
 } from '../../state/actions/popup-actions/popup-actions';
 import { useAppDispatch, useAppSelector } from '../../state/hooks';
 import {
@@ -39,13 +39,11 @@ export function NotSavedPopup(): ReactElement {
     );
 
   function handleSaveClick(): void {
-    dispatch(
-      unlinkAttributionAndSavePackageInfoAndNavigateToTargetViewIfSavingIsNotDisabled(),
-    );
+    dispatch(checkIfWasPreferredAndShowWarningOrUnlinkAndSave());
   }
 
   function handleSaveGloballyClick(): void {
-    dispatch(checkIfPreferredStatusChangedAndShowWarningOrSave());
+    dispatch(checkIfWasPreferredOrPreferredStatusChangedAndShowWarningOrSave());
   }
 
   function handleDiscardClick(): void {
diff --git a/src/Frontend/Components/PackageSearchPopup/ClearlyDefinedPackageCard.tsx b/src/Frontend/Components/PackageSearchPopup/ClearlyDefinedPackageCard.tsx
new file mode 100644
index 00000000..afd55778
--- /dev/null
+++ b/src/Frontend/Components/PackageSearchPopup/ClearlyDefinedPackageCard.tsx
@@ -0,0 +1,143 @@
+// SPDX-FileCopyrightText: Nico Carl <nicocarl@protonmail.com>
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import PlusIcon from '@mui/icons-material/Add';
+import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
+import MuiAccordion from '@mui/material/Accordion';
+import MuiAccordionDetails from '@mui/material/AccordionDetails';
+import MuiAccordionSummary from '@mui/material/AccordionSummary';
+import MuiBox from '@mui/material/Box';
+import MuiSkeleton from '@mui/material/Skeleton';
+import MuiTypography from '@mui/material/Typography';
+import { useQuery } from '@tanstack/react-query';
+import { ReactElement, useState } from 'react';
+
+import { EMPTY_DISPLAY_PACKAGE_INFO } from '../../shared-constants';
+import { baseIcon } from '../../shared-styles';
+import { setTemporaryDisplayPackageInfo } from '../../state/actions/resource-actions/all-views-simple-actions';
+import { closePopup } from '../../state/actions/view-actions/view-actions';
+import { useAppDispatch } from '../../state/hooks';
+import { Alert } from '../Alert/Alert';
+import { IconButton } from '../IconButton/IconButton';
+import { fetchFromClearlyDefined } from './fetch-from-clearly-defined';
+
+interface ClearlyDefinedPackageCardProps {
+  coordinate: string;
+}
+
+const classes = {
+  baseIcon,
+  copyrightHolders: {
+    whiteSpace: 'pre-wrap',
+  },
+  text: {
+    textOverflow: 'ellipsis',
+    overflow: 'overflow',
+    whiteSpace: 'nowrap',
+    width: '25%',
+  },
+  accordionSummary: {
+    width: '100%',
+  },
+  accordionSummaryContent: {
+    width: 'calc( 100% - 24px)',
+  },
+};
+
+export function ClearlyDefinedPackageCard(
+  props: ClearlyDefinedPackageCardProps,
+): ReactElement {
+  const dispatch = useAppDispatch();
+
+  const [expanded, setExpanded] = useState<boolean>(false);
+  const { isLoading, data, isError, error } = useQuery({
+    queryKey: ['clearlyDefinedDefinition', props.coordinate],
+    queryFn: () => fetchFromClearlyDefined(props.coordinate),
+    refetchOnWindowFocus: false,
+  });
+
+  function handleExpansion(): void {
+    if (data?.copyright) {
+      setExpanded((prev) => !prev);
+    }
+  }
+
+  return (
+    <MuiAccordion square={true} expanded={expanded} onChange={handleExpansion}>
+      <MuiAccordionSummary
+        expandIcon={
+          <ExpandMoreIcon
+            aria-label={'expand accordion'}
+            color={data?.copyright ? 'primary' : 'disabled'}
+          />
+        }
+        sx={{
+          '&.MuiAccordionSummary-root': classes.accordionSummary,
+          '& div.MuiAccordionSummary-content': classes.accordionSummaryContent,
+        }}
+      >
+        {!isError ? (
+          <>
+            {isLoading ? (
+              <>
+                <MuiBox sx={classes.text}>
+                  <MuiSkeleton width="60px" />
+                </MuiBox>
+                <MuiTypography noWrap sx={classes.text}>
+                  {props.coordinate}
+                </MuiTypography>
+                <MuiBox sx={classes.text}>
+                  <MuiSkeleton width="60px" />
+                </MuiBox>
+                <MuiBox sx={classes.text}>
+                  <MuiSkeleton width="60px" />
+                </MuiBox>
+              </>
+            ) : (
+              <>
+                <MuiTypography noWrap sx={classes.text}>
+                  {data?.packageName} - {data?.packageVersion}
+                </MuiTypography>
+                <MuiTypography noWrap sx={classes.text}>
+                  {props.coordinate}
+                </MuiTypography>
+                <MuiTypography noWrap color="text.secondary" sx={classes.text}>
+                  {data?.licenseName ?? ''}
+                </MuiTypography>
+                <MuiTypography noWrap sx={classes.text}>
+                  {data?.url ?? ''}
+                </MuiTypography>
+              </>
+            )}
+            <IconButton
+              tooltipTitle={'Add'}
+              tooltipPlacement={'right'}
+              disabled={!Boolean(data)}
+              onClick={(): void => {
+                dispatch(
+                  setTemporaryDisplayPackageInfo(
+                    data ?? EMPTY_DISPLAY_PACKAGE_INFO,
+                  ),
+                );
+                dispatch(closePopup());
+              }}
+              icon={<PlusIcon sx={classes.baseIcon} />}
+            />
+          </>
+        ) : (
+          <Alert
+            errorMessage={`Failed while fetching data for ${props.coordinate}${
+              error instanceof Error ? `: ${error.message}` : ''
+            }`}
+          />
+        )}
+      </MuiAccordionSummary>
+      <MuiAccordionDetails>
+        <MuiTypography sx={classes.copyrightHolders} variant={'body1'}>
+          {data?.copyright ?? ''}
+        </MuiTypography>
+      </MuiAccordionDetails>
+    </MuiAccordion>
+  );
+}
diff --git a/src/Frontend/Components/PackageSearchPopup/PackageSearchPopup.tsx b/src/Frontend/Components/PackageSearchPopup/PackageSearchPopup.tsx
new file mode 100644
index 00000000..22a455dc
--- /dev/null
+++ b/src/Frontend/Components/PackageSearchPopup/PackageSearchPopup.tsx
@@ -0,0 +1,160 @@
+// SPDX-FileCopyrightText: Nico Carl <nicocarl@protonmail.com>
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import MuiBox from '@mui/material/Box';
+import MuiTypography from '@mui/material/Typography';
+import { useQuery } from '@tanstack/react-query';
+import axios from 'axios';
+import { Validator } from 'jsonschema';
+import { ReactElement, useEffect, useState } from 'react';
+
+import { PackageInfo } from '../../../shared/shared-types';
+import { ButtonText } from '../../enums/enums';
+import { closePopup } from '../../state/actions/view-actions/view-actions';
+import { useAppDispatch, useAppSelector } from '../../state/hooks';
+import { getTemporaryDisplayPackageInfo } from '../../state/selectors/all-views-resource-selectors';
+import { Alert } from '../Alert/Alert';
+import { NotificationPopup } from '../NotificationPopup/NotificationPopup';
+import { SearchTextField } from '../SearchTextField/SearchTextField';
+import { Spinner } from '../Spinner/Spinner';
+import { ClearlyDefinedPackageCard } from './ClearlyDefinedPackageCard';
+
+const classes = {
+  root: {
+    minWidth: '300px',
+  },
+  packages: {
+    margin: '0px 10px 0px 10px',
+  },
+  noResultsFound: {
+    display: 'flex',
+    justifyContent: 'center',
+    alignItems: 'center',
+  },
+};
+
+function useDebounceInput(input: string, delay: number): string {
+  const [debouncedInput, setDebouncedInput] = useState<string>(input);
+
+  useEffect(() => {
+    const handler = setTimeout(() => {
+      setDebouncedInput(input);
+    }, delay);
+
+    return (): void => {
+      clearTimeout(handler);
+    };
+  }, [delay, input]);
+
+  return debouncedInput;
+}
+
+const jsonSchemaValidator = new Validator();
+const definitionsSchema = {
+  type: 'array',
+  items: { type: 'string' },
+};
+
+async function searchPackagesOnClearlyDefined(
+  searchTerm: string,
+): Promise<Array<string>> {
+  const response = await axios.get(
+    `https://api.clearlydefined.io/definitions?pattern=${searchTerm}`,
+  );
+  jsonSchemaValidator.validate(response.data, definitionsSchema, {
+    throwError: true,
+  });
+  return response.data as Array<string>;
+}
+
+function getInitialSearchTerm(packageInfo: PackageInfo): string {
+  if (!packageInfo.packageName) {
+    return '';
+  }
+
+  return `${
+    packageInfo.packageNamespace ? `${packageInfo.packageNamespace}/` : ''
+  }${packageInfo.packageName}/${packageInfo.packageVersion ?? ''}`;
+}
+
+export function PackageSearchPopup(): ReactElement {
+  const dispatch = useAppDispatch();
+  const temporaryDisplayPackageInfo = useAppSelector(
+    getTemporaryDisplayPackageInfo,
+  );
+  const [currentSearchTerm, setCurrentSearchTerm] = useState<string>(
+    getInitialSearchTerm(temporaryDisplayPackageInfo),
+  );
+  const debounceDelayInMs = 500;
+  const debouncedSearchTerm = useDebounceInput(
+    currentSearchTerm,
+    debounceDelayInMs,
+  );
+  const { isLoading, data, isError, error } = useQuery({
+    queryKey: ['clearlyDefinedPackageSearch', debouncedSearchTerm],
+    queryFn: () => searchPackagesOnClearlyDefined(debouncedSearchTerm),
+    refetchOnWindowFocus: false,
+    enabled: Boolean(debouncedSearchTerm),
+  });
+
+  function close(): void {
+    dispatch(closePopup());
+  }
+
+  function handleChange(search: string): void {
+    setCurrentSearchTerm(search);
+  }
+
+  const content = (
+    <MuiBox sx={classes.root}>
+      <SearchTextField
+        onInputChange={handleChange}
+        search={currentSearchTerm}
+        autoFocus={false}
+      />
+      {isLoading && Boolean(currentSearchTerm) ? (
+        <Spinner />
+      ) : isError ? (
+        <Alert
+          errorMessage={`Failed while fetching data${
+            error instanceof Error ? `: ${error.message}` : ''
+          }`}
+        />
+      ) : (
+        <MuiBox sx={classes.packages}>
+          {data && data.length > 0 ? (
+            data.map((packageCoordinates: string, idx: number) => {
+              return (
+                <ClearlyDefinedPackageCard
+                  key={`clearlyDefinedSearch-${debouncedSearchTerm}-${idx}`}
+                  coordinate={packageCoordinates}
+                />
+              );
+            })
+          ) : (
+            <MuiBox sx={classes.noResultsFound}>
+              <MuiTypography>{'No results found'}</MuiTypography>
+            </MuiBox>
+          )}
+        </MuiBox>
+      )}
+    </MuiBox>
+  );
+
+  return (
+    <NotificationPopup
+      content={content}
+      header={'Search for packages'}
+      isOpen={true}
+      fullWidth={true}
+      rightButtonConfig={{
+        onClick: close,
+        buttonText: ButtonText.Cancel,
+      }}
+      onBackdropClick={close}
+      onEscapeKeyDown={close}
+    />
+  );
+}
diff --git a/src/Frontend/Components/PackageSearchPopup/fetch-from-clearly-defined.ts b/src/Frontend/Components/PackageSearchPopup/fetch-from-clearly-defined.ts
new file mode 100644
index 00000000..2e42e431
--- /dev/null
+++ b/src/Frontend/Components/PackageSearchPopup/fetch-from-clearly-defined.ts
@@ -0,0 +1,130 @@
+// SPDX-FileCopyrightText: Nico Carl <nicocarl@protonmail.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import axios from 'axios';
+import { Schema, Validator } from 'jsonschema';
+
+import { DisplayPackageInfo } from '../../../shared/shared-types';
+
+const jsonSchemaValidator = new Validator();
+
+interface Payload {
+  licensed: {
+    declared?: string;
+    facets?: {
+      core: {
+        attribution: {
+          parties?: Array<string>;
+        };
+      };
+    };
+  };
+  coordinates: {
+    type: string;
+    provider: string;
+    namespace?: string;
+    name: string;
+    revision: string;
+  };
+  described: {
+    urls?: {
+      registry?: string;
+      version?: string;
+    };
+    sourceLocation?: {
+      url?: string;
+    };
+  };
+}
+
+const clearlyDefinedSchema: Schema = {
+  type: 'object',
+  properties: {
+    described: {
+      type: 'object',
+      properties: {
+        urls: {
+          type: 'object',
+          properties: {
+            registry: {
+              type: 'string',
+            },
+            version: { type: 'string' },
+          },
+        },
+        sourceLocation: {
+          type: 'object',
+          properties: {
+            url: { type: 'string' },
+          },
+        },
+      },
+    },
+    licensed: {
+      type: 'object',
+      properties: {
+        declared: { type: 'string' },
+        facets: {
+          type: 'object',
+          properties: {
+            core: {
+              type: 'object',
+              properties: {
+                attribution: {
+                  type: 'object',
+                  properties: {
+                    parties: { type: 'array', items: { type: 'string' } },
+                  },
+                },
+              },
+              required: ['attribution'],
+            },
+          },
+          required: ['core'],
+        },
+      },
+    },
+    coordinates: {
+      type: 'object',
+      properties: {
+        type: { type: 'string' },
+        provider: { type: 'string' },
+        namespace: { type: 'string' },
+        name: { type: 'string' },
+        revision: { type: 'string' },
+      },
+      required: ['type', 'provider', 'name', 'revision'],
+    },
+  },
+  required: ['licensed', 'coordinates', 'described'],
+};
+
+export async function fetchFromClearlyDefined(
+  coordinate: string,
+): Promise<DisplayPackageInfo> {
+  const response = await axios.get(
+    `https://api.clearlydefined.io/definitions/${coordinate}`,
+  );
+  const payload = response.data as Payload;
+  jsonSchemaValidator.validate(payload, clearlyDefinedSchema, {
+    throwError: true,
+  });
+  return {
+    packageName: payload?.coordinates.name,
+    packageVersion: payload?.coordinates.revision,
+    packageNamespace: payload?.coordinates.namespace,
+    packageType: payload?.coordinates.type,
+    licenseName: payload?.licensed.declared,
+    copyright: payload?.licensed.facets?.core.attribution.parties
+      ? concatCopyright(payload.licensed.facets.core.attribution.parties)
+      : undefined,
+    url:
+      payload?.described.urls?.version ??
+      payload?.described.sourceLocation?.url,
+    attributionIds: [],
+  };
+}
+
+function concatCopyright(copyrightHolders: Array<string>): string {
+  return copyrightHolders.join('\n');
+}
diff --git a/src/Frontend/Components/ResourceDetailsAttributionColumn/ResourceDetailsAttributionColumn.tsx b/src/Frontend/Components/ResourceDetailsAttributionColumn/ResourceDetailsAttributionColumn.tsx
index d11376b3..1eabf221 100644
--- a/src/Frontend/Components/ResourceDetailsAttributionColumn/ResourceDetailsAttributionColumn.tsx
+++ b/src/Frontend/Components/ResourceDetailsAttributionColumn/ResourceDetailsAttributionColumn.tsx
@@ -58,7 +58,9 @@ export function ResourceDetailsAttributionColumn(
   const dispatch = useAppDispatch();
 
   function dispatchUnlinkAttributionAndSavePackageInfoOrOpenWasPreferredPopup(): void {
-    if (attributionIdOfSelectedPackageInManualPanel) {
+    if (temporaryDisplayPackageInfo.wasPreferred) {
+      dispatch(openPopup(PopupType.ModifyWasPreferredAttributionPopup));
+    } else if (attributionIdOfSelectedPackageInManualPanel) {
       dispatch(
         unlinkAttributionAndSavePackageInfo(
           selectedResourceId,
@@ -70,7 +72,9 @@ export function ResourceDetailsAttributionColumn(
   }
 
   function dispatchSavePackageInfoOrOpenPreferGloballyOrWasPreferredPopup(): void {
-    if (showSaveGloballyButton && didPreferredFieldChange) {
+    if (temporaryDisplayPackageInfo.wasPreferred) {
+      dispatch(openPopup(PopupType.ModifyWasPreferredAttributionPopup));
+    } else if (showSaveGloballyButton && didPreferredFieldChange) {
       dispatch(openPopup(PopupType.ChangePreferredStatusGloballyPopup));
     } else {
       dispatch(
@@ -126,7 +130,9 @@ export function ResourceDetailsAttributionColumn(
   }
 
   function saveFileRequestListener(): void {
-    if (
+    if (temporaryDisplayPackageInfo.wasPreferred) {
+      dispatch(openPopup(PopupType.ModifyWasPreferredAttributionPopup));
+    } else if (
       showSaveGloballyButton &&
       !isGlobalSavingDisabled &&
       didPreferredFieldChange
diff --git a/src/Frontend/Components/TextFieldStack/TextFieldStack.tsx b/src/Frontend/Components/TextFieldStack/TextFieldStack.tsx
new file mode 100644
index 00000000..e5f5d10a
--- /dev/null
+++ b/src/Frontend/Components/TextFieldStack/TextFieldStack.tsx
@@ -0,0 +1,46 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import MuiBox from '@mui/material/Box';
+import { ReactElement } from 'react';
+
+import { usePackageInfoChangeHandler } from '../../util/use-package-info-change-handler';
+import { attributionColumnClasses } from '../AttributionColumn/shared-attribution-column-styles';
+import { TextBox } from '../InputElements/TextBox';
+
+interface TextFieldStackProps {
+  isEditable: boolean;
+  comments: Array<string>;
+  showHighlight?: boolean;
+}
+
+export function TextFieldStack(props: TextFieldStackProps): ReactElement {
+  const handleChange = usePackageInfoChangeHandler();
+  const filteredComments = props.comments.filter(
+    (comment) => comment.replace(/\s/g, '') !== '',
+  );
+
+  if (filteredComments.length === 0) {
+    filteredComments.push('');
+  }
+
+  return (
+    <MuiBox sx={attributionColumnClasses.panel}>
+      {filteredComments.map((comment, index) => (
+        <TextBox
+          key={index}
+          isEditable={props.isEditable}
+          title={`Comment ${
+            filteredComments.length === 1 ? '' : index + 1
+          }`.trim()}
+          text={comment}
+          minRows={3}
+          maxRows={10}
+          multiline={true}
+          handleChange={handleChange('comments')}
+        />
+      ))}
+    </MuiBox>
+  );
+}
diff --git a/src/Frontend/enums/enums.ts b/src/Frontend/enums/enums.ts
index e90c5595..216fb4b2 100644
--- a/src/Frontend/enums/enums.ts
+++ b/src/Frontend/enums/enums.ts
@@ -22,11 +22,13 @@ export enum PopupType {
   ConfirmDeletionGloballyPopup = 'ConfirmDeletionGloballyPopup',
   ConfirmMultiSelectDeletionPopup = 'ConfirmMultiSelectDeletionPopup',
   EditAttributionPopup = 'EditAttributionPopup',
+  PackageSearchPopup = 'PackageSearchPopup',
   ChangedInputFilePopup = 'ChangedInputFilePopup',
   FileSupportPopup = 'FileSupportPopup',
   FileSupportDotOpossumAlreadyExistsPopup = 'FileSupportDotOpossumAlreadyExistsPopup',
   UpdateAppPopup = 'UpdateAppPopup',
   LocatorPopup = 'LocatorPopup',
+  ModifyWasPreferredAttributionPopup = 'ModifyWasPreferredAttributionPopup',
   ChangePreferredStatusGloballyPopup = 'ChangePreferredStatusGloballyPopup',
 }
 
diff --git a/src/Frontend/state/actions/popup-actions/popup-actions.ts b/src/Frontend/state/actions/popup-actions/popup-actions.ts
index dd384588..1d68c4e8 100644
--- a/src/Frontend/state/actions/popup-actions/popup-actions.ts
+++ b/src/Frontend/state/actions/popup-actions/popup-actions.ts
@@ -176,6 +176,29 @@ export function unlinkAttributionAndSavePackageInfoAndNavigateToTargetViewIfSavi
   };
 }
 
+export function checkIfWasPreferredAndShowWarningOrUnlinkAndSave(): AppThunkAction {
+  return (dispatch: AppThunkDispatch, getState: () => State): void => {
+    const currentAttributionId = getCurrentAttributionId(getState());
+    const temporaryDisplayPackageInfo =
+      getTemporaryDisplayPackageInfo(getState());
+
+    if (temporaryDisplayPackageInfo.wasPreferred) {
+      dispatch(closePopup());
+      currentAttributionId &&
+        dispatch(
+          openPopup(
+            PopupType.ModifyWasPreferredAttributionPopup,
+            currentAttributionId,
+          ),
+        );
+    } else {
+      dispatch(
+        unlinkAttributionAndSavePackageInfoAndNavigateToTargetViewIfSavingIsNotDisabled(),
+      );
+    }
+  };
+}
+
 export function saveTemporaryDisplayPackageInfoAndNavigateToTargetViewIfSavingIsNotDisabled(): AppThunkAction {
   return (dispatch: AppThunkDispatch, getState: () => State): void => {
     const selectedResourceId = getSelectedResourceId(getState());
@@ -198,9 +221,11 @@ export function saveTemporaryDisplayPackageInfoAndNavigateToTargetViewIfSavingIs
   };
 }
 
-export function checkIfPreferredStatusChangedAndShowWarningOrSave(): AppThunkAction {
+export function checkIfWasPreferredOrPreferredStatusChangedAndShowWarningOrSave(): AppThunkAction {
   return (dispatch: AppThunkDispatch, getState: () => State): void => {
     const currentAttributionId = getCurrentAttributionId(getState());
+    const temporaryDisplayPackageInfo =
+      getTemporaryDisplayPackageInfo(getState());
     const attributionsToResources =
       getManualAttributionsToResources(getState());
     const attributionHasMultipleResources = hasAttributionMultipleResources(
@@ -208,7 +233,16 @@ export function checkIfPreferredStatusChangedAndShowWarningOrSave(): AppThunkAct
       attributionsToResources,
     );
 
-    if (
+    if (temporaryDisplayPackageInfo.wasPreferred) {
+      dispatch(closePopup());
+      currentAttributionId &&
+        dispatch(
+          openPopup(
+            PopupType.ModifyWasPreferredAttributionPopup,
+            currentAttributionId,
+          ),
+        );
+    } else if (
       attributionHasMultipleResources &&
       getDidPreferredFieldChange(getState())
     ) {
@@ -336,3 +370,11 @@ export function locateSignalsFromProjectStatisticsPopup(
     }
   };
 }
+
+export function removeWasPreferred(): AppThunkAction {
+  return (dispatch: AppThunkDispatch, getState: () => State): void => {
+    const { wasPreferred, ...temporaryDisplayPackageInfo } =
+      getTemporaryDisplayPackageInfo(getState());
+    dispatch(setTemporaryDisplayPackageInfo(temporaryDisplayPackageInfo));
+  };
+}
diff --git a/src/Frontend/util/ensure-array.ts b/src/Frontend/util/ensure-array.ts
deleted file mode 100644
index 2355abf1..00000000
--- a/src/Frontend/util/ensure-array.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-
-/**
- * Makes sure that value ends up being array if not already an array
- * @param input
- */
-export function ensureArray<T>(
-  input: T | Array<T> | null | undefined,
-): Array<T> {
-  if (input === null || input === undefined) {
-    return [];
-  }
-
-  return Array.isArray(input) ? input : [input];
-}
diff --git a/src/Frontend/util/handle-purl.ts b/src/Frontend/util/handle-purl.ts
index a2461fba..449e5b78 100644
--- a/src/Frontend/util/handle-purl.ts
+++ b/src/Frontend/util/handle-purl.ts
@@ -5,7 +5,6 @@
 import { PackageURL } from 'packageurl-js';
 
 import { PackageInfo } from '../../shared/shared-types';
-import { text } from '../../shared/text';
 
 export function parsePurl(purl: string): PackageURL | undefined {
   try {
@@ -16,18 +15,14 @@ export function parsePurl(purl: string): PackageURL | undefined {
 }
 
 export function generatePurl(packageInfo: PackageInfo): string {
-  try {
-    return packageInfo.packageType?.trim() && packageInfo.packageName?.trim()
-      ? new PackageURL(
-          packageInfo.packageType.trim(),
-          packageInfo?.packageNamespace?.trim(),
-          packageInfo.packageName.trim(),
-          packageInfo?.packageVersion?.trim(),
-          undefined,
-          undefined,
-        ).toString()
-      : '';
-  } catch (error) {
-    return text.attributionColumn.invalidPurl;
-  }
+  return packageInfo.packageType?.trim() && packageInfo.packageName?.trim()
+    ? new PackageURL(
+        packageInfo.packageType.trim(),
+        packageInfo?.packageNamespace?.trim(),
+        packageInfo.packageName.trim(),
+        packageInfo?.packageVersion?.trim(),
+        undefined,
+        undefined,
+      ).toString()
+    : '';
 }
diff --git a/src/Frontend/util/http-client.ts b/src/Frontend/util/http-client.ts
deleted file mode 100644
index 2c820979..00000000
--- a/src/Frontend/util/http-client.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-
-export interface RequestProps {
-  baseUrl: string;
-  body?: object;
-  headers?: Record<string, string>;
-  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
-  params?: Record<string, string | number | undefined>;
-  path?: string;
-}
-
-export class HttpClient {
-  public request({
-    baseUrl,
-    body,
-    headers,
-    method = 'GET',
-    params = {},
-    path,
-  }: RequestProps): Promise<Response> {
-    const url = path ? new URL(path, baseUrl) : new URL(baseUrl);
-
-    Object.entries(params).forEach(([key, value]) => {
-      if (value !== undefined) {
-        url.searchParams.append(key, value.toString());
-      }
-    });
-
-    return fetch(url, { method, headers, body: JSON.stringify(body) });
-  }
-}
diff --git a/src/Frontend/util/is-important-attribution-information-missing.ts b/src/Frontend/util/is-important-attribution-information-missing.ts
index e2b9fda1..37df6a07 100644
--- a/src/Frontend/util/is-important-attribution-information-missing.ts
+++ b/src/Frontend/util/is-important-attribution-information-missing.ts
@@ -9,8 +9,8 @@ const TYPES_REQUIRING_NAMESPACE = [
   'bitbucket',
   'composer',
   'deb',
+  'golang',
   'github',
-  'gitlab',
   'maven',
 ];
 
diff --git a/src/Frontend/util/lodash-extension-utils.ts b/src/Frontend/util/lodash-extension-utils.ts
index 3aa9d007..917795cb 100644
--- a/src/Frontend/util/lodash-extension-utils.ts
+++ b/src/Frontend/util/lodash-extension-utils.ts
@@ -2,7 +2,7 @@
 // SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
 //
 // SPDX-License-Identifier: Apache-2.0
-import { omit as _omit, pick as _pick, remove } from 'lodash';
+import { remove } from 'lodash';
 
 import { isIdOfResourceWithChildren } from './can-resource-have-children';
 
@@ -73,19 +73,3 @@ export function isChildOf(parentId: string, possibleChildId: string): boolean {
 
   return possibleChildId.includes(parentId);
 }
-
-/** Improves the lodash pick typing by correctly inferring the type of keys */
-export function pick<T extends object, K extends keyof T>(
-  object: T,
-  keys: Array<K>,
-): Pick<T, K> {
-  return _pick(object, keys);
-}
-
-/** Improves the lodash omit typing by correctly inferring the type of keys */
-export function omit<T extends object, K extends keyof T>(
-  object: T,
-  keys: Array<K>,
-): Omit<T, K> {
-  return _omit(object, keys);
-}
diff --git a/src/Frontend/util/package-search-api.ts b/src/Frontend/util/package-search-api.ts
deleted file mode 100644
index 3b11cc33..00000000
--- a/src/Frontend/util/package-search-api.ts
+++ /dev/null
@@ -1,624 +0,0 @@
-// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-import { compareVersions, validate } from 'compare-versions';
-import { compact, mapValues, partition } from 'lodash';
-
-import {
-  AutocompleteSignal,
-  DisplayPackageInfo,
-  PackageInfo,
-  Source,
-} from '../../shared/shared-types';
-import { text } from '../../shared/text';
-import { HttpClient } from './http-client';
-import { pick } from './lodash-extension-utils';
-
-const DEPS_DEV_NON_STANDARD_LICENSE_MARKER = 'non-standard';
-const COPYRIGHT_REGEX = /^Copyright \(c\).*$/gm;
-const GITHUB_REGEX = /^https:\/\/(www\.)?github\.com\/([^\/]+)\/([^\/]+)/;
-const GITLAB_REGEX = /^https:\/\/(www\.)?gitlab\.com\/([^\/]+)\/([^\/]+)/;
-
-export const packageSystems = [
-  'GO',
-  'NPM',
-  'CARGO',
-  'MAVEN',
-  'PYPI',
-  'NUGET',
-] as const;
-export type PackageSystem = (typeof packageSystems)[number];
-
-export const packageSystemsRequiringNamespace: Array<PackageSystem> = ['MAVEN'];
-
-export const projectTypes = ['GITHUB', 'GITLAB', 'BITBUCKET'] as const;
-export type ProjectType = (typeof projectTypes)[number];
-
-export interface PackageSuggestion {
-  kind: 'PACKAGE';
-  name: string;
-  system: PackageSystem;
-  projectType?: never;
-}
-
-export interface ProjectSuggestion {
-  kind: 'PROJECT';
-  name: string;
-  system?: never;
-  projectType: ProjectType;
-}
-
-export interface AdvisorySuggestion {
-  kind: 'ADVISORY';
-  name: string;
-  system?: never;
-  projectType?: never;
-}
-
-export type SearchSuggestion =
-  | PackageSuggestion
-  | ProjectSuggestion
-  | AdvisorySuggestion;
-
-export interface SearchSuggestionResponse {
-  results: Array<SearchSuggestion>;
-}
-
-export interface VersionKey {
-  system: PackageSystem;
-  name: string;
-  version: string;
-}
-
-export interface Links {
-  origins?: Array<string>;
-  homepage?: string;
-  issues?: string;
-  repo?: string;
-}
-
-export interface WebVersionResponse {
-  version: {
-    description?: string;
-    licenses?: Array<string>;
-    links?: Links;
-  };
-}
-
-export interface VersionResponse {
-  versionKey: VersionKey;
-  publishedAt: string;
-  isDefault: boolean;
-}
-
-export interface DefaultVersionResponse {
-  defaultVersion: string;
-}
-
-export interface VersionsResponse {
-  versions: Array<VersionResponse>;
-}
-
-export interface GitHubLicenseResponse {
-  license: { name: string } | null;
-  content: string;
-}
-
-export interface LatestReleaseResponse {
-  tag_name?: string;
-}
-
-export interface TagResponse {
-  name: string;
-}
-
-export interface GitLabProjectResponse {
-  license: { name: string } | null;
-  license_url?: string;
-}
-
-export interface GitLabLicenseResponse {
-  content: string;
-}
-
-export class PackageSearchApi {
-  private readonly baseUrls = {
-    GITHUB: 'https://api.github.com/',
-    GITLAB: 'https://gitlab.com/api/v4/',
-    DEPS_DEV_WEB: 'https://deps.dev/_/',
-    DEPS_DEV_API: 'https://api.deps.dev/v3alpha/',
-  } as const;
-  private readonly projectTypeDomains: Record<ProjectType, string> = {
-    GITHUB: 'github.com',
-    GITLAB: 'gitlab.com',
-    BITBUCKET: 'bitbucket.org',
-  };
-
-  constructor(private readonly httpClient: HttpClient) {}
-
-  private deserialize(
-    input: PackageInfo,
-  ): Partial<SearchSuggestion> & { isComplete: boolean } {
-    const { packageName, packageNamespace, packageType } = mapValues(
-      pick(input, ['packageName', 'packageNamespace', 'packageType']),
-      (value) => value?.trim().toLowerCase(),
-    );
-
-    if (
-      packageType &&
-      [...packageSystems, 'golang'].some(
-        (system) => system.toLowerCase() === packageType,
-      )
-    ) {
-      return {
-        kind: 'PACKAGE',
-        name:
-          packageType === 'maven'
-            ? compact([packageNamespace, packageName]).join(':')
-            : packageName,
-        system:
-          packageType === 'golang'
-            ? 'GO'
-            : (packageType.toUpperCase() as PackageSystem),
-        projectType: undefined,
-        isComplete:
-          packageType === 'maven'
-            ? !!packageName && !!packageNamespace
-            : !!packageName,
-      };
-    }
-
-    if (
-      packageType &&
-      projectTypes.some((type) => type.toLowerCase() === packageType)
-    ) {
-      return {
-        kind: 'PROJECT',
-        name: compact([packageNamespace, packageName]).join('/'),
-        system: undefined,
-        projectType: packageType.toUpperCase() as ProjectType,
-        isComplete: !!packageName && !!packageNamespace,
-      };
-    }
-
-    return {
-      kind: undefined,
-      name: packageName,
-      system: undefined,
-      projectType: undefined,
-      isComplete: false,
-    };
-  }
-
-  private serialize({
-    name,
-    system,
-    projectType,
-    version,
-  }: {
-    name: string;
-    version?: string;
-    system?: PackageSystem;
-    projectType?: ProjectType;
-  }): AutocompleteSignal {
-    const type = system || projectType;
-    const source: Source = {
-      name: text.attributionColumn.openSourceInsights,
-      documentConfidence: 100,
-    };
-    switch (type) {
-      case 'MAVEN': {
-        const [packageNamespace, packageName] = name.split(':');
-        return {
-          default: true,
-          attributionIds: [],
-          packageName,
-          packageNamespace,
-          packageType: 'maven',
-          packageVersion: version,
-          source,
-        };
-      }
-      case 'GITHUB':
-      case 'GITLAB':
-      case 'BITBUCKET': {
-        const [packageNamespace, packageName] = name.split('/');
-        return {
-          default: true,
-          attributionIds: [],
-          packageName,
-          packageNamespace,
-          packageType: type.toLowerCase(),
-          packageVersion: version,
-          source,
-          url: `https://${this.projectTypeDomains[type]}/${name}`,
-        };
-      }
-      case 'GO': {
-        return {
-          default: true,
-          attributionIds: [],
-          packageName: name,
-          packageNamespace: undefined,
-          packageType: 'golang',
-          packageVersion: version,
-          source,
-        };
-      }
-      default: {
-        return {
-          default: true,
-          attributionIds: [],
-          packageName: name,
-          packageNamespace: undefined,
-          packageType: type?.toLowerCase(),
-          packageVersion: version,
-          source,
-        };
-      }
-    }
-  }
-
-  public async getNames(
-    props: PackageInfo,
-  ): Promise<Array<AutocompleteSignal>> {
-    const { name } = this.deserialize(props);
-
-    if (!name) {
-      return [];
-    }
-    const response = await this.httpClient.request({
-      baseUrl: this.baseUrls.DEPS_DEV_WEB, // endpoint not available via API
-      path: 'search/suggest',
-      params: {
-        q: name,
-      },
-    });
-
-    const data: SearchSuggestionResponse = await response.json();
-
-    return data.results
-      .filter(({ kind }) => kind !== 'ADVISORY')
-      .map<AutocompleteSignal>(({ name, system, projectType }) =>
-        this.serialize({ name, system, projectType }),
-      );
-  }
-
-  public async getNamespaces(
-    props: PackageInfo,
-  ): Promise<Array<AutocompleteSignal>> {
-    const { kind, name, system, projectType } = this.deserialize(props);
-
-    if (
-      !name ||
-      (!system && !projectType) ||
-      (system && !packageSystemsRequiringNamespace.includes(system))
-    ) {
-      return [];
-    }
-
-    const response = await this.httpClient.request({
-      baseUrl: this.baseUrls.DEPS_DEV_WEB, // endpoint not available via API
-      path: 'search/suggest',
-      params: { q: name, kind, ...(system && { system }) },
-    });
-    const data: SearchSuggestionResponse = await response.json();
-
-    return data.results.map<AutocompleteSignal>(
-      ({ name, system, projectType }) =>
-        this.serialize({ name, system, projectType }),
-    );
-  }
-
-  public getVersions({
-    packageVersion,
-    ...props
-  }: PackageInfo): Promise<Array<AutocompleteSignal>> {
-    const { isComplete, kind, name, projectType, system } =
-      this.deserialize(props);
-
-    if (isComplete && kind && system && name) {
-      return this.getSystemPackageVersions({ kind, name, projectType, system });
-    }
-
-    if (isComplete && kind && projectType && name) {
-      return this.getProjectPackageVersions({
-        kind,
-        name,
-        projectType,
-        system,
-        packageVersion,
-      });
-    }
-
-    return Promise.resolve([]);
-  }
-
-  private async getSystemPackageVersions({
-    name,
-    system,
-  }: PackageSuggestion): Promise<Array<AutocompleteSignal>> {
-    const response = await this.httpClient.request({
-      baseUrl: this.baseUrls.DEPS_DEV_API,
-      path: `systems/${system}/packages/${encodeURIComponent(name)}`,
-    });
-    const { versions }: VersionsResponse = await response.json();
-
-    const [defaultVersions, otherVersions] = partition(
-      versions
-        .sort((a, b) => {
-          const v1 = a.versionKey.version;
-          const v2 = b.versionKey.version;
-          return validate(v1) && validate(v2) ? compareVersions(v1, v2) : 0;
-        })
-        .reverse(),
-      ({ isDefault }) => isDefault,
-    );
-
-    return [
-      ...defaultVersions.map<AutocompleteSignal>(
-        ({ versionKey: { name, system, version } }) => ({
-          ...this.serialize({ name, system, version }),
-          suffix: '(default)',
-        }),
-      ),
-      ...otherVersions.map<AutocompleteSignal>(
-        ({ versionKey: { name, system, version } }) =>
-          this.serialize({ name, system, version }),
-      ),
-    ];
-  }
-
-  private async getProjectPackageVersions({
-    name,
-    projectType,
-    system,
-    packageVersion,
-  }: ProjectSuggestion & { packageVersion: string | undefined }): Promise<
-    Array<AutocompleteSignal>
-  > {
-    switch (projectType) {
-      case 'GITHUB': {
-        const response = await this.httpClient.request({
-          baseUrl: this.baseUrls.GITHUB,
-          path: `repos/${name}/tags`,
-          params: { per_page: 100 },
-        });
-
-        const tags: Array<TagResponse> = await response.json();
-
-        return tags.map((tag) =>
-          this.serialize({ name, projectType, system, version: tag.name }),
-        );
-      }
-      case 'GITLAB': {
-        const response = await this.httpClient.request({
-          baseUrl: this.baseUrls.GITLAB,
-          path: `projects/${encodeURIComponent(name)}/repository/tags`,
-          params: { search: packageVersion, per_page: 100 },
-        });
-
-        const tags: Array<TagResponse> = await response.json();
-
-        return tags.map((tag) =>
-          this.serialize({ name, projectType, system, version: tag.name }),
-        );
-      }
-      default:
-        return [];
-    }
-  }
-
-  public async enrichPackageInfo(
-    packageInfo: DisplayPackageInfo,
-  ): Promise<DisplayPackageInfo> {
-    if (packageInfo.url && packageInfo.copyright && packageInfo.licenseName) {
-      return packageInfo;
-    }
-
-    const { isComplete, name, projectType, system, kind } =
-      this.deserialize(packageInfo);
-
-    if (isComplete && kind && name && system) {
-      return this.enrichSystemPackageInfo({
-        ...packageInfo,
-        kind,
-        name,
-        system,
-      });
-    }
-
-    if (isComplete && kind && name && projectType) {
-      return this.enrichProjectPackageInfo({
-        ...packageInfo,
-        kind,
-        name,
-        projectType,
-      });
-    }
-
-    return packageInfo;
-  }
-
-  private async enrichSystemPackageInfo({
-    kind,
-    name,
-    system,
-    ...packageInfo
-  }: PackageSuggestion & DisplayPackageInfo): Promise<DisplayPackageInfo> {
-    const effectiveVersion =
-      packageInfo.packageVersion ||
-      (await this.getSystemPackageDefaultVersion({
-        kind,
-        name,
-        system,
-      }));
-
-    if (!effectiveVersion) {
-      return packageInfo;
-    }
-
-    const response = await this.httpClient.request({
-      baseUrl: this.baseUrls.DEPS_DEV_WEB, // website provides source repo URL in a better format than API
-      path: `s/${system}/p/${encodeURIComponent(name)}/v/${encodeURIComponent(
-        effectiveVersion,
-      )}`,
-    });
-
-    if (!response.ok) {
-      return packageInfo;
-    }
-
-    const {
-      version: { links, licenses, description },
-    }: WebVersionResponse = await response.json();
-
-    return this.enrichPackageInfoViaRepoUrl({
-      ...packageInfo,
-      comments: packageInfo.comments || [
-        [
-          ...(description
-            ? [`${text.attributionColumn.description}: ${description}`]
-            : []),
-          ...(links?.homepage
-            ? [`${text.attributionColumn.homepage}: ${links.homepage}`]
-            : []),
-          ...(links?.origins?.length
-            ? links.origins.map(
-                (origin, index, origins) =>
-                  `${text.attributionColumn.origin}${
-                    origins.length > 1 ? ` #${index + 1}` : ''
-                  }: ${origin}`,
-              )
-            : []),
-        ].join('\n'),
-      ],
-      licenseName:
-        packageInfo.licenseName ||
-        licenses
-          ?.filter(
-            (license) => license !== DEPS_DEV_NON_STANDARD_LICENSE_MARKER,
-          )
-          .join(' AND '),
-      url: packageInfo.url || links?.repo,
-    });
-  }
-
-  private async getSystemPackageDefaultVersion({
-    name,
-    system,
-  }: PackageSuggestion): Promise<string | undefined> {
-    const response = await this.httpClient.request({
-      baseUrl: this.baseUrls.DEPS_DEV_WEB, // endpoint not available via API
-      path: `s/${system}/p/${encodeURIComponent(name)}/v/`,
-    });
-
-    if (!response.ok) {
-      return undefined;
-    }
-
-    const { defaultVersion }: DefaultVersionResponse = await response.json();
-
-    return defaultVersion;
-  }
-
-  private enrichProjectPackageInfo({
-    name,
-    projectType,
-    ...packageInfo
-  }: ProjectSuggestion & DisplayPackageInfo): Promise<DisplayPackageInfo> {
-    return this.enrichPackageInfoViaRepoUrl({
-      ...packageInfo,
-      url:
-        packageInfo.url ||
-        `https://${this.projectTypeDomains[projectType]}/${name}`,
-    });
-  }
-
-  private async enrichPackageInfoViaRepoUrl(
-    packageInfo: DisplayPackageInfo,
-  ): Promise<DisplayPackageInfo> {
-    if (packageInfo.copyright && packageInfo.licenseName) {
-      return packageInfo;
-    }
-
-    const githubMatch = packageInfo.url?.match(GITHUB_REGEX);
-
-    if (githubMatch) {
-      const response = await this.httpClient.request({
-        baseUrl: this.baseUrls.GITHUB,
-        path: `repos/${githubMatch[2]}/${githubMatch[3]}/license`,
-      });
-
-      if (!response.ok) {
-        return packageInfo;
-      }
-
-      const { license, content }: GitHubLicenseResponse = await response.json();
-
-      return {
-        ...packageInfo,
-        copyright:
-          packageInfo.copyright || this.getCopyrightFromFileContent(content),
-        licenseName: packageInfo.licenseName || license?.name,
-      };
-    }
-
-    const gitlabMatch = packageInfo.url?.match(GITLAB_REGEX);
-
-    if (gitlabMatch) {
-      const response = await this.httpClient.request({
-        baseUrl: this.baseUrls.GITLAB,
-        path: `projects/${gitlabMatch[2]}%2F${gitlabMatch[3]}`,
-        params: { license: 'true' },
-      });
-
-      if (!response.ok) {
-        return packageInfo;
-      }
-
-      const { license_url: licenseUrl, license }: GitLabProjectResponse =
-        await response.json();
-
-      if (licenseUrl) {
-        const [fileName, ref] = licenseUrl.split('/').reverse();
-
-        const licenseResponse = await this.httpClient.request({
-          baseUrl: this.baseUrls.GITLAB,
-          path: `projects/${gitlabMatch[2]}%2F${gitlabMatch[3]}/repository/files/${fileName}`,
-          params: { ref },
-        });
-
-        if (!licenseResponse.ok) {
-          return {
-            ...packageInfo,
-            licenseName: packageInfo.licenseName || license?.name,
-          };
-        }
-
-        const { content }: GitLabLicenseResponse = await licenseResponse.json();
-
-        return {
-          ...packageInfo,
-          copyright:
-            packageInfo.copyright || this.getCopyrightFromFileContent(content),
-          licenseName: packageInfo.licenseName || license?.name,
-        };
-      }
-
-      return {
-        ...packageInfo,
-        licenseName: packageInfo.licenseName || license?.name,
-      };
-    }
-
-    return packageInfo;
-  }
-
-  private getCopyrightFromFileContent(content: string) {
-    return atob(content).match(COPYRIGHT_REGEX)?.join('\n');
-  }
-}
-
-export default new PackageSearchApi(new HttpClient());
diff --git a/src/Frontend/util/package-search-hooks.ts b/src/Frontend/util/package-search-hooks.ts
deleted file mode 100644
index d6d725f4..00000000
--- a/src/Frontend/util/package-search-hooks.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-import { useMutation, useQuery } from '@tanstack/react-query';
-
-import { DisplayPackageInfo } from '../../shared/shared-types';
-import PackageSearchApi from './package-search-api';
-import { tryit } from './tryit';
-
-function usePackageNames({
-  packageName,
-  packageNamespace,
-  packageType,
-}: DisplayPackageInfo) {
-  const { data, error, isLoading } = useQuery({
-    queryKey: [
-      'package-name-suggestions',
-      packageName,
-      packageNamespace,
-      packageType,
-    ],
-    queryFn: () =>
-      PackageSearchApi.getNames({ packageName, packageNamespace, packageType }),
-    enabled: !!packageName,
-  });
-  return {
-    packageNames: data,
-    packageNamesError: error,
-    packageNamesLoading: isLoading,
-  };
-}
-
-function usePackageNamespaces({
-  packageName,
-  packageNamespace,
-  packageType,
-}: DisplayPackageInfo) {
-  const { data, error, isLoading } = useQuery({
-    queryKey: [
-      'package-namespace-suggestions',
-      packageName,
-      packageNamespace,
-      packageType,
-    ],
-    queryFn: () =>
-      PackageSearchApi.getNamespaces({
-        packageName,
-        packageNamespace,
-        packageType,
-      }),
-    enabled: !!packageName && !!packageType,
-  });
-  return {
-    packageNamespaces: data,
-    packageNamespacesError: error,
-    packageNamespacesLoading: isLoading,
-  };
-}
-
-function usePackageVersions({
-  packageName,
-  packageNamespace,
-  packageType,
-  packageVersion,
-}: DisplayPackageInfo) {
-  const { data, error, isLoading } = useQuery({
-    queryKey: [
-      'package-version-suggestions',
-      packageName,
-      packageNamespace,
-      packageType,
-      packageVersion,
-    ],
-    queryFn: () =>
-      PackageSearchApi.getVersions({
-        packageName,
-        packageNamespace,
-        packageType,
-        packageVersion,
-      }),
-    enabled: !!packageName && !!packageType,
-  });
-  return {
-    packageVersions: data,
-    packageVersionsError: error,
-    packageVersionsLoading: isLoading,
-  };
-}
-
-function useEnrichPackageInfo() {
-  const { mutateAsync, error, isPending } = useMutation({
-    mutationFn: (props: DisplayPackageInfo) =>
-      PackageSearchApi.enrichPackageInfo(props),
-  });
-
-  return {
-    enrichPackageInfo: tryit(mutateAsync),
-    enrichPackageInfoError: error,
-    enrichPackageInfoLoading: isPending,
-  };
-}
-
-export const PackageSearchHooks = {
-  useEnrichPackageInfo,
-  usePackageNames,
-  usePackageNamespaces,
-  usePackageVersions,
-};
diff --git a/src/Frontend/util/tryit.ts b/src/Frontend/util/tryit.ts
deleted file mode 100644
index 287e515c..00000000
--- a/src/Frontend/util/tryit.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-
-/**
- * Converts a function that might throw an error into a function that returns undefined instead.
- * @param func The function that might throw an error. It can be async.
- * @returns
- */
-export function tryit<A extends Array<unknown>, R>(
-  func: (...args: A) => R,
-): (
-  ...args: A
-) => R extends Promise<infer P> ? Promise<P | undefined> : R | undefined {
-  //@ts-expect-error fixing this type error would just repeat the definition above
-  return (...args) => {
-    try {
-      return func(...args);
-    } catch (error) {
-      return undefined;
-    }
-  };
-}
diff --git a/src/Frontend/util/use-debounced-input.ts b/src/Frontend/util/use-debounced-input.ts
deleted file mode 100644
index a78d21cc..00000000
--- a/src/Frontend/util/use-debounced-input.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-import { useEffect, useState } from 'react';
-
-const default_delay = 500;
-
-export function useDebouncedInput<T>(input: T, delay = default_delay): T {
-  const [debouncedInput, setDebouncedInput] = useState<T>(input);
-
-  useEffect(() => {
-    const handler = setTimeout(() => {
-      setDebouncedInput(input);
-    }, delay);
-
-    return (): void => {
-      clearTimeout(handler);
-    };
-  }, [delay, input]);
-
-  return debouncedInput;
-}
diff --git a/src/Frontend/util/use-package-info-change-handler.ts b/src/Frontend/util/use-package-info-change-handler.ts
new file mode 100644
index 00000000..a01af4d3
--- /dev/null
+++ b/src/Frontend/util/use-package-info-change-handler.ts
@@ -0,0 +1,39 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { ChangeEvent } from 'react';
+
+import { DisplayPackageInfo } from '../../shared/shared-types';
+import { setTemporaryDisplayPackageInfo } from '../state/actions/resource-actions/all-views-simple-actions';
+import { useAppDispatch, useAppSelector } from '../state/hooks';
+import { getTemporaryDisplayPackageInfo } from '../state/selectors/all-views-resource-selectors';
+
+export function usePackageInfoChangeHandler(): (
+  propertyToUpdate: keyof DisplayPackageInfo,
+) => (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void {
+  const dispatch = useAppDispatch();
+  const temporaryDisplayPackageInfo = useAppSelector(
+    getTemporaryDisplayPackageInfo,
+  );
+
+  return (propertyToUpdate: keyof DisplayPackageInfo) => {
+    return (
+      event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
+    ): void => {
+      const newValueOfTemporaryDisplayPackageInfoProperty =
+        event.target.type === 'number'
+          ? parseInt(event.target.value)
+          : propertyToUpdate === 'comments'
+            ? [event.target.value]
+            : event.target.value;
+
+      dispatch(
+        setTemporaryDisplayPackageInfo({
+          ...temporaryDisplayPackageInfo,
+          [propertyToUpdate]: newValueOfTemporaryDisplayPackageInfoProperty,
+        }),
+      );
+    };
+  };
+}
diff --git a/src/Frontend/web-workers/scripts/get-autocomplete-signals.ts b/src/Frontend/web-workers/scripts/get-autocomplete-signals.ts
index 4f7508ef..40a9691d 100644
--- a/src/Frontend/web-workers/scripts/get-autocomplete-signals.ts
+++ b/src/Frontend/web-workers/scripts/get-autocomplete-signals.ts
@@ -5,9 +5,9 @@
 import { compact, orderBy } from 'lodash';
 
 import {
-  AutocompleteSignal,
   ExternalAttributionSources,
   PackageInfo,
+  SignalWithCount,
 } from '../../../shared/shared-types';
 import {
   getContainedExternalPackages,
@@ -57,8 +57,8 @@ export function getAutocompleteSignals({
     ...signalsOnResource,
     ...signalsOnChildren,
     ...attributionsOnChildren,
-  ].reduce<Array<AutocompleteSignal>>((acc, { comment, ...signal }) => {
-    if (!generatePurl(signal) || signal.preferred) {
+  ].reduce<Array<SignalWithCount>>((acc, signal) => {
+    if (!generatePurl(signal)) {
       return acc;
     }
 
@@ -67,16 +67,16 @@ export function getAutocompleteSignals({
 
     if (dupeIndex === -1) {
       acc.push({
-        attributionIds: [],
         count: 1,
-        comments: comment ? [comment] : undefined,
         ...signal,
       });
     } else {
       acc[dupeIndex] = {
         ...acc[dupeIndex],
         count: (acc[dupeIndex].count ?? 0) + 1,
+        preferred: acc[dupeIndex].preferred || signal.preferred,
         wasPreferred: acc[dupeIndex].wasPreferred || signal.wasPreferred,
+        preSelected: acc[dupeIndex].preSelected || signal.preSelected,
       };
     }
 
@@ -87,9 +87,10 @@ export function getAutocompleteSignals({
     signals,
     [
       ({ source }) => (source && sources[source.name])?.priority ?? 0,
+      ({ preferred }) => (preferred ? 1 : 0),
       ({ wasPreferred }) => (wasPreferred ? 1 : 0),
       ({ count }) => count ?? 0,
     ],
-    ['desc', 'desc', 'desc'],
+    ['desc', 'desc', 'desc', 'desc'],
   );
 }
diff --git a/src/Frontend/web-workers/signals-worker.ts b/src/Frontend/web-workers/signals-worker.ts
index 1faeeb7d..fd3eea4d 100644
--- a/src/Frontend/web-workers/signals-worker.ts
+++ b/src/Frontend/web-workers/signals-worker.ts
@@ -4,9 +4,9 @@
 // SPDX-License-Identifier: Apache-2.0
 import {
   AttributionsToHashes,
-  AutocompleteSignal,
   ExternalAttributionSources,
   Resources,
+  SignalWithCount,
 } from '../../shared/shared-types';
 import { PanelData, ProgressBarData } from '../types/types';
 import { PanelAttributionData } from '../util/get-contained-packages';
@@ -32,7 +32,7 @@ export type SignalsWorkerInput =
 export type SignalsWorkerOutput =
   | {
       name: 'autocompleteSignals';
-      data: Array<AutocompleteSignal>;
+      data: Array<SignalWithCount>;
     }
   | {
       name: 'attributionsInFolderContent';
diff --git a/src/Frontend/web-workers/use-signals-worker.ts b/src/Frontend/web-workers/use-signals-worker.ts
index ff37e549..d0a8e9c7 100644
--- a/src/Frontend/web-workers/use-signals-worker.ts
+++ b/src/Frontend/web-workers/use-signals-worker.ts
@@ -4,7 +4,7 @@
 // SPDX-License-Identifier: Apache-2.0
 import { useEffect, useState } from 'react';
 
-import { AutocompleteSignal } from '../../shared/shared-types';
+import { SignalWithCount } from '../../shared/shared-types';
 import { useAppSelector } from '../state/hooks';
 import {
   getAttributionBreakpoints,
@@ -50,7 +50,7 @@ const initialWorkerPanelData: WorkerPanelData = {
 };
 
 export function useAutocompleteSignals() {
-  const [autocompleteSignals] = useVariable<Array<AutocompleteSignal>>(
+  const [autocompleteSignals] = useVariable<Array<SignalWithCount>>(
     WORKER_REDUX_KEYS.AUTOCOMPLETE_SIGNALS,
     [],
   );
@@ -101,7 +101,7 @@ export function useSignalsWorker() {
   const { projectId } = useAppSelector(getProjectMetadata);
 
   const [worker, setWorker] = useState<Worker>();
-  const [, setAutocompleteSignals] = useVariable<Array<AutocompleteSignal>>(
+  const [, setAutocompleteSignals] = useVariable<Array<SignalWithCount>>(
     WORKER_REDUX_KEYS.AUTOCOMPLETE_SIGNALS,
     [],
   );
diff --git a/src/shared/Faker.ts b/src/shared/Faker.ts
index 11fff13b..e780e672 100644
--- a/src/shared/Faker.ts
+++ b/src/shared/Faker.ts
@@ -11,30 +11,6 @@ import type {
   ParsedOpossumOutputFile,
   RawFrequentLicense,
 } from '../ElectronBackend/types/types';
-import { HttpClient } from '../Frontend/util/http-client';
-import {
-  AdvisorySuggestion,
-  DefaultVersionResponse,
-  GitHubLicenseResponse,
-  GitLabLicenseResponse,
-  GitLabProjectResponse,
-  LatestReleaseResponse,
-  Links,
-  PackageSuggestion,
-  PackageSystem,
-  packageSystems,
-  ProjectSuggestion,
-  ProjectType,
-  projectTypes,
-  SearchSuggestion,
-  SearchSuggestionResponse,
-  TagResponse,
-  VersionKey,
-  VersionResponse,
-  VersionsResponse,
-  WebVersionResponse,
-} from '../Frontend/util/package-search-api';
-import { PackageSearchHooks } from '../Frontend/util/package-search-hooks';
 import {
   AttributionData,
   AttributionsToResources,
@@ -105,10 +81,6 @@ class OpossumModule {
     };
   }
 
-  public static copyright(name = faker.company.name()) {
-    return `Copyright (c) ${name}`;
-  }
-
   public static manualPackageInfo(
     props: Partial<ManualPackageInfo> = {},
   ): ManualPackageInfo {
@@ -117,7 +89,7 @@ class OpossumModule {
         min: DiscreteConfidence.Low + 1,
         max: DiscreteConfidence.High - 1,
       }),
-      copyright: OpossumModule.copyright(),
+      copyright: faker.lorem.sentences(),
       licenseName: faker.commerce.productName(),
       packageName: faker.internet.domainWord(),
       packageVersion: faker.system.semver(),
@@ -320,214 +292,8 @@ class OpossumModule {
   }
 }
 
-class PackageSearchModule {
-  public static usePackageNames() {
-    jest.spyOn(PackageSearchHooks, 'usePackageNames').mockReturnValue({
-      packageNames: [],
-      packageNamesError: null,
-      packageNamesLoading: false,
-    });
-  }
-
-  public static usePackageNamespaces() {
-    jest.spyOn(PackageSearchHooks, 'usePackageNamespaces').mockReturnValue({
-      packageNamespaces: [],
-      packageNamespacesError: null,
-      packageNamespacesLoading: false,
-    });
-  }
-
-  public static usePackageVersions() {
-    jest.spyOn(PackageSearchHooks, 'usePackageVersions').mockReturnValue({
-      packageVersions: [],
-      packageVersionsError: null,
-      packageVersionsLoading: false,
-    });
-  }
-
-  public static packageSystem(): PackageSystem {
-    return faker.helpers.arrayElement(packageSystems);
-  }
-
-  public static projectType(): ProjectType {
-    return faker.helpers.arrayElement(projectTypes);
-  }
-
-  public static versionKey(props: Partial<VersionKey> = {}): VersionKey {
-    return {
-      name: faker.commerce.productName(),
-      system: 'NPM',
-      version: faker.system.semver(),
-      ...props,
-    };
-  }
-
-  public static versionResponse(
-    props: Partial<VersionResponse> = {},
-  ): VersionResponse {
-    return {
-      versionKey: PackageSearchModule.versionKey(),
-      publishedAt: faker.date.past().toISOString(),
-      isDefault: faker.datatype.boolean(),
-      ...props,
-    };
-  }
-
-  public static defaultVersionResponse(
-    props: Partial<DefaultVersionResponse> = {},
-  ): DefaultVersionResponse {
-    return {
-      defaultVersion: faker.system.semver(),
-      ...props,
-    };
-  }
-
-  public static versionsResponse(
-    props: Partial<VersionsResponse> = {},
-  ): VersionsResponse {
-    return {
-      versions: faker.helpers.multiple(PackageSearchModule.versionResponse),
-      ...props,
-    };
-  }
-
-  public static licenseTextWithCopyright(
-    copyright = OpossumModule.copyright(),
-  ) {
-    return btoa(
-      `${faker.lorem.sentence()}\n${copyright}\n${faker.lorem.sentence()}`,
-    );
-  }
-
-  public static gitHubLicenseResponse(
-    props: Partial<GitHubLicenseResponse> = {},
-  ): GitHubLicenseResponse {
-    return {
-      content: PackageSearchModule.licenseTextWithCopyright(),
-      license: { name: faker.commerce.productName() },
-      ...props,
-    };
-  }
-
-  public static gitLabProjectResponse(
-    props: Partial<GitLabProjectResponse> = {},
-  ): GitLabProjectResponse {
-    return {
-      license: { name: faker.commerce.productName() },
-      license_url: faker.internet.url(),
-      ...props,
-    };
-  }
-
-  public static gitLabLicenseResponse(
-    props: Partial<GitLabLicenseResponse> = {},
-  ): GitLabLicenseResponse {
-    return {
-      content: PackageSearchModule.licenseTextWithCopyright(),
-      ...props,
-    };
-  }
-
-  public static packageSuggestion(
-    props: Partial<PackageSuggestion> = {},
-  ): PackageSuggestion {
-    return {
-      kind: 'PACKAGE',
-      name: faker.internet.domainWord(),
-      system: PackageSearchModule.packageSystem(),
-      ...props,
-    };
-  }
-
-  public static projectSuggestion(
-    props: Partial<ProjectSuggestion> = {},
-  ): ProjectSuggestion {
-    return {
-      kind: 'PROJECT',
-      name: `${faker.internet.domainWord()}/${faker.internet.domainWord()}`,
-      projectType: PackageSearchModule.projectType(),
-      ...props,
-    };
-  }
-
-  public static advisorySuggestion(
-    props: Partial<AdvisorySuggestion> = {},
-  ): AdvisorySuggestion {
-    return {
-      kind: 'ADVISORY',
-      name: faker.internet.domainWord(),
-      ...props,
-    };
-  }
-
-  public static searchSuggestion(): SearchSuggestion {
-    return faker.helpers.arrayElement([
-      PackageSearchModule.packageSuggestion,
-      PackageSearchModule.projectSuggestion,
-    ])();
-  }
-
-  public static searchSuggestionResponse(
-    props: Partial<SearchSuggestionResponse> = {},
-  ): SearchSuggestionResponse {
-    return {
-      results: faker.helpers.multiple(PackageSearchModule.searchSuggestion),
-      ...props,
-    };
-  }
-
-  public static links(props: Partial<Links> = {}): Links {
-    return {
-      origins: faker.helpers.multiple(faker.internet.url),
-      homepage: faker.internet.url(),
-      repo: faker.internet.url(),
-      issues: faker.internet.url(),
-      ...props,
-    };
-  }
-
-  public static webVersionResponse(
-    props: Partial<WebVersionResponse> = {},
-  ): WebVersionResponse {
-    return {
-      version: {
-        licenses: faker.helpers.multiple(faker.commerce.productName),
-        links: PackageSearchModule.links(),
-      },
-      ...props,
-    };
-  }
-
-  public static tagResponse(props: Partial<TagResponse> = {}): TagResponse {
-    return {
-      name: faker.system.semver(),
-      ...props,
-    };
-  }
-
-  public static latestReleaseResponse(
-    props: Partial<LatestReleaseResponse> = {},
-  ): LatestReleaseResponse {
-    return {
-      tag_name: faker.system.semver(),
-      ...props,
-    };
-  }
-}
-
 class Faker extends NativeFaker {
   public readonly opossum = OpossumModule;
-  public readonly packageSearch = PackageSearchModule;
-
-  public httpClient(...body: Array<object>): HttpClient {
-    const request = jest.fn();
-
-    body.forEach((item) =>
-      request.mockResolvedValueOnce(new Response(JSON.stringify(item))),
-    );
-
-    return { request } satisfies Partial<HttpClient> as unknown as HttpClient;
-  }
 }
 
 export const faker = new Faker({ locale: [en, base] });
diff --git a/src/shared/shared-types.ts b/src/shared/shared-types.ts
index 92a56bdf..d9f6cc34 100644
--- a/src/shared/shared-types.ts
+++ b/src/shared/shared-types.ts
@@ -278,8 +278,6 @@ export interface UserSettings {
   qaMode: boolean;
 }
 
-export type AutocompleteSignal = DisplayPackageInfo & {
+export type SignalWithCount = PackageInfo & {
   count?: number;
-  suffix?: string;
-  default?: boolean;
 };
diff --git a/src/shared/text.ts b/src/shared/text.ts
index 56df1ac8..699c2ee0 100644
--- a/src/shared/text.ts
+++ b/src/shared/text.ts
@@ -8,6 +8,10 @@ export const text = {
     packageSubPanel: {
       confidence: 'Confidence',
       copyToClipboard: 'Copy to clipboard',
+      fetchPackageInfo: 'Fetch package information',
+      fetchPackageInfoNotPossible:
+        'Fetching package information is not possible. Please enter a valid URL with one of the following domains: pypi.org, npmjs.com, github.com.',
+      fetching: 'Fetching...',
       noLinkToOpen: 'No link to open. Please enter a URL.',
       openLinkInBrowser: 'Open repository URL in browser',
       packageName: 'Package Name',
@@ -22,7 +26,6 @@ export const text = {
     amongSignals: 'among signals',
     commonEcosystems: 'Common Ecosystems',
     commonLicenses: 'Common Licenses',
-    openSourceInsights: 'Open Source Insights',
     legalInformation: 'Legal Information',
     licenseName: 'License Name',
     licenseTextModified: '(License text modified)',
@@ -32,21 +35,12 @@ export const text = {
     useAutocompleteSuggestion:
       'Adopt all coordinates and legal information from suggestion',
     originallyFrom: 'Originally from ',
-    invalidPurl: 'INVALID PURL',
-    description: 'Description',
-    homepage: 'Homepage',
-    origin: 'Origin',
   },
   changePreferredStatusGloballyPopup: {
     markAsPreferred: 'Do you really want to prefer the attribution globally?',
     unmarkAsPreferred:
       'Do you really want to remove the preferred status globally?',
   },
-  modifyWasPreferredPopup: {
-    header: 'Warning',
-    message:
-      'You are about to modify an attribution that was preferred in the past. Are you sure you want to continue? The attribution will no longer be marked with a',
-  },
   processPopup: {
     title: 'Processing…',
   },
diff --git a/yarn.lock b/yarn.lock
index 660f95fa..b15f7885 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2964,17 +2964,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@tanstack/eslint-plugin-query@npm:^5.12.1":
-  version: 5.12.1
-  resolution: "@tanstack/eslint-plugin-query@npm:5.12.1"
-  dependencies:
-    "@typescript-eslint/utils": "npm:^5.54.0"
-  peerDependencies:
-    eslint: ^8.0.0
-  checksum: 91b5c61e7154a4c51dc8d2863f18708a33aa98e3b8f46b792db3d864f5e169552b201b3ffbed3fbc2bfedb3abde6830601fc004293eb8c0a2258956ad2727d2f
-  languageName: node
-  linkType: hard
-
 "@tanstack/query-core@npm:5.14.2":
   version: 5.14.2
   resolution: "@tanstack/query-core@npm:5.14.2"
@@ -3709,7 +3698,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@typescript-eslint/utils@npm:^5.10.0, @typescript-eslint/utils@npm:^5.54.0, @typescript-eslint/utils@npm:^5.58.0":
+"@typescript-eslint/utils@npm:^5.10.0, @typescript-eslint/utils@npm:^5.58.0":
   version: 5.62.0
   resolution: "@typescript-eslint/utils@npm:5.62.0"
   dependencies:
@@ -4855,13 +4844,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"compare-versions@npm:^6.1.0":
-  version: 6.1.0
-  resolution: "compare-versions@npm:6.1.0"
-  checksum: 20f349e7f8ad784704c68265f4e660e2abbe2c3d5c75793184fccb85f0c5c0263260e01fdd4488376f6b74b0f069e16c9684463f7316b075716fb1581eb36b77
-  languageName: node
-  linkType: hard
-
 "concat-map@npm:0.0.1":
   version: 0.0.1
   resolution: "concat-map@npm:0.0.1"
@@ -9236,7 +9218,6 @@ __metadata:
     "@mui/system": "npm:^5.15.1"
     "@playwright/test": "npm:^1.40.1"
     "@reduxjs/toolkit": "npm:^2.0.1"
-    "@tanstack/eslint-plugin-query": "npm:^5.12.1"
     "@tanstack/react-query": "npm:^5.14.2"
     "@testing-library/dom": "npm:^9.3.3"
     "@testing-library/jest-dom": "npm:^6.1.5"
@@ -9263,7 +9244,6 @@ __metadata:
     babel-jest: "npm:^29.7.0"
     babel-plugin-transform-import-meta: "npm:^2.2.1"
     buffer: "npm:^6.0.3"
-    compare-versions: "npm:^6.1.0"
     cross-env: "npm:^7.0.3"
     dayjs: "npm:^1.11.10"
     dotenv: "npm:^16.3.1"
@@ -9301,6 +9281,7 @@ __metadata:
     packageurl-js: "npm:^1.2.1"
     prettier: "npm:^3.1.1"
     prettier-plugin-sh: "npm:^0.13.1"
+    prop-types: "npm:^15.8.1"
     proxy-memoize: "npm:^2.0.4"
     re-resizable: "npm:^6.9.11"
     react: "npm:^18.2.0"
@@ -9321,7 +9302,6 @@ __metadata:
     vite-plugin-electron: "npm:^0.15.5"
     vite-plugin-svgr: "npm:^4.2.0"
     vite-tsconfig-paths: "npm:^4.2.2"
-    whatwg-fetch: "npm:^3.6.19"
   languageName: unknown
   linkType: soft
 
@@ -11788,13 +11768,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"whatwg-fetch@npm:^3.6.19":
-  version: 3.6.19
-  resolution: "whatwg-fetch@npm:3.6.19"
-  checksum: 257b130a06bc0fca4e3f15cb4a7b7822d12b7493c6743353e3a107b62ef2716f77fae35b4c81b4b8630e221aca30ea5b9770969db762d63336108f57bee9f963
-  languageName: node
-  linkType: hard
-
 "whatwg-mimetype@npm:^3.0.0":
   version: 3.0.0
   resolution: "whatwg-mimetype@npm:3.0.0"


Test patch:
diff --git a/.github/workflows/build-and-e2e-test.yml b/.github/workflows/build-and-e2e-test.yml
index 5a775d01..099a30bc 100644
--- a/.github/workflows/build-and-e2e-test.yml
+++ b/.github/workflows/build-and-e2e-test.yml
@@ -44,29 +44,7 @@ jobs:
 
       - run: yarn ship:auto
 
-      - name: Create SPDX document
-        run: yarn spdx
-
-      - name: Verify name in SPDX document on MacOS and Ubuntu
-        if: matrix.os == 'macos-latest' || matrix.os == 'ubuntu-latest'
-        run: |
-          set -e
-          name=$(jq .name opossum-ui.spdx.json -r)
-          echo "name=$name"
-          if [[ "$name" != "opossum-ui" ]]; then
-            echo "Name in SPDX document is not opossum-ui"
-            exit 1
-          fi
-
-      - name: Verify name in SPDX document on Windows
-        if: matrix.os == 'windows-latest'
-        run: |
-          $name=jq .name opossum-ui.spdx.json -r
-          echo "name=$name"
-          if ("$name" -ne "opossum-ui") {
-            echo "Name in SPDX document is not opossum-ui"
-            exit 1
-          }
+      - run: yarn spdx
 
       - run: ${{ matrix.E2E }}
 
diff --git a/src/ElectronBackend/input/__tests__/importFromFile.test.ts b/src/ElectronBackend/input/__tests__/importFromFile.test.ts
index 9c8ae111..2fa24dc3 100644
--- a/src/ElectronBackend/input/__tests__/importFromFile.test.ts
+++ b/src/ElectronBackend/input/__tests__/importFromFile.test.ts
@@ -192,7 +192,7 @@ describe('Test of loading function', () => {
     const jsonPath = path.join(upath.toUnix(temporaryPath), 'test.json');
     await writeFile({ path: jsonPath, content: inputFileContent });
 
-    jest.spyOn(Date, 'now').mockReturnValue(1);
+    Date.now = jest.fn(() => 1);
 
     (dialog.showMessageBox as jest.Mock).mockImplementationOnce(
       jest.fn(() => {
@@ -242,7 +242,7 @@ describe('Test of loading function', () => {
       path: opossumPath,
     });
 
-    jest.spyOn(Date, 'now').mockReturnValue(1);
+    Date.now = jest.fn(() => 1);
 
     const globalBackendState = {
       resourceFilePath: '/previous/file.opossum',
@@ -275,7 +275,7 @@ describe('Test of loading function', () => {
       path: opossumPath,
     });
 
-    jest.spyOn(Date, 'now').mockReturnValue(1691761892037);
+    Date.now = jest.fn(() => 1691761892037);
 
     setGlobalBackendState({});
     await loadInputAndOutputFromFilePath(mainWindow, opossumPath);
@@ -296,7 +296,7 @@ describe('Test of loading function', () => {
       const jsonPath = path.join(upath.toUnix(temporaryPath), 'test.json');
       await writeFile({ path: jsonPath, content: inputFileContent });
 
-      jest.spyOn(Date, 'now').mockReturnValue(1);
+      Date.now = jest.fn(() => 1);
 
       setGlobalBackendState({});
       await loadInputAndOutputFromFilePath(mainWindow, jsonPath);
@@ -319,7 +319,7 @@ describe('Test of loading function', () => {
         zlib.gzipSync(JSON.stringify(inputFileContent)),
       );
 
-      jest.spyOn(Date, 'now').mockReturnValue(1);
+      Date.now = jest.fn(() => 1);
 
       setGlobalBackendState({});
       await loadInputAndOutputFromFilePath(mainWindow, jsonPath);
@@ -357,7 +357,7 @@ describe('Test of loading function', () => {
     };
     await writeFile({ path: attributionJsonPath, content: attributions });
 
-    jest.spyOn(Date, 'now').mockReturnValue(1);
+    Date.now = jest.fn(() => 1);
 
     const globalBackendState = {
       resourceFilePath: '/previous/file.json',
@@ -438,7 +438,7 @@ describe('Test of loading function', () => {
         content: inputFileContentWithPreselectedAttribution,
       });
 
-      jest.spyOn(Date, 'now').mockReturnValue(1);
+      Date.now = jest.fn(() => 1);
 
       const globalBackendState = {
         resourceFilePath: '/previous/file.json',
@@ -547,7 +547,7 @@ describe('Test of loading function', () => {
       content: inputFileContentWithCustomMetadata,
     });
 
-    jest.spyOn(Date, 'now').mockReturnValue(1);
+    Date.now = jest.fn(() => 1);
 
     setGlobalBackendState({});
     await loadInputAndOutputFromFilePath(mainWindow, jsonPath);
@@ -589,7 +589,7 @@ describe('Test of loading function', () => {
 
     await writeFile({ path: jsonPath, content: inputFileContentWithOriginIds });
 
-    jest.spyOn(Date, 'now').mockReturnValue(1);
+    Date.now = jest.fn(() => 1);
 
     setGlobalBackendState({});
     await loadInputAndOutputFromFilePath(mainWindow, jsonPath);
diff --git a/src/ElectronBackend/main/__tests__/createWindowDev.test.ts b/src/ElectronBackend/main/__tests__/createWindowDev.test.ts
index e7a3d547..e3c66dab 100644
--- a/src/ElectronBackend/main/__tests__/createWindowDev.test.ts
+++ b/src/ElectronBackend/main/__tests__/createWindowDev.test.ts
@@ -37,6 +37,8 @@ jest.mock('electron', () => ({
 jest.mock('electron-is-dev', () => true);
 
 describe('createWindow', () => {
+  beforeEach(() => jest.clearAllMocks());
+
   it('returns correct BrowserWindow in devMode', async () => {
     const browserWindow = await createWindow();
     expect(browserWindow.webContents.openDevTools).toHaveBeenCalled();
diff --git a/src/ElectronBackend/main/__tests__/createWindowProd.test.ts b/src/ElectronBackend/main/__tests__/createWindowProd.test.ts
index e48abf4a..9eb5e8d6 100644
--- a/src/ElectronBackend/main/__tests__/createWindowProd.test.ts
+++ b/src/ElectronBackend/main/__tests__/createWindowProd.test.ts
@@ -45,6 +45,8 @@ jest.mock('../iconHelpers', () => ({
 }));
 
 describe('createWindow', () => {
+  beforeEach(() => jest.clearAllMocks());
+
   it('returns correct BrowserWindow in production', async () => {
     const browserWindow = await createWindow();
     expect(browserWindow.webContents.openDevTools).not.toHaveBeenCalled();
diff --git a/src/ElectronBackend/main/__tests__/listeners.test.ts b/src/ElectronBackend/main/__tests__/listeners.test.ts
index 6afe65cf..ff5973b3 100644
--- a/src/ElectronBackend/main/__tests__/listeners.test.ts
+++ b/src/ElectronBackend/main/__tests__/listeners.test.ts
@@ -566,6 +566,10 @@ describe('getSelectBaseURLListener', () => {
 });
 
 describe('getSaveFileListener', () => {
+  beforeEach(() => {
+    (writeFile as jest.Mock).mockReset();
+  });
+
   it('throws error when projectId is not set', async () => {
     const mockCallback = jest.fn();
     const mainWindow = {
@@ -826,6 +830,10 @@ describe('getExportBomListener', () => {
 });
 
 describe('getExportSpdxDocumentListener', () => {
+  beforeEach(() => {
+    (writeSpdxFile as jest.Mock).mockReset();
+  });
+
   it('throws if path is not set', async () => {
     const mainWindow = await prepareBomSPdxAndFollowUpit();
 
@@ -889,6 +897,10 @@ describe('getOpenLinkListener', () => {
 });
 
 describe('_exportFileAndOpenFolder', () => {
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
   it('calls the createFile function', async () => {
     const mainWindow = await prepareBomSPdxAndFollowUpit();
     const testSpdxDocumentYamlFilePath = '/some/path.json';
diff --git a/src/ElectronBackend/main/__tests__/openFileFromCliOrEnvVariableIfProvided.test.ts b/src/ElectronBackend/main/__tests__/openFileFromCliOrEnvVariableIfProvided.test.ts
index b87b32ca..f0f0dfd0 100644
--- a/src/ElectronBackend/main/__tests__/openFileFromCliOrEnvVariableIfProvided.test.ts
+++ b/src/ElectronBackend/main/__tests__/openFileFromCliOrEnvVariableIfProvided.test.ts
@@ -23,6 +23,8 @@ describe('openFileFromCli', () => {
     process.argv = oldProcessArgv;
   });
 
+  beforeEach(() => jest.clearAllMocks());
+
   it.each`
     inputFileName          | extraParameter
     ${'inputFile.json'}    | ${null}
@@ -78,6 +80,7 @@ describe('openFileFromEnvVariable', () => {
   const oldProcessArgv = process.argv;
 
   beforeEach(() => {
+    jest.resetModules();
     process.env = cloneDeep(oldEnvVariables);
     process.argv = cloneDeep(oldProcessArgv);
   });
diff --git a/src/Frontend/Components/AttributionList/__tests__/AttributionList.test.tsx b/src/Frontend/Components/AttributionList/__tests__/AttributionList.test.tsx
index 5c1de6b7..20d4ad72 100644
--- a/src/Frontend/Components/AttributionList/__tests__/AttributionList.test.tsx
+++ b/src/Frontend/Components/AttributionList/__tests__/AttributionList.test.tsx
@@ -53,6 +53,9 @@ describe('The AttributionList', () => {
   const mockCallback = jest.fn((attributionId: string) => {
     return attributionId;
   });
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
 
   it('renders', () => {
     renderComponent(
diff --git a/src/Frontend/Components/ConfirmationDialog/__test__/ConfirmationDialog.test.tsx b/src/Frontend/Components/ConfirmationDialog/__test__/ConfirmationDialog.test.tsx
deleted file mode 100644
index c181dbf9..00000000
--- a/src/Frontend/Components/ConfirmationDialog/__test__/ConfirmationDialog.test.tsx
+++ /dev/null
@@ -1,108 +0,0 @@
-// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-import { screen, waitFor } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-
-import { faker } from '../../../../shared/Faker';
-import { ButtonText } from '../../../enums/enums';
-import { renderComponent } from '../../../test-helpers/render';
-import {
-  ConfirmationDialog,
-  ConfirmOptions,
-  useConfirmationDialog,
-} from '../ConfirmationDialog';
-
-const TestComponent: React.FC<
-  ConfirmOptions & {
-    onConfirm?: () => unknown | Promise<unknown>;
-    title?: string;
-    message?: string;
-    buttonTitle?: string;
-  }
-> = ({
-  onCancel,
-  onConfirm = jest.fn(),
-  skip,
-  buttonTitle = faker.string.sample(),
-  message = faker.lorem.paragraph(),
-  title = faker.lorem.sentence(),
-}) => {
-  const [ref, confirm] = useConfirmationDialog();
-
-  return (
-    <>
-      <button onClick={() => confirm(onConfirm, { onCancel, skip })}>
-        {buttonTitle}
-      </button>
-      <ConfirmationDialog ref={ref} title={title} message={message} />
-    </>
-  );
-};
-
-describe('ConfirmationDialog', () => {
-  it('shows confirmation dialog when confirmation is required', async () => {
-    renderComponent(<TestComponent />);
-
-    await userEvent.click(screen.getByRole('button'));
-
-    expect(screen.getByLabelText('confirmation dialog')).toBeInTheDocument();
-  });
-
-  it('does not show confirmation dialog and calls confirm handler immediately when no confirmation is required', async () => {
-    const onConfirm = jest.fn();
-    const onCancel = jest.fn();
-    renderComponent(
-      <TestComponent skip={true} onConfirm={onConfirm} onCancel={onCancel} />,
-    );
-
-    await userEvent.click(screen.getByRole('button'));
-
-    expect(onConfirm).toHaveBeenCalledTimes(1);
-    expect(onCancel).not.toHaveBeenCalled();
-    expect(
-      screen.queryByLabelText('confirmation dialog'),
-    ).not.toBeInTheDocument();
-  });
-
-  it('closes confirmation dialog and calls confirm handler when user confirms confirmation dialog', async () => {
-    const onConfirm = jest.fn();
-    const onCancel = jest.fn();
-    renderComponent(
-      <TestComponent onConfirm={onConfirm} onCancel={onCancel} />,
-    );
-
-    await userEvent.click(screen.getByRole('button'));
-    await userEvent.click(screen.getByRole('button', { name: ButtonText.Ok }));
-
-    expect(onConfirm).toHaveBeenCalledTimes(1);
-    await waitFor(() =>
-      expect(
-        screen.queryByLabelText('confirmation dialog'),
-      ).not.toBeInTheDocument(),
-    );
-    expect(onCancel).not.toHaveBeenCalled();
-  });
-
-  it('closes confirmation dialog and calls cancel handler when user cancels out of confirmation dialog', async () => {
-    const onConfirm = jest.fn();
-    const onCancel = jest.fn();
-    renderComponent(
-      <TestComponent onConfirm={onConfirm} onCancel={onCancel} />,
-    );
-
-    await userEvent.click(screen.getByRole('button'));
-    await userEvent.click(
-      screen.getByRole('button', { name: ButtonText.Cancel }),
-    );
-
-    expect(onCancel).toHaveBeenCalledTimes(1);
-    await waitFor(() =>
-      expect(
-        screen.queryByLabelText('confirmation dialog'),
-      ).not.toBeInTheDocument(),
-    );
-    expect(onConfirm).not.toHaveBeenCalled();
-  });
-});
diff --git a/src/Frontend/Components/FetchLicenseInformationButton/__tests__/FetchLicenseInformationButton.test.tsx b/src/Frontend/Components/FetchLicenseInformationButton/__tests__/FetchLicenseInformationButton.test.tsx
new file mode 100644
index 00000000..51772935
--- /dev/null
+++ b/src/Frontend/Components/FetchLicenseInformationButton/__tests__/FetchLicenseInformationButton.test.tsx
@@ -0,0 +1,196 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import {
+  act,
+  fireEvent,
+  render,
+  screen,
+  waitFor,
+} from '@testing-library/react';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+
+import { PackageInfo } from '../../../../shared/shared-types';
+import { text } from '../../../../shared/text';
+import { getTemporaryDisplayPackageInfo } from '../../../state/selectors/all-views-resource-selectors';
+import { renderComponent, renderHook } from '../../../test-helpers/render';
+import {
+  FetchLicenseInformationButton,
+  FetchStatus,
+  useFetchPackageInfo,
+} from '../FetchLicenseInformationButton';
+import {
+  convertGithubPayload,
+  getGithubAPIUrl,
+} from '../github-fetching-helpers';
+import { getPypiAPIUrl } from '../pypi-fetching-helpers';
+
+jest.useFakeTimers();
+
+const axiosMock = new MockAdapter(axios);
+
+describe('FetchLicenseInformationButton', () => {
+  it('renders no button when disabled', () => {
+    const { container } = render(
+      <FetchLicenseInformationButton
+        disabled={true}
+        url={'https://github.com/reactchartjs/react-chartjs-2/tree/d8c'}
+      />,
+    );
+    expect(container).toBeEmptyDOMElement();
+  });
+
+  it('renders tooltip vor invalid domain', () => {
+    render(
+      <FetchLicenseInformationButton disabled={false} url={'invalid url'} />,
+    );
+    expect(
+      screen.getByLabelText(
+        text.attributionColumn.packageSubPanel.fetchPackageInfoNotPossible,
+      ),
+    ).toBeInTheDocument();
+  });
+
+  it('renders enabled button', () => {
+    renderComponent(
+      <FetchLicenseInformationButton
+        url={'https://pypi.org/pypi/pip'}
+        disabled={false}
+      />,
+    );
+    expect(screen.getByRole('button')).toBeInTheDocument();
+  });
+
+  describe('Tooltip', () => {
+    it("shows fetching error tooltip 'Network error'", async () => {
+      const MOCK_URL = 'https://pypi.org/pypi/test';
+
+      axiosMock.onGet(getPypiAPIUrl(MOCK_URL)).networkErrorOnce();
+
+      renderComponent(
+        <FetchLicenseInformationButton url={MOCK_URL} disabled={false} />,
+      );
+
+      fireEvent.click(screen.getByRole('button'));
+      fireEvent.mouseOver(screen.getByRole('button'));
+      jest.runAllTimers();
+
+      expect(await screen.findByText('Network Error')).toBeInTheDocument();
+    });
+
+    it("shows fetching error tooltip 'Request failed with status code 404'", async () => {
+      const MOCK_URL = 'https://github.com/opossum-tool/Oposs';
+      const notFoundStatus = 404;
+
+      axiosMock.onGet(getGithubAPIUrl(MOCK_URL)).replyOnce(notFoundStatus);
+
+      renderComponent(
+        <FetchLicenseInformationButton url={MOCK_URL} disabled={false} />,
+      );
+
+      fireEvent.click(screen.getByRole('button'));
+      fireEvent.mouseOver(screen.getByRole('button'));
+      jest.runAllTimers();
+
+      expect(
+        await screen.findByText('Request failed with status code 404'),
+      ).toBeInTheDocument();
+    });
+
+    it('shows fetch data tooltip after successful fetch', async () => {
+      const MOCK_URL = 'https://pypi.org/project/pip';
+      const okStatus = 200;
+
+      axiosMock.onGet(getPypiAPIUrl(MOCK_URL)).replyOnce(okStatus, {
+        info: {
+          license: 'Apache-2.0',
+          name: 'react',
+        },
+      });
+
+      renderComponent(
+        <FetchLicenseInformationButton url={MOCK_URL} disabled={false} />,
+      );
+
+      fireEvent.click(screen.getByRole('button'));
+      fireEvent.mouseOver(screen.getByRole('button'));
+      jest.runAllTimers();
+
+      expect(
+        await screen.findByText(
+          text.attributionColumn.packageSubPanel.fetchPackageInfo,
+        ),
+      ).toBeInTheDocument();
+    });
+  });
+});
+
+function mockConvertPayload(_: Response): PackageInfo {
+  return { licenseName: 'testLicense' };
+}
+
+function mockConvertPayloadRaises(_: Response): PackageInfo {
+  throw new Error('unexpected error');
+}
+
+const MOCK_URL = 'mock_url';
+
+const GITHUB_URL = 'https://github.com/opossum-tool/OpossumUI';
+
+describe('useFetchPackageInfo', () => {
+  it('returns idle', () => {
+    const { result } = renderHook(() =>
+      useFetchPackageInfo({
+        url: MOCK_URL,
+        convertPayload: mockConvertPayload,
+      }),
+    );
+    expect(result.current.fetchStatus).toBe(FetchStatus.Idle);
+  });
+
+  it('fetches data', async () => {
+    const okStatus = 200;
+    axiosMock.onGet(GITHUB_URL).replyOnce(okStatus, {
+      license: { spdx_id: 'Apache-2.0' },
+      content: 'TGljZW5zZSBUZXh0', // "License Text" in base64
+      html_url: 'https://github.com/opossum-tool/OpossumUI/blob/main/LICENSE',
+    });
+
+    const licenseFetchingInformation = {
+      url: GITHUB_URL,
+      convertPayload: convertGithubPayload,
+    };
+
+    const { result, store } = renderHook(() =>
+      useFetchPackageInfo(licenseFetchingInformation),
+    );
+    act(() => {
+      result.current.fetchData();
+    });
+
+    await waitFor(() => {
+      expect(result.current.fetchStatus).toBe(FetchStatus.Success);
+    });
+    expect(getTemporaryDisplayPackageInfo(store.getState())).toMatchObject({
+      licenseName: 'Apache-2.0',
+    });
+  });
+
+  it('handles errors', async () => {
+    const { result, store } = renderHook(() =>
+      useFetchPackageInfo({
+        url: MOCK_URL,
+        convertPayload: mockConvertPayloadRaises,
+      }),
+    );
+    act(() => {
+      result.current.fetchData();
+    });
+    await waitFor(() => {
+      expect(result.current.fetchStatus).toBe(FetchStatus.Error);
+    });
+    expect(getTemporaryDisplayPackageInfo(store.getState())).toMatchObject({});
+  });
+});
diff --git a/src/Frontend/Components/FetchLicenseInformationButton/__tests__/github-fetching-helpers.test.ts b/src/Frontend/Components/FetchLicenseInformationButton/__tests__/github-fetching-helpers.test.ts
new file mode 100644
index 00000000..de21913c
--- /dev/null
+++ b/src/Frontend/Components/FetchLicenseInformationButton/__tests__/github-fetching-helpers.test.ts
@@ -0,0 +1,93 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import {
+  convertGithubPayload,
+  getGithubAPIUrl,
+} from '../github-fetching-helpers';
+
+describe('getGithubAPIUrl', () => {
+  it('handles a normal github url', () => {
+    expect(getGithubAPIUrl('https://github.com/opossum-tool/OpossumUI')).toBe(
+      'https://api.github.com/repos/opossum-tool/OpossumUI/license',
+    );
+  });
+
+  it('handles a trailing slash', () => {
+    expect(getGithubAPIUrl('https://github.com/opossum-tool/OpossumUI/')).toBe(
+      'https://api.github.com/repos/opossum-tool/OpossumUI/license',
+    );
+  });
+
+  it('handles additional parts at the end of the url', () => {
+    expect(
+      getGithubAPIUrl(
+        'https://github.com/opossum-tool/OpossumUI/some/more/suffixes',
+      ),
+    ).toBe('https://api.github.com/repos/opossum-tool/OpossumUI/license');
+  });
+});
+
+describe('convertGithubPayload', () => {
+  it('raises for invalid payload', () => {
+    const payload = { license: {} };
+
+    expect(() => convertGithubPayload(payload)).toThrow(
+      'requires property "spdx_id"',
+    );
+  });
+
+  it('parses payload correctly', () => {
+    const payload = {
+      license: { spdx_id: 'Apache-2.0' },
+      content: 'TGljZW5zZSBUZXh0', // "License Text" in base64
+      html_url: 'https://github.com/opossum-tool/OpossumUI/blob/main/LICENSE',
+    };
+
+    const packageInfo = convertGithubPayload(payload);
+    expect(packageInfo).toStrictEqual({
+      licenseName: 'Apache-2.0',
+      licenseText: 'License Text',
+      packageType: 'github',
+      packageNamespace: 'opossum-tool',
+      packageName: 'OpossumUI',
+      packagePURLAppendix: undefined,
+    });
+  });
+
+  it('handles non existing license text correctly', () => {
+    const payload = {
+      license: { spdx_id: 'Apache-2.0' },
+      html_url: 'https://github.com/opossum-tool/OpossumUI/blob/main/LICENSE',
+    };
+
+    const packageInfo = convertGithubPayload(payload);
+    expect(packageInfo).toStrictEqual({
+      licenseName: 'Apache-2.0',
+      packageType: 'github',
+      licenseText: undefined,
+      packageNamespace: 'opossum-tool',
+      packageName: 'OpossumUI',
+      packagePURLAppendix: undefined,
+    });
+  });
+
+  it('handles empty license text correctly', () => {
+    const payload = {
+      license: { spdx_id: 'Apache-2.0' },
+      content: '',
+      html_url: 'https://github.com/opossum-tool/OpossumUI/blob/main/LICENSE',
+    };
+
+    const packageInfo = convertGithubPayload(payload);
+    expect(packageInfo).toStrictEqual({
+      licenseName: 'Apache-2.0',
+      packageType: 'github',
+      licenseText: undefined,
+      packageNamespace: 'opossum-tool',
+      packageName: 'OpossumUI',
+      packagePURLAppendix: undefined,
+    });
+  });
+});
diff --git a/src/Frontend/Components/FetchLicenseInformationButton/__tests__/license-fetching-helpers.test.ts b/src/Frontend/Components/FetchLicenseInformationButton/__tests__/license-fetching-helpers.test.ts
new file mode 100644
index 00000000..89185d36
--- /dev/null
+++ b/src/Frontend/Components/FetchLicenseInformationButton/__tests__/license-fetching-helpers.test.ts
@@ -0,0 +1,81 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { convertGithubPayload } from '../github-fetching-helpers';
+import { getLicenseFetchingInformation } from '../license-fetching-helpers';
+import { convertNpmPayload } from '../npm-fetching-helpers';
+import { convertPypiPayload } from '../pypi-fetching-helpers';
+
+describe('getLicenseFetchingInformation', () => {
+  it('returns null for undefined as input', () => {
+    expect(getLicenseFetchingInformation()).toBeNull();
+  });
+
+  it('returns null for no match', () => {
+    expect(getLicenseFetchingInformation('https://unknown-url.com')).toBeNull();
+  });
+
+  it('recognizes pypi urls', () => {
+    expect(
+      getLicenseFetchingInformation('https://pypi.org/project/numpy'),
+    ).toMatchObject({
+      url: 'https://pypi.org/pypi/numpy/json',
+      convertPayload: convertPypiPayload,
+    });
+  });
+
+  it('recognizes npm urls', () => {
+    expect(
+      getLicenseFetchingInformation('https://npmjs.com/package/react'),
+    ).toMatchObject({
+      url: 'https://registry.npmjs.org/react',
+      convertPayload: convertNpmPayload,
+    });
+  });
+
+  it('recognizes npm urls starting with www', () => {
+    expect(
+      getLicenseFetchingInformation('https://www.npmjs.com/package/react'),
+    ).toMatchObject({
+      url: 'https://registry.npmjs.org/react',
+      convertPayload: convertNpmPayload,
+    });
+  });
+
+  it('recognizes npm urls with complicated package names', () => {
+    expect(
+      getLicenseFetchingInformation(
+        'https://npmjs.com/package/@angular/animations/',
+      ),
+    ).toMatchObject({
+      url: 'https://registry.npmjs.org/@angular/animations',
+      convertPayload: convertNpmPayload,
+    });
+  });
+
+  it('rejects npm urls with version in url', () => {
+    expect(
+      getLicenseFetchingInformation(
+        'https://npmjs.com/package/@angular/animations/1.0',
+      ),
+    ).toBeNull();
+  });
+
+  it('recognizes github urls consisting of namespace and name', () => {
+    expect(
+      getLicenseFetchingInformation(
+        'https://github.com/opossum-tool/OpossumUI',
+      ),
+    ).toMatchObject({
+      url: 'https://api.github.com/repos/opossum-tool/OpossumUI/license',
+      convertPayload: convertGithubPayload,
+    });
+  });
+
+  it('ignores github urls missing namespace or name', () => {
+    expect(
+      getLicenseFetchingInformation('https://github.com/opossum-tool/'),
+    ).toBeNull();
+  });
+});
diff --git a/src/Frontend/Components/FetchLicenseInformationButton/__tests__/npm-fetching-helpers.test.ts b/src/Frontend/Components/FetchLicenseInformationButton/__tests__/npm-fetching-helpers.test.ts
new file mode 100644
index 00000000..5f0a903c
--- /dev/null
+++ b/src/Frontend/Components/FetchLicenseInformationButton/__tests__/npm-fetching-helpers.test.ts
@@ -0,0 +1,66 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { convertNpmPayload, getNpmAPIUrl } from '../npm-fetching-helpers';
+
+describe('getNpmAPIUrl', () => {
+  it('handles a simple package name', () => {
+    expect(getNpmAPIUrl('https://npmjs.com/package/react')).toBe(
+      'https://registry.npmjs.org/react',
+    );
+  });
+
+  it('handles a simple package name without package in the url', () => {
+    expect(getNpmAPIUrl('https://npmjs.com/react')).toBe(
+      'https://registry.npmjs.org/react',
+    );
+  });
+
+  it('handles a simple package name with version', () => {
+    expect(getNpmAPIUrl('https://npmjs.com/package/react', '1.0')).toBe(
+      'https://registry.npmjs.org/react/1.0',
+    );
+  });
+
+  it('handles a simple package name with trailing slash', () => {
+    expect(getNpmAPIUrl('https://npmjs.com/package/react/', '1.0')).toBe(
+      'https://registry.npmjs.org/react/1.0',
+    );
+  });
+
+  it('handles a complicated package name', () => {
+    expect(getNpmAPIUrl('https://npmjs.com/package/@angular/animation')).toBe(
+      'https://registry.npmjs.org/@angular/animation',
+    );
+  });
+
+  it('handles a complicated package name with version', () => {
+    expect(
+      getNpmAPIUrl('https://npmjs.com/package/@angular/animation', '1.0'),
+    ).toBe('https://registry.npmjs.org/@angular/animation/1.0');
+  });
+});
+
+describe('convertNpmPayload', () => {
+  it('raises error for invalid payload', () => {
+    const payload = { name: 'test' };
+
+    expect(() => convertNpmPayload(payload)).toThrow(
+      'requires property "license"',
+    );
+  });
+
+  it('parses payload correctly', () => {
+    const payload = { name: 'test', license: 'MIT' };
+
+    const packageInfo = convertNpmPayload(payload);
+    expect(packageInfo).toStrictEqual({
+      licenseName: 'MIT',
+      packageName: 'test',
+      packageType: 'npm',
+      packageNamespace: undefined,
+      packagePURLAppendix: undefined,
+    });
+  });
+});
diff --git a/src/Frontend/Components/FetchLicenseInformationButton/__tests__/pypi-fetching-helpers.test.ts b/src/Frontend/Components/FetchLicenseInformationButton/__tests__/pypi-fetching-helpers.test.ts
new file mode 100644
index 00000000..98ebdf44
--- /dev/null
+++ b/src/Frontend/Components/FetchLicenseInformationButton/__tests__/pypi-fetching-helpers.test.ts
@@ -0,0 +1,47 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { convertPypiPayload, getPypiAPIUrl } from '../pypi-fetching-helpers';
+
+describe('getPypiAPIUrl', () => {
+  const EXPECTED_URL = 'https://pypi.org/pypi/numpy/json';
+
+  it('appends /json', () => {
+    expect(getPypiAPIUrl('https://pypi.org/pypi/numpy')).toBe(EXPECTED_URL);
+  });
+
+  it('appends json', () => {
+    expect(getPypiAPIUrl('https://pypi.org/pypi/numpy/')).toBe(EXPECTED_URL);
+  });
+
+  it('replaces project by pypi', () => {
+    expect(getPypiAPIUrl('https://pypi.org/project/numpy')).toBe(EXPECTED_URL);
+  });
+});
+
+describe('convertPypiPayload', () => {
+  it('raises for invalid payload', () => {
+    const payload = {
+      info: { packageName: 'test' },
+    };
+    expect(() => convertPypiPayload(payload)).toThrow(
+      'requires property "license"',
+    );
+  });
+
+  it('returns correct packageInfo', () => {
+    const payload = {
+      info: { license: 'test', name: 'test package' },
+    };
+
+    const packageInfo = convertPypiPayload(payload);
+    expect(packageInfo).toStrictEqual({
+      licenseName: 'test',
+      packageName: 'test package',
+      packageType: 'pypi',
+      packageNamespace: undefined,
+      packagePURLAppendix: undefined,
+    });
+  });
+});
diff --git a/src/Frontend/Components/ManualAttributionList/__tests__/ManualAttributionList.test.tsx b/src/Frontend/Components/ManualAttributionList/__tests__/ManualAttributionList.test.tsx
index e19b8aa9..75485420 100644
--- a/src/Frontend/Components/ManualAttributionList/__tests__/ManualAttributionList.test.tsx
+++ b/src/Frontend/Components/ManualAttributionList/__tests__/ManualAttributionList.test.tsx
@@ -54,6 +54,9 @@ describe('The ManualAttributionList', () => {
   const mockCallback = jest.fn((attributionId: string) => {
     return attributionId;
   });
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
 
   it('renders', () => {
     renderComponent(
diff --git a/src/Frontend/Components/PackageSearchPopup/__tests__/ClearlyDefinedPackageCard.test.tsx b/src/Frontend/Components/PackageSearchPopup/__tests__/ClearlyDefinedPackageCard.test.tsx
new file mode 100644
index 00000000..fc75e166
--- /dev/null
+++ b/src/Frontend/Components/PackageSearchPopup/__tests__/ClearlyDefinedPackageCard.test.tsx
@@ -0,0 +1,105 @@
+// SPDX-FileCopyrightText: Nico Carl <nicocarl@protonmail.com>
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { fireEvent, screen } from '@testing-library/react';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+
+import { getTemporaryDisplayPackageInfo } from '../../../state/selectors/all-views-resource-selectors';
+import { getOpenPopup } from '../../../state/selectors/view-selector';
+import { renderComponent } from '../../../test-helpers/render';
+import { ClearlyDefinedPackageCard } from '../ClearlyDefinedPackageCard';
+
+describe('ClearlyDefinedPackageCard', () => {
+  const axiosMock = new MockAdapter(axios);
+  const queryClient = new QueryClient({
+    defaultOptions: {
+      queries: {
+        retry: false,
+      },
+    },
+  });
+  const testCoordinate = 'sqlalchemy';
+  const definitionEndpoint = `https://api.clearlydefined.io/definitions/${testCoordinate}`;
+  const okStatus = 200;
+  const notFoundStatus = 404;
+
+  it('renders after successful fetch', async () => {
+    axiosMock.onGet(definitionEndpoint).replyOnce(okStatus, {
+      licensed: {
+        declared: 'MIT',
+        facets: {
+          core: {
+            attribution: {
+              parties: ['Copyright Jane Doe', 'Copyright John Doe'],
+            },
+          },
+        },
+      },
+      coordinates: {
+        type: 'pypi',
+        provider: 'pypi',
+        name: 'sqlalchemy',
+        revision: '1.4.1',
+      },
+      described: {
+        urls: {
+          registry: 'https://pypi.org/project/SQLAlchemy',
+          version: 'https://pypi.org/project/SQLAlchemy/1.4.1',
+        },
+      },
+    });
+
+    const { store } = renderComponent(
+      <QueryClientProvider client={queryClient}>
+        <ClearlyDefinedPackageCard coordinate={testCoordinate} />
+      </QueryClientProvider>,
+    );
+
+    expect(await screen.findByText('MIT')).toBeInTheDocument();
+    expect(
+      screen.getByText('https://pypi.org/project/SQLAlchemy/1.4.1'),
+    ).toBeInTheDocument();
+    expect(screen.getByText('sqlalchemy - 1.4.1')).toBeInTheDocument();
+
+    const expandAccordionButton = screen.getByLabelText('expand accordion');
+    fireEvent.click(expandAccordionButton);
+
+    expect(screen.getByText(RegExp('^Copyright Jane Doe'))).toBeInTheDocument();
+
+    const addPackageInformationButton = screen.getByLabelText('Add');
+    fireEvent.click(addPackageInformationButton);
+
+    expect(getTemporaryDisplayPackageInfo(store.getState())).toStrictEqual({
+      packageName: 'sqlalchemy',
+      packageVersion: '1.4.1',
+      licenseName: 'MIT',
+      packageNamespace: undefined,
+      packageType: 'pypi',
+      copyright: 'Copyright Jane Doe\nCopyright John Doe',
+      url: 'https://pypi.org/project/SQLAlchemy/1.4.1',
+      attributionIds: [],
+    });
+    expect(getOpenPopup(store.getState())).toBeNull();
+  });
+  it('shows error message when fetch fails', async () => {
+    // suppress output to console
+    jest.spyOn(console, 'error').mockImplementation(() => {});
+    axiosMock.onGet(definitionEndpoint).replyOnce(notFoundStatus);
+
+    renderComponent(
+      <QueryClientProvider client={queryClient}>
+        <ClearlyDefinedPackageCard coordinate={testCoordinate} />
+      </QueryClientProvider>,
+    );
+
+    expect(
+      await screen.findByText(
+        `Failed while fetching data for ${testCoordinate}: Request failed with status code 404`,
+      ),
+    ).toBeInTheDocument();
+  });
+});
diff --git a/src/Frontend/Components/PackageSearchPopup/__tests__/PackageSearchPopup.test.tsx b/src/Frontend/Components/PackageSearchPopup/__tests__/PackageSearchPopup.test.tsx
new file mode 100644
index 00000000..f4ffd560
--- /dev/null
+++ b/src/Frontend/Components/PackageSearchPopup/__tests__/PackageSearchPopup.test.tsx
@@ -0,0 +1,147 @@
+// SPDX-FileCopyrightText: Nico Carl <nicocarl@protonmail.com>
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { fireEvent, screen } from '@testing-library/react';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+
+import { renderComponent } from '../../../test-helpers/render';
+import { PackageSearchPopup } from '../PackageSearchPopup';
+
+describe('PackageSearchPopup', () => {
+  beforeAll(() => {
+    jest.useFakeTimers();
+  });
+
+  afterAll(() => {
+    jest.useRealTimers();
+  });
+
+  const axiosMock = new MockAdapter(axios);
+  const requestURLSearchEndpoint = RegExp(
+    '^https:\\/\\/api\\.clearlydefined\\.io\\/definitions\\?pattern=',
+  );
+  const baseURLDefinitionEndpoint =
+    'https://api.clearlydefined.io/definitions/';
+  const queryClient = new QueryClient({
+    defaultOptions: {
+      queries: {
+        retry: false,
+      },
+    },
+  });
+  const okStatus = 200;
+  const notFoundStatus = 404;
+
+  it('performs search successfully', async () => {
+    axiosMock
+      .onGet(requestURLSearchEndpoint)
+      .replyOnce(okStatus, ['sqlalchemy/1.4.1', 'sqlalchemy/1.3.0']);
+
+    axiosMock
+      .onGet(`${baseURLDefinitionEndpoint}sqlalchemy/1.4.1`)
+      .replyOnce(okStatus, {
+        licensed: {
+          declared: 'MIT',
+          facets: {
+            core: {
+              attribution: {
+                parties: ['Copyright Jane Doe', 'Copyright John Doe'],
+              },
+            },
+          },
+        },
+        coordinates: {
+          type: 'pypi',
+          provider: 'pypi',
+          name: 'sqlalchemy',
+          revision: '1.4.1',
+        },
+        described: {
+          urls: {
+            registry: 'https://pypi.org/project/SQLAlchemy',
+            version: 'https://pypi.org/project/SQLAlchemy/1.4.1',
+          },
+        },
+      });
+
+    axiosMock
+      .onGet(`${baseURLDefinitionEndpoint}sqlalchemy/1.3.0`)
+      .replyOnce(okStatus, {
+        licensed: {
+          declared: 'MIT',
+          facets: {
+            core: {
+              attribution: {
+                parties: ['Copyright Jane Doe', 'Copyright John Doe'],
+              },
+            },
+          },
+        },
+        coordinates: {
+          type: 'pypi',
+          provider: 'pypi',
+          name: 'sqlalchemy',
+          revision: '1.3.0',
+        },
+        described: {
+          urls: {
+            registry: 'https://pypi.org/project/SQLAlchemy',
+            version: 'https://pypi.org/project/SQLAlchemy/1.3.0',
+          },
+        },
+      });
+
+    renderComponent(
+      <QueryClientProvider client={queryClient}>
+        <PackageSearchPopup />
+      </QueryClientProvider>,
+    );
+    fireEvent.change(screen.getByRole('searchbox'), {
+      target: { value: 'sqlalchemy' },
+    });
+
+    expect(await screen.findByText('sqlalchemy/1.3.0')).toBeInTheDocument();
+    expect(screen.getByText('sqlalchemy/1.4.1')).toBeInTheDocument();
+    expect(screen.getAllByText('MIT')).toHaveLength(2);
+  });
+
+  it('shows an error message', async () => {
+    axiosMock.onGet(requestURLSearchEndpoint).replyOnce(notFoundStatus);
+
+    renderComponent(
+      <QueryClientProvider client={queryClient}>
+        <PackageSearchPopup />
+      </QueryClientProvider>,
+    );
+
+    fireEvent.change(screen.getByRole('searchbox'), {
+      target: { value: 'sqlalchemy' },
+    });
+
+    expect(
+      await screen.findByText(
+        'Failed while fetching data: Request failed with status code 404',
+      ),
+    ).toBeInTheDocument();
+  });
+
+  it('shows a message when nothing is found', async () => {
+    axiosMock.onGet(requestURLSearchEndpoint).reply(okStatus, []);
+
+    renderComponent(
+      <QueryClientProvider client={queryClient}>
+        <PackageSearchPopup />
+      </QueryClientProvider>,
+    );
+
+    fireEvent.change(screen.getByRole('searchbox'), {
+      target: { value: 'sqlalchemy' },
+    });
+
+    expect(await screen.findByText('No results found')).toBeInTheDocument();
+  });
+});
diff --git a/src/Frontend/Components/PackageSearchPopup/__tests__/fetch-license-from-clearly-defined.test.ts b/src/Frontend/Components/PackageSearchPopup/__tests__/fetch-license-from-clearly-defined.test.ts
new file mode 100644
index 00000000..0b384c87
--- /dev/null
+++ b/src/Frontend/Components/PackageSearchPopup/__tests__/fetch-license-from-clearly-defined.test.ts
@@ -0,0 +1,63 @@
+// SPDX-FileCopyrightText: Nico Carl <nicocarl@protonmail.com>
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+
+import { fetchFromClearlyDefined } from '../fetch-from-clearly-defined';
+
+describe('fetchFromClearlyDefined', () => {
+  const axiosMock = new MockAdapter(axios);
+  const testCoordinate = 'pypi/pypi/-/SQLAlchemy/1.4.14';
+  const requestURL = `https://api.clearlydefined.io/definitions/${testCoordinate}`;
+
+  it('parses payload correctly', async () => {
+    const okStatus = 200;
+    axiosMock.onGet(requestURL).replyOnce(okStatus, {
+      licensed: {
+        declared: 'MIT',
+        facets: {
+          core: {
+            attribution: {
+              parties: ['Copyright Jane Doe', 'Copyright John Doe'],
+            },
+          },
+        },
+      },
+      coordinates: {
+        type: 'pypi',
+        provider: 'pypi',
+        name: 'sqlalchemy',
+        revision: '1.4.14',
+      },
+      described: {
+        urls: {
+          registry: 'https://pypi.org/project/SQLAlchemy',
+          version: 'https://pypi.org/project/SQLAlchemy/1.4.14',
+        },
+      },
+    });
+    const packageInfo = await fetchFromClearlyDefined(testCoordinate);
+
+    expect(packageInfo).toStrictEqual({
+      packageName: 'sqlalchemy',
+      packageVersion: '1.4.14',
+      packageType: 'pypi',
+      packageNamespace: undefined,
+      copyright: 'Copyright Jane Doe\nCopyright John Doe',
+      licenseName: 'MIT',
+      url: 'https://pypi.org/project/SQLAlchemy/1.4.14',
+      attributionIds: [],
+    });
+  });
+
+  it('reject in correct payload', async () => {
+    const okStatus = 200;
+    axiosMock
+      .onGet(requestURL)
+      .replyOnce(okStatus, { someRandomField: 'value' });
+    await expect(fetchFromClearlyDefined(testCoordinate)).rejects.toBeTruthy();
+  });
+});
diff --git a/src/Frontend/Components/ProcessPopup/__tests__/ProcessPopup.test.tsx b/src/Frontend/Components/ProcessPopup/__tests__/ProcessPopup.test.tsx
index 713729df..040c56a8 100644
--- a/src/Frontend/Components/ProcessPopup/__tests__/ProcessPopup.test.tsx
+++ b/src/Frontend/Components/ProcessPopup/__tests__/ProcessPopup.test.tsx
@@ -32,6 +32,7 @@ const electronAPI: {
 
 describe('ProcessPopup', () => {
   beforeEach(() => {
+    jest.clearAllMocks();
     electronAPI.events = {};
     global.window.electronAPI = electronAPI as unknown as ElectronAPI;
   });
diff --git a/src/Frontend/Components/ReportTableItem/__tests__/report-table-item-helpers.test.tsx b/src/Frontend/Components/ReportTableItem/__tests__/report-table-item-helpers.test.tsx
index 6aaf9641..7cf96f61 100644
--- a/src/Frontend/Components/ReportTableItem/__tests__/report-table-item-helpers.test.tsx
+++ b/src/Frontend/Components/ReportTableItem/__tests__/report-table-item-helpers.test.tsx
@@ -85,7 +85,7 @@ describe('The table helpers', () => {
     ).toBe('No');
   });
 
-  it.each`
+  test.each`
     followUp     | expected
     ${undefined} | ${'No'}
     ${FollowUp}  | ${'Yes'}
@@ -112,7 +112,7 @@ describe('The table helpers', () => {
     },
   );
 
-  it.each`
+  test.each`
     property
     ${'copyright'}
     ${'licenseName'}
diff --git a/src/Frontend/Components/ResourcesTree/__tests__/resources-tree-helpers.test.ts b/src/Frontend/Components/ResourcesTree/__tests__/resources-tree-helpers.test.ts
index 9b840d7c..24c2a9dc 100644
--- a/src/Frontend/Components/ResourcesTree/__tests__/resources-tree-helpers.test.ts
+++ b/src/Frontend/Components/ResourcesTree/__tests__/resources-tree-helpers.test.ts
@@ -34,33 +34,63 @@ describe('splitResourceItToCurrentAndOtherFolder', () => {
     );
   });
 
-  it('correctly converts a list of resource ids (paths) into a Resources object', () => {
-    const testResourcesList: Array<string> = [
-      '/OpossumUI/DCO.md',
-      '/OpossumUI/src/Frontend/test.txt',
-      '/OpossumUI/src/Frontend/Components/file.tsx',
-      '/OpossumUI/src/abc.test.tsx',
-      '/OpossumUI/.idea/',
-    ];
+  const testResourcesList: Array<string> = [
+    '/OpossumUI/DCO.md',
+    '/OpossumUI/src/Frontend/test.txt',
+    '/OpossumUI/src/Frontend/Components/file.tsx',
+    '/OpossumUI/src/abc.test.tsx',
+    '/OpossumUI/.idea/',
+  ];
 
-    const expectedResources: Resources = {
-      OpossumUI: {
-        '.idea': {},
-        src: {
-          Frontend: {
-            'test.txt': 1,
-            Components: {
-              'file.tsx': 1,
-            },
+  const expectedResources: Resources = {
+    OpossumUI: {
+      '.idea': {},
+      src: {
+        Frontend: {
+          'test.txt': 1,
+          Components: {
+            'file.tsx': 1,
           },
-          'abc.test.tsx': 1,
         },
-        'DCO.md': 1,
+        'abc.test.tsx': 1,
       },
-    };
+      'DCO.md': 1,
+    },
+  };
 
+  it('correctly converts a list of resource ids (paths) into a Resources object', () => {
     expect(getResourcesFromResourcePaths(testResourcesList)).toEqual(
       expectedResources,
     );
   });
 });
+
+const testResourcesList: Array<string> = [
+  '/OpossumUI/DCO.md',
+  '/OpossumUI/src/Frontend/test.txt',
+  '/OpossumUI/src/Frontend/Components/file.tsx',
+  '/OpossumUI/src/abc.test.tsx',
+  '/OpossumUI/.idea/',
+];
+
+const expectedResources: Resources = {
+  OpossumUI: {
+    '.idea': {},
+    src: {
+      Frontend: {
+        'test.txt': 1,
+        Components: {
+          'file.tsx': 1,
+        },
+      },
+      'abc.test.tsx': 1,
+    },
+    'DCO.md': 1,
+  },
+};
+
+it('correctly converts a list of resource ids (paths) into a Resources object', () => {
+  expect(getResourcesFromResourcePaths(testResourcesList)).toEqual(
+    expectedResources,
+  );
+});
diff --git a/src/Frontend/Components/TextFieldStack/__tests__/TextFieldStack.test.tsx b/src/Frontend/Components/TextFieldStack/__tests__/TextFieldStack.test.tsx
new file mode 100644
index 00000000..16d0fde6
--- /dev/null
+++ b/src/Frontend/Components/TextFieldStack/__tests__/TextFieldStack.test.tsx
@@ -0,0 +1,29 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+// SPDX-FileCopyrightText: Nico Carl <nicocarl@protonmail.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { screen } from '@testing-library/react';
+
+import { renderComponent } from '../../../test-helpers/render';
+import { TextFieldStack } from '../TextFieldStack';
+
+describe('The TextFieldStack', () => {
+  it('renders comment TextBoxes with right titles and content', () => {
+    const isEditable = false;
+    const comments = [
+      'This is the first comment',
+      'This is the second comment',
+      'This is the third comment',
+      'This is the forth comment',
+    ];
+
+    renderComponent(
+      <TextFieldStack isEditable={isEditable} comments={comments} />,
+    );
+    comments.forEach((comment, index) => {
+      expect(screen.getByLabelText(`Comment ${index + 1}`)).toBeInTheDocument();
+      expect(screen.getByDisplayValue(comment)).toBeInTheDocument();
+    });
+  });
+});
diff --git a/src/Frontend/state/actions/popup-actions/__tests__/popup-actions.test.ts b/src/Frontend/state/actions/popup-actions/__tests__/popup-actions.test.ts
index 828b145f..9bb0ffe2 100644
--- a/src/Frontend/state/actions/popup-actions/__tests__/popup-actions.test.ts
+++ b/src/Frontend/state/actions/popup-actions/__tests__/popup-actions.test.ts
@@ -27,6 +27,7 @@ import { getParsedInputFileEnrichedWithTestData } from '../../../../test-helpers
 import { PanelPackage, State } from '../../../../types/types';
 import { createAppStore } from '../../../configure-store';
 import {
+  getCurrentAttributionId,
   getDisplayedPackage,
   getManualAttributions,
   getManualData,
@@ -45,6 +46,7 @@ import {
 import { getShowNoSignalsLocatedMessage } from '../../../selectors/locate-popup-selectors';
 import {
   getOpenPopup,
+  getPopupAttributionId,
   getSelectedView,
   getTargetView,
 } from '../../../selectors/view-selector';
@@ -70,12 +72,14 @@ import {
 } from '../../view-actions/view-actions';
 import {
   changeSelectedAttributionIdOrOpenUnsavedPopup,
-  checkIfPreferredStatusChangedAndShowWarningOrSave,
+  checkIfWasPreferredAndShowWarningOrUnlinkAndSave,
+  checkIfWasPreferredOrPreferredStatusChangedAndShowWarningOrSave,
   closePopupAndUnsetTargets,
   locateSignalsFromLocatorPopup,
   locateSignalsFromProjectStatisticsPopup,
   navigateToSelectedPathOrOpenUnsavedPopup,
   navigateToTargetResourceOrAttributionOrOpenFileDialog,
+  removeWasPreferred,
   saveTemporaryDisplayPackageInfoAndNavigateToTargetViewIfSavingIsNotDisabled,
   selectPackageCardInAuditViewOrOpenUnsavedPopup,
   setSelectedResourceIdOrOpenUnsavedPopup,
@@ -476,13 +480,69 @@ describe('The actions called from the unsaved popup', () => {
   }
 
   const views = [View.Audit, View.Attribution, View.Report];
+  describe.each(views)(
+    'checkIfWasPreferredAndShowWarningOrUnlinkAndSave in %s View',
+    (view: View) => {
+      const notSelectedViews = views.filter(
+        (viewCandidate) => viewCandidate !== view,
+      );
+
+      it('shows warning that attribution was preferred', () => {
+        const testDisplayPackageInfo: DisplayPackageInfo = {
+          ...testPackage,
+          wasPreferred: true,
+          attributionIds: ['id1'],
+        };
+        const testStore = prepareTestStore(view, testDisplayPackageInfo);
+        testStore.dispatch(setTargetView(notSelectedViews[0]));
+
+        testStore.dispatch(checkIfWasPreferredAndShowWarningOrUnlinkAndSave());
+        expect(getCurrentAttributionId(testStore.getState())).toBe('id1');
+        expect(getOpenPopup(testStore.getState())).toBe(
+          PopupType.ModifyWasPreferredAttributionPopup,
+        );
+        expect(getPopupAttributionId(testStore.getState())).toBe('id1');
+      });
+
+      it('saves and navigates to target if attribution was not preferred', () => {
+        const testDisplayPackageInfo: DisplayPackageInfo = {
+          ...testPackage,
+          attributionIds: ['id1'],
+        };
+        const testStore = prepareTestStore(view, testDisplayPackageInfo);
+        testStore.dispatch(setTargetView(notSelectedViews[0]));
+
+        testStore.dispatch(checkIfWasPreferredAndShowWarningOrUnlinkAndSave());
+        expect(getSelectedView(testStore.getState())).toBe(notSelectedViews[0]);
+      });
+    },
+  );
 
   describe.each(views)(
-    'checkIfPreferredStatusChangedAndShowWarningOrSave in %s View',
+    'checkIfWasPreferredOrPreferredStatusChangedAndShowWarningOrSave in %s View',
     (view: View) => {
       const notSelectedViews = views.filter(
         (viewCandidate) => viewCandidate !== view,
       );
+      it('shows warning that attribution was preferred', () => {
+        const testDisplayPackageInfo: DisplayPackageInfo = {
+          ...testPackage,
+          wasPreferred: true,
+          attributionIds: ['id1'],
+        };
+        const testStore = prepareTestStore(view, testDisplayPackageInfo);
+        testStore.dispatch(setTargetView(notSelectedViews[0]));
+
+        testStore.dispatch(
+          checkIfWasPreferredOrPreferredStatusChangedAndShowWarningOrSave(),
+        );
+        expect(getCurrentAttributionId(testStore.getState())).toBe('id1');
+        expect(getOpenPopup(testStore.getState())).toBe(
+          PopupType.ModifyWasPreferredAttributionPopup,
+        );
+        expect(getPopupAttributionId(testStore.getState())).toBe('id1');
+      });
+
       it('saves and navigates to target view', () => {
         const testDisplayPackageInfo: DisplayPackageInfo = {
           ...testPackage,
@@ -491,7 +551,9 @@ describe('The actions called from the unsaved popup', () => {
         const testStore = prepareTestStore(view, testDisplayPackageInfo);
         testStore.dispatch(setTargetView(notSelectedViews[0]));
 
-        testStore.dispatch(checkIfPreferredStatusChangedAndShowWarningOrSave());
+        testStore.dispatch(
+          checkIfWasPreferredOrPreferredStatusChangedAndShowWarningOrSave(),
+        );
         expect(getSelectedView(testStore.getState())).toBe(notSelectedViews[0]);
       });
     },
@@ -547,232 +609,244 @@ describe('The actions called from the unsaved popup', () => {
   });
 });
 
-describe('unlinkAttributionAndSavePackageInfoAndNavigateToTargetViewIfSavingIsNotDisabled', () => {
-  const testReact = {
-    packageName: 'React',
-  };
-  const testPackageCardId = 'Attributions-0';
-  const testDisplayPackageInfo: DisplayPackageInfo = {
-    ...testReact,
-    attributionIds: ['reactUuid'],
-  };
-  const testResources: Resources = {
-    'something.js': 1,
-    'somethingElse.js': 1,
-  };
-  const testInitialManualAttributions: Attributions = {
-    reactUuid: testReact,
-  };
-  const testInitialResourcesToManualAttributions: ResourcesToAttributions = {
-    '/something.js': ['reactUuid'],
-    '/somethingElse.js': ['reactUuid'],
-  };
+describe('Actions used by the ModifyWasPreferredAttributionPopup', () => {
+  describe('unlinkAttributionAndSavePackageInfoAndNavigateToTargetViewIfSavingIsNotDisabled', () => {
+    const testReact = {
+      packageName: 'React',
+    };
+    const testPackageCardId = 'Attributions-0';
+    const testDisplayPackageInfo: DisplayPackageInfo = {
+      ...testReact,
+      attributionIds: ['reactUuid'],
+    };
+    const testResources: Resources = {
+      'something.js': 1,
+      'somethingElse.js': 1,
+    };
+    const testInitialManualAttributions: Attributions = {
+      reactUuid: testReact,
+    };
+    const testInitialResourcesToManualAttributions: ResourcesToAttributions = {
+      '/something.js': ['reactUuid'],
+      '/somethingElse.js': ['reactUuid'],
+    };
 
-  function prepareTestStore() {
-    const testStore = createAppStore();
-    testStore.dispatch(
-      loadFromFile(
-        getParsedInputFileEnrichedWithTestData({
-          resources: testResources,
-          manualAttributions: testInitialManualAttributions,
-          resourcesToManualAttributions:
-            testInitialResourcesToManualAttributions,
+    function prepareTestStore() {
+      const testStore = createAppStore();
+      testStore.dispatch(
+        loadFromFile(
+          getParsedInputFileEnrichedWithTestData({
+            resources: testResources,
+            manualAttributions: testInitialManualAttributions,
+            resourcesToManualAttributions:
+              testInitialResourcesToManualAttributions,
+          }),
+        ),
+      );
+      testStore.dispatch(setSelectedResourceId('/something.js'));
+      testStore.dispatch(
+        setDisplayedPackage({
+          panel: PackagePanelTitle.ManualPackages,
+          packageCardId: testPackageCardId,
+          displayPackageInfo: testDisplayPackageInfo,
         }),
-      ),
-    );
-    testStore.dispatch(setSelectedResourceId('/something.js'));
-    testStore.dispatch(
-      setDisplayedPackage({
-        panel: PackagePanelTitle.ManualPackages,
-        packageCardId: testPackageCardId,
-        displayPackageInfo: testDisplayPackageInfo,
-      }),
-    );
-    testStore.dispatch(
-      setTemporaryDisplayPackageInfo(EMPTY_DISPLAY_PACKAGE_INFO),
-    );
-    return testStore;
-  }
+      );
+      testStore.dispatch(
+        setTemporaryDisplayPackageInfo(EMPTY_DISPLAY_PACKAGE_INFO),
+      );
+      return testStore;
+    }
 
-  it('unlinks and navigates to target view', () => {
-    const expectedManualData: AttributionData = {
-      attributions: testInitialManualAttributions,
-      resourcesToAttributions: {
-        '/somethingElse.js': ['reactUuid'],
-      },
-      attributionsToResources: {
-        reactUuid: ['/somethingElse.js'],
-      },
-      resourcesWithAttributedChildren: {
-        attributedChildren: {
-          '1': new Set<number>().add(2),
+    it('unlinks and navigates to target view', () => {
+      const expectedManualData: AttributionData = {
+        attributions: testInitialManualAttributions,
+        resourcesToAttributions: {
+          '/somethingElse.js': ['reactUuid'],
         },
-        pathsToIndices: {
-          '/': 1,
-          '/something.js': 0,
-          '/somethingElse.js': 2,
+        attributionsToResources: {
+          reactUuid: ['/somethingElse.js'],
         },
-        paths: ['/something.js', '/', '/somethingElse.js'],
-      },
-    };
+        resourcesWithAttributedChildren: {
+          attributedChildren: {
+            '1': new Set<number>().add(2),
+          },
+          pathsToIndices: {
+            '/': 1,
+            '/something.js': 0,
+            '/somethingElse.js': 2,
+          },
+          paths: ['/something.js', '/', '/somethingElse.js'],
+        },
+      };
 
-    const testStore = prepareTestStore();
-    testStore.dispatch(setTargetView(View.Attribution));
-    expect(getSelectedView(testStore.getState())).toBe(View.Audit);
+      const testStore = prepareTestStore();
+      testStore.dispatch(setTargetView(View.Attribution));
+      expect(getSelectedView(testStore.getState())).toBe(View.Audit);
 
-    testStore.dispatch(
-      unlinkAttributionAndSavePackageInfoAndNavigateToTargetViewIfSavingIsNotDisabled(),
-    );
-    expect(getManualData(testStore.getState())).toEqual(expectedManualData);
-    expect(getSelectedView(testStore.getState())).toBe(View.Attribution);
-  });
+      testStore.dispatch(
+        unlinkAttributionAndSavePackageInfoAndNavigateToTargetViewIfSavingIsNotDisabled(),
+      );
+      expect(getManualData(testStore.getState())).toEqual(expectedManualData);
+      expect(getSelectedView(testStore.getState())).toBe(View.Attribution);
+    });
 
-  it('warns the user that saving is disabled and does not change data', () => {
-    const expectedManualData: AttributionData = {
-      attributions: testInitialManualAttributions,
-      resourcesToAttributions: testInitialResourcesToManualAttributions,
-      attributionsToResources: {
-        reactUuid: ['/something.js', '/somethingElse.js'],
-      },
-      resourcesWithAttributedChildren: {
-        attributedChildren: {
-          '1': new Set<number>().add(0).add(2),
+    it('warns the user that saving is disabled and does not change data', () => {
+      const expectedManualData: AttributionData = {
+        attributions: testInitialManualAttributions,
+        resourcesToAttributions: testInitialResourcesToManualAttributions,
+        attributionsToResources: {
+          reactUuid: ['/something.js', '/somethingElse.js'],
         },
-        pathsToIndices: {
-          '/': 1,
-          '/something.js': 0,
-          '/somethingElse.js': 2,
+        resourcesWithAttributedChildren: {
+          attributedChildren: {
+            '1': new Set<number>().add(0).add(2),
+          },
+          pathsToIndices: {
+            '/': 1,
+            '/something.js': 0,
+            '/somethingElse.js': 2,
+          },
+          paths: ['/something.js', '/', '/somethingElse.js'],
         },
-        paths: ['/something.js', '/', '/somethingElse.js'],
-      },
-    };
-    const testStore = prepareTestStore();
+      };
+      const testStore = prepareTestStore();
 
-    testStore.dispatch(setAllowedSaveOperations(AllowedSaveOperations.None));
-    testStore.dispatch(setTargetView(View.Attribution));
-    expect(getSelectedView(testStore.getState())).toBe(View.Audit);
+      testStore.dispatch(setAllowedSaveOperations(AllowedSaveOperations.None));
+      testStore.dispatch(setTargetView(View.Attribution));
+      expect(getSelectedView(testStore.getState())).toBe(View.Audit);
 
-    testStore.dispatch(
-      unlinkAttributionAndSavePackageInfoAndNavigateToTargetViewIfSavingIsNotDisabled(),
-    );
-    expect(getOpenPopup(testStore.getState())).toBe(
-      PopupType.UnableToSavePopup,
-    );
-    expect(getManualData(testStore.getState())).toEqual(expectedManualData);
-    expect(getSelectedView(testStore.getState())).toBe(View.Audit);
+      testStore.dispatch(
+        unlinkAttributionAndSavePackageInfoAndNavigateToTargetViewIfSavingIsNotDisabled(),
+      );
+      expect(getOpenPopup(testStore.getState())).toBe(
+        PopupType.UnableToSavePopup,
+      );
+      expect(getManualData(testStore.getState())).toEqual(expectedManualData);
+      expect(getSelectedView(testStore.getState())).toBe(View.Audit);
+    });
   });
-});
 
-describe('saveTemporaryDisplayPackageInfoAndNavigateToTargetViewIfSavingIsNotDisabled', () => {
-  function prepareTestStore() {
-    const testStore = createAppStore();
-    const testResources: Resources = {
-      selectedResource: 1,
-      newSelectedResource: 1,
-    };
-    testStore.dispatch(
-      loadFromFile(getParsedInputFileEnrichedWithTestData(testResources)),
-    );
-    testStore.dispatch(setSelectedResourceId('selectedResource'));
-    testStore.dispatch(
-      setTemporaryDisplayPackageInfo({
-        packageName: 'Test',
-        attributionIds: [],
-      }),
-    );
-    testStore.dispatch(navigateToView(View.Audit));
-    testStore.dispatch(setTargetView(View.Attribution));
-    testStore.dispatch(openPopup(PopupType.NotSavedPopup));
-    testStore.dispatch(setTargetSelectedResourceId('newSelectedResource'));
+  describe('saveTemporaryDisplayPackageInfoAndNavigateToTargetViewIfSavingIsNotDisabled', () => {
+    function prepareTestStore() {
+      const testStore = createAppStore();
+      const testResources: Resources = {
+        selectedResource: 1,
+        newSelectedResource: 1,
+      };
+      testStore.dispatch(
+        loadFromFile(getParsedInputFileEnrichedWithTestData(testResources)),
+      );
+      testStore.dispatch(setSelectedResourceId('selectedResource'));
+      testStore.dispatch(
+        setTemporaryDisplayPackageInfo({
+          packageName: 'Test',
+          attributionIds: [],
+        }),
+      );
+      testStore.dispatch(navigateToView(View.Audit));
+      testStore.dispatch(setTargetView(View.Attribution));
+      testStore.dispatch(
+        openPopup(PopupType.ModifyWasPreferredAttributionPopup),
+      );
+      testStore.dispatch(setTargetSelectedResourceId('newSelectedResource'));
 
-    return testStore;
-  }
+      return testStore;
+    }
 
-  function prepareTestState(): State {
-    const testStore = prepareTestStore();
-    testStore.dispatch(
-      saveTemporaryDisplayPackageInfoAndNavigateToTargetViewIfSavingIsNotDisabled(),
-    );
+    function prepareTestState(): State {
+      const testStore = prepareTestStore();
+      testStore.dispatch(
+        saveTemporaryDisplayPackageInfoAndNavigateToTargetViewIfSavingIsNotDisabled(),
+      );
 
-    return testStore.getState();
-  }
+      return testStore.getState();
+    }
 
-  it('saves temporaryDisplayPackageInfo', () => {
-    const state: State = prepareTestState();
-    expect(getTemporaryDisplayPackageInfo(state)).toMatchObject({
-      attributionIds: [],
+    it('saves temporaryDisplayPackageInfo', () => {
+      const state: State = prepareTestState();
+      expect(getTemporaryDisplayPackageInfo(state)).toMatchObject({
+        attributionIds: [],
+      });
     });
-  });
 
-  it('sets TargetSelectedResourceOrAttribution', () => {
-    const state: State = prepareTestState();
-    expect(getSelectedResourceId(state)).toBe('newSelectedResource');
-  });
+    it('sets TargetSelectedResourceOrAttribution', () => {
+      const state: State = prepareTestState();
+      expect(getSelectedResourceId(state)).toBe('newSelectedResource');
+    });
 
-  it('sets View', () => {
-    const state: State = prepareTestState();
-    expect(getSelectedView(state)).toBe(View.Attribution);
-  });
+    it('sets View', () => {
+      const state: State = prepareTestState();
+      expect(getSelectedView(state)).toBe(View.Attribution);
+    });
 
-  it('closesPopup', () => {
-    const state: State = prepareTestState();
-    expect(getOpenPopup(state)).toBeFalsy();
-  });
+    it('closesPopup', () => {
+      const state: State = prepareTestState();
+      expect(getOpenPopup(state)).toBeFalsy();
+    });
 
-  it('warns if saving is disabled and does not change data', () => {
-    const testStore = prepareTestStore();
-    testStore.dispatch(setAllowedSaveOperations(AllowedSaveOperations.None));
+    it('warns if saving is disabled and does not change data', () => {
+      const testStore = prepareTestStore();
+      testStore.dispatch(setAllowedSaveOperations(AllowedSaveOperations.None));
 
-    testStore.dispatch(
-      saveTemporaryDisplayPackageInfoAndNavigateToTargetViewIfSavingIsNotDisabled(),
-    );
-    expect(getOpenPopup(testStore.getState())).toBe(
-      PopupType.UnableToSavePopup,
-    );
-    expect(getTemporaryDisplayPackageInfo(testStore.getState())).toMatchObject({
-      packageName: 'Test',
-      attributionIds: [],
+      testStore.dispatch(
+        saveTemporaryDisplayPackageInfoAndNavigateToTargetViewIfSavingIsNotDisabled(),
+      );
+      expect(getOpenPopup(testStore.getState())).toBe(
+        PopupType.UnableToSavePopup,
+      );
+      expect(
+        getTemporaryDisplayPackageInfo(testStore.getState()),
+      ).toMatchObject({
+        packageName: 'Test',
+        attributionIds: [],
+      });
+      expect(getSelectedView(testStore.getState())).toBe(View.Audit);
     });
-    expect(getSelectedView(testStore.getState())).toBe(View.Audit);
   });
 });
 
-describe('closePopupAndReopenEditAttributionPopupIfItWasPreviouslyOpen', () => {
-  it.each([[View.Audit], [View.Attribution]])(
-    'closes popup and unsets targets in % view',
-    (view: View) => {
+describe('Action used by NotSavedPopup and ModifyWasPreferredAttributionPopup', () => {
+  describe('closePopupAndReopenEditAttributionPopupIfItWasPreviouslyOpen', () => {
+    it.each([[View.Audit], [View.Attribution]])(
+      'closes popup and unsets targets in % view',
+      (view: View) => {
+        const testStore = createAppStore();
+        testStore.dispatch(navigateToView(view));
+        if (view === View.Attribution) {
+          testStore.dispatch(setSelectedAttributionId('id1'));
+        }
+        if (view === View.Audit) {
+          const testSelectedPackage: PanelPackage = {
+            panel: PackagePanelTitle.AllAttributions,
+            packageCardId: 'All Attributions-0',
+            displayPackageInfo: { attributionIds: ['id1'] },
+          };
+          testStore.dispatch(setDisplayedPackage(testSelectedPackage));
+        }
+        testStore.dispatch(
+          openPopup(PopupType.ModifyWasPreferredAttributionPopup),
+        );
+
+        testStore.dispatch(closePopupAndUnsetTargets());
+        expect(getTargetView(testStore.getState())).toBe(null);
+        expect(getTargetSelectedResourceId(testStore.getState())).toBe('');
+        expect(getTargetSelectedAttributionId(testStore.getState())).toBe('');
+        expect(getOpenPopup(testStore.getState())).toBeFalsy();
+      },
+    );
+
+    it('closes popup and unsets targets', () => {
       const testStore = createAppStore();
-      testStore.dispatch(navigateToView(view));
-      if (view === View.Attribution) {
-        testStore.dispatch(setSelectedAttributionId('id1'));
-      }
-      if (view === View.Audit) {
-        const testSelectedPackage: PanelPackage = {
-          panel: PackagePanelTitle.AllAttributions,
-          packageCardId: 'All Attributions-0',
-          displayPackageInfo: { attributionIds: ['id1'] },
-        };
-        testStore.dispatch(setDisplayedPackage(testSelectedPackage));
-      }
-      testStore.dispatch(openPopup(PopupType.NotSavedPopup));
+      testStore.dispatch(
+        openPopup(PopupType.ModifyWasPreferredAttributionPopup),
+      );
 
       testStore.dispatch(closePopupAndUnsetTargets());
       expect(getTargetView(testStore.getState())).toBe(null);
       expect(getTargetSelectedResourceId(testStore.getState())).toBe('');
       expect(getTargetSelectedAttributionId(testStore.getState())).toBe('');
-      expect(getOpenPopup(testStore.getState())).toBeFalsy();
-    },
-  );
-
-  it('closes popup and unsets targets', () => {
-    const testStore = createAppStore();
-    testStore.dispatch(openPopup(PopupType.NotSavedPopup));
-
-    testStore.dispatch(closePopupAndUnsetTargets());
-    expect(getTargetView(testStore.getState())).toBe(null);
-    expect(getTargetSelectedResourceId(testStore.getState())).toBe('');
-    expect(getTargetSelectedAttributionId(testStore.getState())).toBe('');
-    expect(getOpenPopup(testStore.getState())).toBe(null);
+      expect(getOpenPopup(testStore.getState())).toBe(null);
+    });
   });
 });
 
@@ -1071,3 +1145,21 @@ describe('locateSignalsFromProjectStatisticsPopup', () => {
     expect(getSelectedView(testStore.getState())).toBe(View.Audit);
   });
 });
+
+describe('removeWasPreferred', () => {
+  it('removes wasPreferred field from TemporaryDisplayPackageInfo', () => {
+    const testStore = createAppStore();
+    const temporaryDisplayPackageInfo = {
+      wasPreferred: true,
+      attributionIds: ['id1'],
+    };
+    testStore.dispatch(
+      setTemporaryDisplayPackageInfo(temporaryDisplayPackageInfo),
+    );
+
+    testStore.dispatch(removeWasPreferred());
+    expect(
+      getTemporaryDisplayPackageInfo(testStore.getState()).wasPreferred,
+    ).toBeFalsy();
+  });
+});
diff --git a/src/Frontend/state/actions/view-actions/__tests__/view-actions.test.ts b/src/Frontend/state/actions/view-actions/__tests__/view-actions.test.ts
index 68d2b14b..5afbcf53 100644
--- a/src/Frontend/state/actions/view-actions/__tests__/view-actions.test.ts
+++ b/src/Frontend/state/actions/view-actions/__tests__/view-actions.test.ts
@@ -206,9 +206,11 @@ describe('popup actions', () => {
       PopupType.EditAttributionPopup,
     );
 
-    testStore.dispatch(openPopup(PopupType.LocatorPopup));
+    testStore.dispatch(openPopup(PopupType.PackageSearchPopup));
     expect(getPopupAttributionId(testStore.getState())).toBeNull();
-    expect(getOpenPopup(testStore.getState())).toBe(PopupType.LocatorPopup);
+    expect(getOpenPopup(testStore.getState())).toBe(
+      PopupType.PackageSearchPopup,
+    );
 
     testStore.dispatch(closePopup());
     expect(getPopupAttributionId(testStore.getState())).toEqual(
diff --git a/src/Frontend/test-helpers/render.tsx b/src/Frontend/test-helpers/render.tsx
index e984ca0e..657ae0bb 100644
--- a/src/Frontend/test-helpers/render.tsx
+++ b/src/Frontend/test-helpers/render.tsx
@@ -2,7 +2,6 @@
 // SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
 //
 // SPDX-License-Identifier: Apache-2.0
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
 import { renderHook as nativeRenderHook, render } from '@testing-library/react';
 import { ReactElement } from 'react';
 import { Provider } from 'react-redux';
@@ -10,16 +9,6 @@ import { VirtuosoMockContext } from 'react-virtuoso';
 
 import { Action, createAppStore } from '../state/configure-store';
 
-function makeReactQueryClient() {
-  return new QueryClient({
-    defaultOptions: {
-      queries: {
-        retry: false,
-      },
-    },
-  });
-}
-
 export function renderComponent(
   component: ReactElement,
   {
@@ -36,13 +25,11 @@ export function renderComponent(
     ...render(component, {
       wrapper: ({ children }) => (
         <Provider store={store}>
-          <QueryClientProvider client={makeReactQueryClient()}>
-            <VirtuosoMockContext.Provider
-              value={{ itemHeight: 200, viewportHeight: 600 }}
-            >
-              {children}
-            </VirtuosoMockContext.Provider>
-          </QueryClientProvider>
+          <VirtuosoMockContext.Provider
+            value={{ itemHeight: 200, viewportHeight: 600 }}
+          >
+            {children}
+          </VirtuosoMockContext.Provider>
         </Provider>
       ),
     }),
diff --git a/src/Frontend/util/__tests__/package-search-api.test.ts b/src/Frontend/util/__tests__/package-search-api.test.ts
deleted file mode 100644
index 220c42f1..00000000
--- a/src/Frontend/util/__tests__/package-search-api.test.ts
+++ /dev/null
@@ -1,576 +0,0 @@
-// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
-// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
-//
-// SPDX-License-Identifier: Apache-2.0
-import { difference } from 'lodash';
-
-import { faker } from '../../../shared/Faker';
-import {
-  AutocompleteSignal,
-  DisplayPackageInfo,
-} from '../../../shared/shared-types';
-import { text } from '../../../shared/text';
-import { RequestProps } from '../http-client';
-import {
-  PackageSearchApi,
-  packageSystems,
-  packageSystemsRequiringNamespace,
-} from '../package-search-api';
-
-describe('PackageSearchApi', () => {
-  describe('getNames', () => {
-    it('serializes project with deterministic URL', async () => {
-      const packageInfo = faker.opossum.externalPackageInfo();
-      const name = faker.internet.domainWord();
-      const namespace = faker.internet.domainWord();
-      const projectSuggestion = faker.packageSearch.projectSuggestion({
-        name: `${namespace}/${name}`,
-        projectType: 'GITHUB',
-      });
-      const httpClient = faker.httpClient(
-        faker.packageSearch.searchSuggestionResponse({
-          results: [projectSuggestion],
-        }),
-      );
-      const packageSearchApi = new PackageSearchApi(httpClient);
-
-      const searchResults = await packageSearchApi.getNames(packageInfo);
-
-      expect(httpClient.request).toHaveBeenCalledTimes(1);
-      expect(searchResults).toEqual([
-        expect.objectContaining<Partial<AutocompleteSignal>>({
-          packageName: name,
-          packageNamespace: namespace,
-          packageType: 'github',
-          url: `https://github.com/${projectSuggestion.name}`,
-        }),
-      ]);
-    });
-
-    it('serializes maven package', async () => {
-      const packageInfo = faker.opossum.externalPackageInfo();
-      const name = faker.internet.domainWord();
-      const namespace = faker.internet.domainWord();
-      const projectSuggestion = faker.packageSearch.packageSuggestion({
-        name: `${namespace}:${name}`,
-        system: 'MAVEN',
-      });
-      const httpClient = faker.httpClient(
-        faker.packageSearch.searchSuggestionResponse({
-          results: [projectSuggestion],
-        }),
-      );
-      const packageSearchApi = new PackageSearchApi(httpClient);
-
-      const searchResults = await packageSearchApi.getNames(packageInfo);
-
-      expect(httpClient.request).toHaveBeenCalledTimes(1);
-      expect(searchResults).toEqual([
-        expect.objectContaining<Partial<AutocompleteSignal>>({
-          packageName: name,
-          packageNamespace: namespace,
-          packageType: 'maven',
-        }),
-      ]);
-    });
-
-    it('serializes golang package', async () => {
-      const packageInfo = faker.opossum.externalPackageInfo();
-      const projectSuggestion = faker.packageSearch.packageSuggestion({
-        system: 'GO',
-      });
-      const httpClient = faker.httpClient(
-        faker.packageSearch.searchSuggestionResponse({
-          results: [projectSuggestion],
-        }),
-      );
-      const packageSearchApi = new PackageSearchApi(httpClient);
-
-      const searchResults = await packageSearchApi.getNames(packageInfo);
-
-      expect(httpClient.request).toHaveBeenCalledTimes(1);
-      expect(searchResults).toEqual([
-        expect.objectContaining<Partial<AutocompleteSignal>>({
-          packageName: projectSuggestion.name,
-          packageNamespace: undefined,
-          packageType: 'golang',
-        }),
-      ]);
-    });
-
-    it('serializes NPM package', async () => {
-      const packageInfo = faker.opossum.externalPackageInfo();
-      const projectSuggestion = faker.packageSearch.packageSuggestion({
-        system: 'NPM',
-      });
-      const httpClient = faker.httpClient(
-        faker.packageSearch.searchSuggestionResponse({
-          results: [projectSuggestion],
-        }),
-      );
-      const packageSearchApi = new PackageSearchApi(httpClient);
-
-      const searchResults = await packageSearchApi.getNames(packageInfo);
-
-      expect(httpClient.request).toHaveBeenCalledTimes(1);
-      expect(searchResults).toEqual([
-        expect.objectContaining<Partial<AutocompleteSignal>>({
-          packageName: projectSuggestion.name,
-          packageNamespace: undefined,
-          packageType: 'npm',
-        }),
-      ]);
-    });
-
-    it('removes advisories from results', async () => {
-      const packageInfo = faker.opossum.externalPackageInfo();
-      const advisorySuggestion = faker.packageSearch.advisorySuggestion();
-      const httpClient = faker.httpClient(
-        faker.packageSearch.searchSuggestionResponse({
-          results: [advisorySuggestion],
-        }),
-      );
-      const packageSearchApi = new PackageSearchApi(httpClient);
-
-      const searchResults = await packageSearchApi.getNames(packageInfo);
-
-      expect(httpClient.request).toHaveBeenCalledTimes(1);
-      expect(searchResults).toEqual([]);
-    });
-
-    it('deserializes input name', async () => {
-      const deserializedPackageName = faker.internet.domainWord();
-      const packageInfo = faker.opossum.externalPackageInfo({
-        packageName: ` ${deserializedPackageName.toUpperCase()} `,
-      });
-      const httpClient = faker.httpClient(
-        faker.packageSearch.searchSuggestionResponse({
-          results: [],
-        }),
-      );
-      const packageSearchApi = new PackageSearchApi(httpClient);
-
-      await packageSearchApi.getNames(packageInfo);
-
-      expect(httpClient.request).toHaveBeenCalledTimes(1);
-      expect(httpClient.request).toHaveBeenCalledWith(
-        expect.objectContaining<Partial<RequestProps>>({
-          params: { q: deserializedPackageName },
-        }),
-      );
-    });
-
-    it('does not perform request when no package name present', async () => {
-      const packageInfo = faker.opossum.externalPackageInfo({
-        packageName: '   ',
-      });
-      const httpClient = faker.httpClient(
-        faker.packageSearch.searchSuggestionResponse({
-          results: [],
-        }),
-      );
-      const packageSearchApi = new PackageSearchApi(httpClient);
-
-      await packageSearchApi.getNames(packageInfo);
-
-      expect(httpClient.request).not.toHaveBeenCalled();
-    });
-  });
-
-  describe('getNamespaces', () => {
-    it('gets namespaces for project', async () => {
-      const packageInfo = faker.opossum.externalPackageInfo({
-        packageType: faker.packageSearch.projectType(),
-      });
-      const httpClient = faker.httpClient(
-        faker.packageSearch.searchSuggestionResponse(),
-      );
-      const packageSearchApi = new PackageSearchApi(httpClient);
-
-      await packageSearchApi.getNamespaces(packageInfo);
-
-      expect(httpClient.request).toHaveBeenCalledTimes(1);
-      expect(httpClient.request).toHaveBeenCalledWith(
-        expect.objectContaining<Partial<RequestProps>>({
-          params: { q: packageInfo.packageName, kind: 'PROJECT' },
-        }),
-      );
-    });
-
-    it('gets namespaces for package system which requires one', async () => {
-      const system = faker.helpers.arrayElement(
-        packageSystemsRequiringNamespace,
-      );
-      const packageInfo = faker.opossum.externalPackageInfo({
-        packageType: system,
-      });
-      const httpClient = faker.httpClient(
-        faker.packageSearch.searchSuggestionResponse(),
-      );
-      const packageSearchApi = new PackageSearchApi(httpClient);
-
-      await packageSearchApi.getNamespaces(packageInfo);
-
-      expect(httpClient.request).toHaveBeenCalledTimes(1);
-      expect(httpClient.request).toHaveBeenCalledWith(
-        expect.objectContaining<Partial<RequestProps>>({
-          params: { q: packageInfo.packageName, system, kind: 'PACKAGE' },
-        }),
-      );
-    });
-
-    it('does not get namespaces for package system which does not require one', async () => {
-      const packageInfo = faker.opossum.externalPackageInfo({
-        packageType: faker.helpers.arrayElement(
-          difference(packageSystems, packageSystemsRequiringNamespace),
-        ),
-      });
-      const httpClient = faker.httpClient(
-        faker.packageSearch.searchSuggestionResponse(),
-      );
-      const packageSearchApi = new PackageSearchApi(httpClient);
-
-      await packageSearchApi.getNamespaces(packageInfo);
-
-      expect(httpClient.request).not.toHaveBeenCalled();
-    });
-
-    it('does not get namespaces for unknown package type', async () => {
-      const packageInfo = faker.opossum.externalPackageInfo();
-      const httpClient = faker.httpClient(
-        faker.packageSearch.searchSuggestionResponse(),
-      );
-      const packageSearchApi = new PackageSearchApi(httpClient);
-
-      await packageSearchApi.getNamespaces(packageInfo);
-
-      expect(httpClient.request).not.toHaveBeenCalled();
-    });
-  });
-
-  describe('getVersions', () => {
-    it('provides semantically sorted default and non-default versions for known package types', async () => {
-      const packageInfo = faker.opossum.externalPackageInfo({
-        packageType: 'npm',
-      });
-      const defaultVersion = faker.packageSearch.versionResponse({
-        isDefault: true,
-      });
-      const nonDefaultVersion1 = faker.packageSearch.versionResponse({
-        isDefault: false,
-        versionKey: faker.packageSearch.versionKey({
-          version: '9.0.0',
-        }),
-      });
-      const nonDefaultVersion2 = faker.packageSearch.versionResponse({
-        isDefault: false,
-        versionKey: faker.packageSearch.versionKey({
-          version: '13.0.0',
-        }),
-      });
-      const httpClient = faker.httpClient(
-        faker.packageSearch.versionsResponse({
-          versions: [nonDefaultVersion1, defaultVersion, nonDefaultVersion2],
-        }),
-      );
-      const packageSearchApi = new PackageSearchApi(httpClient);
-
-      const versions = await packageSearchApi.getVersions(packageInfo);
-
-      expect(httpClient.request).toHaveBeenCalledTimes(1);
-      expect(versions).toEqual([
-        expect.objectContaining<Partial<AutocompleteSignal>>({
-          packageName: defaultVersion.versionKey.name,
-          packageType: defaultVersion.versionKey.system.toLowerCase(),
-          packageVersion: defaultVersion.versionKey.version,
-          source: {
-            name: text.attributionColumn.openSourceInsights,
-            documentConfidence: 100,
-          },
-          suffix: '(default)',
-        }),
-        expect.objectContaining<Partial<AutocompleteSignal>>({
-          packageName: nonDefaultVersion2.versionKey.name,
-          packageType: nonDefaultVersion2.versionKey.system.toLowerCase(),
-          packageVersion: nonDefaultVersion2.versionKey.version,
-          source: {
-            name: text.attributionColumn.openSourceInsights,
-            documentConfidence: 100,
-          },
-        }),
-        expect.objectContaining<Partial<AutocompleteSignal>>({
-          packageName: nonDefaultVersion1.versionKey.name,
-          packageType: nonDefaultVersion1.versionKey.system.toLowerCase(),
-          packageVersion: nonDefaultVersion1.versionKey.version,
-          source: {
-            name: text.attributionColumn.openSourceInsights,
-            documentConfidence: 100,
-          },
-        }),
-      ]);
-    });
-
-    it('provides tags as versions for GitHub packages', async () => {
-      const packageName = faker.internet.domainWord();
-      const packageNamespace = faker.internet.domainWord();
-      const packageInfo = faker.opossum.externalPackageInfo({
-        packageType: 'github',
-        packageName,
-        packageNamespace,
-      });
-      const tagName = faker.system.semver();
-      const httpClient = faker.httpClient([
-        faker.packageSearch.tagResponse({ name: tagName }),
-      ]);
-      const packageSearchApi = new PackageSearchApi(httpClient);
-
-      const versions = await packageSearchApi.getVersions(packageInfo);
-
-      expect(httpClient.request).toHaveBeenCalledTimes(1);
-      expect(versions).toEqual([
-        expect.objectContaining<Partial<AutocompleteSignal>>({
-          packageName,
-          packageNamespace,
-          packageType: 'github',
-          packageVersion: tagName,
-          source: {
-            name: text.attributionColumn.openSourceInsights,
-            documentConfidence: 100,
-          },
-          url: `https://github.com/${packageNamespace}/${packageName}`,
-        }),
-      ]);
-    });
-
-    it('provides tags as versions for GitLab packages', async () => {
-      const packageName = faker.internet.domainWord();
-      const packageNamespace = faker.internet.domainWord();
-      const packageInfo = faker.opossum.externalPackageInfo({
-        packageType: 'gitlab',
-        packageName,
-        packageNamespace,
-      });
-      const tagName = faker.system.semver();
-      const httpClient = faker.httpClient([
-        faker.packageSearch.tagResponse({ name: tagName }),
-      ]);
-      const packageSearchApi = new PackageSearchApi(httpClient);
-
-      const versions = await packageSearchApi.getVersions(packageInfo);
-
-      expect(httpClient.request).toHaveBeenCalledTimes(1);
-      expect(versions).toEqual([
-        expect.objectContaining<Partial<AutocompleteSignal>>({
-          packageName,
-          packageNamespace,
-          packageType: 'gitlab',
-          packageVersion: tagName,
-          source: {
-            name: text.attributionColumn.openSourceInsights,
-            documentConfidence: 100,
-          },
-          url: `https://gitlab.com/${packageNamespace}/${packageName}`,
-        }),
-      ]);
-    });
-
-    it('deserializes "golang" package type', async () => {
-      const packageInfo = faker.opossum.externalPackageInfo({
-        packageType: 'golang',
-      });
-      const httpClient = faker.httpClient(
-        faker.packageSearch.versionsResponse(),
-      );
-      const packageSearchApi = new PackageSearchApi(httpClient);
-
-      await packageSearchApi.getVersions(packageInfo);
-
-      expect(httpClient.request).toHaveBeenCalledWith(
-        expect.objectContaining<Partial<RequestProps>>({
-          path: expect.stringContaining('/GO/'),
-        }),
-      );
-    });
-
-    it('deserializes "go" package type', async () => {
-      const packageInfo = faker.opossum.externalPackageInfo({
-        packageType: 'go',
-      });
-      const httpClient = faker.httpClient(
-        faker.packageSearch.versionsResponse(),
-      );
-      const packageSearchApi = new PackageSearchApi(httpClient);
-
-      await packageSearchApi.getVersions(packageInfo);
-
-      expect(httpClient.request).toHaveBeenCalledWith(
-        expect.objectContaining<Partial<RequestProps>>({
-          path: expect.stringContaining('/GO/'),
-        }),
-      );
-    });
-
-    it('deserializes maven name and namespace', async () => {
-      const name = faker.internet.domainWord();
-      const namespace = faker.internet.domainWord();
-      const packageInfo = faker.opossum.externalPackageInfo({
-        packageType: 'maven',
-        packageName: name,
-        packageNamespace: namespace,
-      });
-      const httpClient = faker.httpClient(
-        faker.packageSearch.versionsResponse(),
-      );
-      const packageSearchApi = new PackageSearchApi(httpClient);
-
-      await packageSearchApi.getVersions(packageInfo);
-
-      expect(httpClient.request).toHaveBeenCalledWith(
-        expect.objectContaining<Partial<RequestProps>>({
-          path: expect.stringContaining(`/${namespace}%3A${name}`),
-        }),
-      );
-    });
-  });
-
-  describe('enrichPackageInfo', () => {
-    it('provides repo URL and license for a given package version', async () => {
-      const packageInfo = faker.opossum.displayPackageInfo({
-        packageType: faker.packageSearch.packageSystem(),
-        packageNamespace: faker.internet.domainWord(),
-        url: undefined,
-        licenseName: undefined,
-      });
-      const links = faker.packageSearch.links({ origins: [] });
-      const license1 = faker.commerce.productName();
-      const license2 = faker.commerce.productName();
-      const httpClient = faker.httpClient(
-        faker.packageSearch.webVersionResponse({
-          version: { links, licenses: [license1, license2] },
-        }),
-      );
-      const packageSearchApi = new PackageSearchApi(httpClient);
-
-      const enriched = await packageSearchApi.enrichPackageInfo(packageInfo);
-
-      expect(httpClient.request).toHaveBeenCalledTimes(1);
-      expect(enriched).toEqual(
-        expect.objectContaining<Partial<DisplayPackageInfo>>({
-          url: links.repo,
-          licenseName: `${license1} AND ${license2}`,
-          comments: [`${text.attributionColumn.homepage}: ${links.homepage!}`],
-        }),
-      );
-    });
-
-    it('gets license based on package default version if no package version provided', async () => {
-      const packageInfo = faker.opossum.displayPackageInfo({
-        packageType: faker.packageSearch.packageSystem(),
-        packageNamespace: faker.internet.domainWord(),
-        packageVersion: undefined,
-        url: undefined,
-        licenseName: undefined,
-        copyright: undefined,
-      });
-      const defaultVersion = faker.system.semver();
-      const httpClient = faker.httpClient(
-        faker.packageSearch.defaultVersionResponse({ defaultVersion }),
-        faker.packageSearch.webVersionResponse(),
-      );
-      const packageSearchApi = new PackageSearchApi(httpClient);
-
-      await packageSearchApi.enrichPackageInfo(packageInfo);
-
-      expect(httpClient.request).toHaveBeenCalledTimes(2);
-      expect(httpClient.request).toHaveBeenNthCalledWith(
-        2,
-        expect.objectContaining<Partial<RequestProps>>({
-          path: expect.stringContaining(`/v/${defaultVersion}`),
-        }),
-      );
-    });
-
-    it('provides repo URL, license, and copyright for a GitHub project', async () => {
-      const name = faker.internet.domainWord();
-      const namespace = faker.internet.domainWord();
-      const packageInfo = faker.opossum.displayPackageInfo({
-        packageType: 'github',
-        packageName: name,
-        packageNamespace: namespace,
-        url: undefined,
-        licenseName: undefined,
-        copyright: undefined,
-      });
-      const license = faker.commerce.productName();
-      const copyright = faker.opossum.copyright();
-      const httpClient = faker.httpClient(
-        faker.packageSearch.gitHubLicenseResponse({
-          license: { name: license },
-          content: faker.packageSearch.licenseTextWithCopyright(copyright),
-        }),
-      );
-      const packageSearchApi = new PackageSearchApi(httpClient);
-
-      const enriched = await packageSearchApi.enrichPackageInfo(packageInfo);
-
-      expect(httpClient.request).toHaveBeenCalledTimes(1);
-      expect(enriched).toEqual(
-        expect.objectContaining<Partial<DisplayPackageInfo>>({
-          url: `https://github.com/${namespace}/${name}`,
-          licenseName: license,
-          packageVersion: packageInfo.packageVersion,
-          copyright,
-        }),
-      );
-    });
-
-    it('provides repo URL, license, and copyright for a GitLab project', async () => {
-      const name = faker.internet.domainWord();
-      const namespace = faker.internet.domainWord();
-      const packageInfo = faker.opossum.displayPackageInfo({
-        packageType: 'gitlab',
-        packageName: name,
-        packageNamespace: namespace,
-        url: undefined,
-        licenseName: undefined,
-        copyright: undefined,
-      });
-      const license = faker.commerce.productName();
-      const copyright = faker.opossum.copyright();
-      const httpClient = faker.httpClient(
-        faker.packageSearch.gitLabProjectResponse({
-          license: { name: license },
-        }),
-        faker.packageSearch.gitLabLicenseResponse({
-          content: faker.packageSearch.licenseTextWithCopyright(copyright),
-        }),
-      );
-      const packageSearchApi = new PackageSearchApi(httpClient);
-
-      const enriched = await packageSearchApi.enrichPackageInfo(packageInfo);
-
-      expect(httpClient.request).toHaveBeenCalledTimes(2);
-      expect(enriched).toEqual(
-        expect.objectContaining<Partial<DisplayPackageInfo>>({
-          url: `https://gitlab.com/${namespace}/${name}`,
-          licenseName: license,
-          packageVersion: packageInfo.packageVersion,
-          copyright,
-        }),
-      );
-    });
-
-    it('does not enrich package info if URL, copyright, and license name are already known', async () => {
-      const packageInfo = faker.opossum.displayPackageInfo({
-        packageType: faker.packageSearch.packageSystem(),
-        packageNamespace: faker.internet.domainWord(),
-      });
-      const httpClient = faker.httpClient();
-      const packageSearchApi = new PackageSearchApi(httpClient);
-
-      await packageSearchApi.enrichPackageInfo(packageInfo);
-
-      expect(httpClient.request).not.toHaveBeenCalled();
-    });
-  });
-});
diff --git a/src/Frontend/web-workers/scripts/__tests__/get-autocomplete-signals.test.ts b/src/Frontend/web-workers/scripts/__tests__/get-autocomplete-signals.test.ts
index f7afc83e..3c2b32b5 100644
--- a/src/Frontend/web-workers/scripts/__tests__/get-autocomplete-signals.test.ts
+++ b/src/Frontend/web-workers/scripts/__tests__/get-autocomplete-signals.test.ts
@@ -3,8 +3,7 @@
 //
 // SPDX-License-Identifier: Apache-2.0
 import { faker } from '../../../../shared/Faker';
-import { AutocompleteSignal } from '../../../../shared/shared-types';
-import { EMPTY_DISPLAY_PACKAGE_INFO } from '../../../shared-constants';
+import { SignalWithCount } from '../../../../shared/shared-types';
 import { getAutocompleteSignals } from '../get-autocomplete-signals';
 
 describe('getAutocompleteSignals', () => {
@@ -60,54 +59,11 @@ describe('getAutocompleteSignals', () => {
     });
 
     expect(signals).toHaveLength(1);
-    expect(signals).toEqual<Array<AutocompleteSignal>>([
-      { ...EMPTY_DISPLAY_PACKAGE_INFO, ...attribution1, count: 2 },
+    expect(signals).toEqual<Array<SignalWithCount>>([
+      { ...attribution1, count: 2 },
     ]);
   });
 
-  it('does not offer preferred attributions as suggestions', () => {
-    const [resourceName1, resourceName2] = faker.opossum.resourceNames({
-      count: 2,
-    });
-    const [attributionId1, attribution1] = faker.opossum.externalAttribution({
-      preferred: true,
-    });
-
-    const signals = getAutocompleteSignals({
-      externalData: faker.opossum.externalAttributionData({
-        resourcesToAttributions: faker.opossum.resourcesToAttributions({
-          [faker.opossum.folderPath(resourceName1)]: [attributionId1],
-          [faker.opossum.filePath(resourceName1, resourceName2)]: [
-            attributionId1,
-          ],
-        }),
-        attributions: faker.opossum.externalAttributions({
-          [attributionId1]: attribution1,
-        }),
-        resourcesWithAttributedChildren:
-          faker.opossum.resourcesWithAttributedChildren({
-            attributedChildren: {
-              '0': new Set([1]),
-            },
-            pathsToIndices: {
-              [faker.opossum.folderPath(resourceName1)]: 0,
-              [faker.opossum.filePath(resourceName1, resourceName2)]: 1,
-            },
-            paths: [
-              faker.opossum.folderPath(resourceName1),
-              faker.opossum.filePath(resourceName1, resourceName2),
-            ],
-          }),
-      }),
-      manualData: faker.opossum.manualAttributionData(),
-      resolvedExternalAttributions: new Set(),
-      resourceId: faker.opossum.folderPath(resourceName1),
-      sources: faker.opossum.externalAttributionSources(),
-    });
-
-    expect(signals).toHaveLength(0);
-  });
-
   it('ranks data by count', () => {
     const [resourceName1, resourceName2, resourceName3] =
       faker.opossum.resourceNames({
@@ -155,47 +111,67 @@ describe('getAutocompleteSignals', () => {
     });
 
     expect(signals).toHaveLength(2);
-    expect(signals).toEqual<Array<AutocompleteSignal>>([
-      { ...EMPTY_DISPLAY_PACKAGE_INFO, ...attribution1, count: 2 },
-      { ...EMPTY_DISPLAY_PACKAGE_INFO, ...attribution2, count: 1 },
+    expect(signals).toEqual<Array<SignalWithCount>>([
+      { ...attribution1, count: 2 },
+      { ...attribution2, count: 1 },
     ]);
   });
 
-  it('ranks data by was-preferred', () => {
-    const [resourceName1, resourceName2] = faker.opossum.resourceNames({
-      count: 2,
-    });
+  it('ranks data by preferred and then was-preferred', () => {
+    const [resourceName1, resourceName2, resourceName3] =
+      faker.opossum.resourceNames({
+        count: 3,
+      });
     const [attributionId1, attribution1] = faker.opossum.externalAttribution({
+      preferred: false,
       wasPreferred: false,
     });
     const [attributionId2, attribution2] = faker.opossum.externalAttribution({
+      preferred: false,
       wasPreferred: true,
     });
+    const [attributionId3, attribution3] = faker.opossum.externalAttribution({
+      preferred: true,
+      wasPreferred: undefined,
+    });
 
     const signals = getAutocompleteSignals({
       externalData: faker.opossum.externalAttributionData({
         resourcesToAttributions: faker.opossum.resourcesToAttributions({
           [faker.opossum.folderPath(resourceName1)]: [attributionId1],
-          [faker.opossum.filePath(resourceName1, resourceName2)]: [
+          [faker.opossum.folderPath(resourceName1, resourceName2)]: [
             attributionId2,
           ],
+          [faker.opossum.filePath(resourceName1, resourceName2, resourceName3)]:
+            [attributionId3],
         }),
         attributions: faker.opossum.externalAttributions({
           [attributionId1]: attribution1,
           [attributionId2]: attribution2,
+          [attributionId3]: attribution3,
         }),
         resourcesWithAttributedChildren:
           faker.opossum.resourcesWithAttributedChildren({
             attributedChildren: {
-              '0': new Set([1]),
+              '0': new Set([1, 2]),
             },
             pathsToIndices: {
               [faker.opossum.folderPath(resourceName1)]: 0,
-              [faker.opossum.filePath(resourceName1, resourceName2)]: 1,
+              [faker.opossum.folderPath(resourceName1, resourceName2)]: 1,
+              [faker.opossum.filePath(
+                resourceName1,
+                resourceName2,
+                resourceName3,
+              )]: 2,
             },
             paths: [
               faker.opossum.folderPath(resourceName1),
-              faker.opossum.filePath(resourceName1, resourceName2),
+              faker.opossum.folderPath(resourceName1, resourceName2),
+              faker.opossum.filePath(
+                resourceName1,
+                resourceName2,
+                resourceName3,
+              ),
             ],
           }),
       }),
@@ -205,10 +181,11 @@ describe('getAutocompleteSignals', () => {
       sources: faker.opossum.externalAttributionSources(),
     });
 
-    expect(signals).toHaveLength(2);
-    expect(signals).toEqual<Array<AutocompleteSignal>>([
-      { ...EMPTY_DISPLAY_PACKAGE_INFO, ...attribution2, count: 1 },
-      { ...EMPTY_DISPLAY_PACKAGE_INFO, ...attribution1, count: 1 },
+    expect(signals).toHaveLength(3);
+    expect(signals).toEqual<Array<SignalWithCount>>([
+      { ...attribution3, count: 1 },
+      { ...attribution2, count: 1 },
+      { ...attribution1, count: 1 },
     ]);
   });
 
@@ -262,9 +239,9 @@ describe('getAutocompleteSignals', () => {
     });
 
     expect(signals).toHaveLength(2);
-    expect(signals).toEqual<Array<AutocompleteSignal>>([
-      { ...EMPTY_DISPLAY_PACKAGE_INFO, ...attribution2, count: 1 },
-      { ...EMPTY_DISPLAY_PACKAGE_INFO, ...attribution1, count: 1 },
+    expect(signals).toEqual<Array<SignalWithCount>>([
+      { ...attribution2, count: 1 },
+      { ...attribution1, count: 1 },
     ]);
   });
 });
diff --git a/src/e2e-tests/__tests__/saving-attributions.test.ts b/src/e2e-tests/__tests__/saving-attributions.test.ts
index bb19d50d..4da9e63a 100644
--- a/src/e2e-tests/__tests__/saving-attributions.test.ts
+++ b/src/e2e-tests/__tests__/saving-attributions.test.ts
@@ -5,13 +5,8 @@
 import { DiscreteConfidence } from '../../shared/shared-types';
 import { expect, faker, test } from '../utils';
 
-const [
-  resourceName1,
-  resourceName2,
-  resourceName3,
-  resourceName4,
-  resourceName5,
-] = faker.opossum.resourceNames({ count: 5 });
+const [resourceName1, resourceName2, resourceName3, resourceName4] =
+  faker.opossum.resourceNames({ count: 4 });
 const license1 = faker.opossum.license();
 const license2 = faker.opossum.license();
 const [attributionId1, packageInfo1] = faker.opossum.manualAttribution({
@@ -21,10 +16,6 @@ const [attributionId2, packageInfo2] = faker.opossum.manualAttribution({
   packageType: undefined,
   licenseName: license1.fullName,
 });
-const [wasPreferredAttributionId, wasPreferredPackageInfo] =
-  faker.opossum.manualAttribution({
-    wasPreferred: true,
-  });
 
 test.use({
   data: {
@@ -34,7 +25,6 @@ test.use({
         [resourceName2]: 1,
         [resourceName3]: 1,
         [resourceName4]: 1,
-        [resourceName5]: 1,
       }),
       frequentLicenses: [license1, license2],
     }),
@@ -42,14 +32,12 @@ test.use({
       manualAttributions: faker.opossum.manualAttributions({
         [attributionId1]: packageInfo1,
         [attributionId2]: packageInfo2,
-        [wasPreferredAttributionId]: wasPreferredPackageInfo,
       }),
       resourcesToAttributions: faker.opossum.resourcesToAttributions({
         [faker.opossum.filePath(resourceName1)]: [attributionId1],
         [faker.opossum.filePath(resourceName2)]: [attributionId1],
         [faker.opossum.filePath(resourceName3)]: [attributionId2],
         [faker.opossum.filePath(resourceName4)]: [attributionId2],
-        [faker.opossum.filePath(resourceName5)]: [wasPreferredAttributionId],
       }),
     }),
   },
@@ -285,40 +273,3 @@ test('warns user of unsaved changes if user attempts to navigate away before sav
   await topBar.gotoAuditView();
   await notSavedPopup.assert.isVisible();
 });
-
-test('removes was-preferred status from attribution when user saves changes', async ({
-  attributionDetails,
-  confirmationDialog,
-  resourceBrowser,
-  resourceDetails,
-}) => {
-  await resourceBrowser.goto(resourceName5);
-  await resourceDetails.attributionCard.assert.wasPreferredIconIsVisible(
-    wasPreferredPackageInfo,
-  );
-  await attributionDetails.assert.auditingLabelIsVisible(
-    'previouslyPreferredLabel',
-  );
-
-  await attributionDetails.comment().fill(faker.lorem.sentence());
-  await confirmationDialog.assert.isVisible();
-
-  await confirmationDialog.cancelButton.click();
-  await resourceDetails.attributionCard.assert.wasPreferredIconIsVisible(
-    wasPreferredPackageInfo,
-  );
-
-  await attributionDetails.comment().fill(faker.lorem.sentence());
-  await confirmationDialog.okButton.click();
-  await resourceDetails.attributionCard.assert.wasPreferredIconIsVisible(
-    wasPreferredPackageInfo,
-  );
-  await attributionDetails.assert.auditingLabelIsHidden(
-    'previouslyPreferredLabel',
-  );
-
-  await attributionDetails.saveButton.click();
-  await resourceDetails.attributionCard.assert.wasPreferredIconIsHidden(
-    wasPreferredPackageInfo,
-  );
-});
diff --git a/src/e2e-tests/__tests__/saving-was-preferred-attributions.test.ts b/src/e2e-tests/__tests__/saving-was-preferred-attributions.test.ts
new file mode 100644
index 00000000..bb1fe270
--- /dev/null
+++ b/src/e2e-tests/__tests__/saving-was-preferred-attributions.test.ts
@@ -0,0 +1,124 @@
+// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates
+// SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
+//
+// SPDX-License-Identifier: Apache-2.0
+import { faker, test } from '../utils';
+
+const [
+  resourceName1,
+  resourceName2,
+  resourceName3,
+  resourceName4,
+  resourceName5,
+] = faker.opossum.resourceNames({ count: 5 });
+const [attributionId1, packageInfo1] = faker.opossum.manualAttribution({
+  wasPreferred: true,
+});
+const [attributionId2, packageInfo2] = faker.opossum.manualAttribution({
+  wasPreferred: true,
+});
+const [attributionId3, packageInfo3] = faker.opossum.manualAttribution({
+  wasPreferred: true,
+});
+
+test.use({
+  data: {
+    inputData: faker.opossum.inputData({
+      resources: faker.opossum.resources({
+        [resourceName1]: 1,
+        [resourceName2]: 1,
+        [resourceName3]: 1,
+        [resourceName4]: 1,
+        [resourceName5]: 1,
+      }),
+    }),
+    outputData: faker.opossum.outputData({
+      manualAttributions: faker.opossum.manualAttributions({
+        [attributionId1]: packageInfo1,
+        [attributionId2]: packageInfo2,
+        [attributionId3]: packageInfo3,
+      }),
+      resourcesToAttributions: faker.opossum.resourcesToAttributions({
+        [faker.opossum.filePath(resourceName1)]: [attributionId1],
+        [faker.opossum.filePath(resourceName2)]: [attributionId1],
+        [faker.opossum.filePath(resourceName3)]: [attributionId1],
+        [faker.opossum.filePath(resourceName4)]: [attributionId2],
+        [faker.opossum.filePath(resourceName5)]: [attributionId3],
+      }),
+    }),
+  },
+});
+
+test('removes was-preferred status from attribution when user saves changes', async ({
+  attributionDetails,
+  attributionList,
+  editAttributionPopup,
+  modifyWasPreferredAttributionPopup,
+  notSavedPopup,
+  reportView,
+  resourceBrowser,
+  resourceDetails,
+  topBar,
+}) => {
+  await resourceBrowser.goto(resourceName4);
+  await resourceDetails.attributionCard.assert.wasPreferredIconIsVisible(
+    packageInfo2,
+  );
+  await attributionDetails.assert.auditingLabelIsVisible(
+    'previouslyPreferredLabel',
+  );
+
+  await attributionDetails.comment().fill(faker.lorem.sentence());
+  await attributionDetails.saveButton.click();
+  await modifyWasPreferredAttributionPopup.assert.isVisible();
+
+  await modifyWasPreferredAttributionPopup.cancelButton.click();
+  await resourceDetails.attributionCard.assert.wasPreferredIconIsVisible(
+    packageInfo2,
+  );
+
+  await attributionDetails.saveButton.click();
+  await modifyWasPreferredAttributionPopup.saveButton.click();
+  await resourceDetails.attributionCard.assert.wasPreferredIconIsHidden(
+    packageInfo2,
+  );
+  await attributionDetails.assert.auditingLabelIsHidden(
+    'previouslyPreferredLabel',
+  );
+
+  await resourceBrowser.goto(resourceName2);
+  await resourceDetails.attributionCard.assert.wasPreferredIconIsVisible(
+    packageInfo1,
+  );
+
+  await attributionDetails.comment().fill(faker.lorem.sentence());
+  await topBar.gotoAttributionView();
+  await notSavedPopup.assert.isVisible();
+
+  await notSavedPopup.saveGloballyButton.click();
+  await modifyWasPreferredAttributionPopup.assert.isVisible();
+
+  await modifyWasPreferredAttributionPopup.saveGloballyButton.click();
+  await attributionList.assert.isVisible();
+  await attributionList.attributionCard.assert.wasPreferredIconIsHidden(
+    packageInfo2,
+  );
+  await attributionList.attributionCard.assert.wasPreferredIconIsHidden(
+    packageInfo1,
+  );
+  await attributionList.attributionCard.assert.wasPreferredIconIsVisible(
+    packageInfo3,
+  );
+
+  await topBar.gotoReportView();
+  await reportView.editAttribution(packageInfo3);
+  await attributionDetails.comment().fill(faker.lorem.sentence());
+  await editAttributionPopup.saveButton.click();
+  await modifyWasPreferredAttributionPopup.assert.isVisible();
+
+  await modifyWasPreferredAttributionPopup.saveButton.click();
+  await topBar.gotoAttributionView();
+  await attributionList.attributionCard.assert.wasPreferredIconIsHidden(
+    packageInfo3,
+  );
+});
diff --git a/src/e2e-tests/page-objects/ConfirmationDialog.ts b/src/e2e-tests/page-objects/ModifyWasPreferredAttributionPopup.ts
similarity index 68%
rename from src/e2e-tests/page-objects/ConfirmationDialog.ts
rename to src/e2e-tests/page-objects/ModifyWasPreferredAttributionPopup.ts
index bc302c2c..3f637630 100644
--- a/src/e2e-tests/page-objects/ConfirmationDialog.ts
+++ b/src/e2e-tests/page-objects/ModifyWasPreferredAttributionPopup.ts
@@ -4,21 +4,26 @@
 // SPDX-License-Identifier: Apache-2.0
 import { expect, type Locator, type Page } from '@playwright/test';
 
-export class ConfirmationDialog {
+export class ModifyWasPreferredAttributionPopup {
   private readonly window: Page;
   private readonly node: Locator;
   readonly cancelButton: Locator;
-  readonly okButton: Locator;
+  readonly saveButton: Locator;
+  readonly saveGloballyButton: Locator;
 
   constructor(window: Page) {
     this.window = window;
-    this.node = window.getByLabel('confirmation dialog');
+    this.node = window.getByLabel('modify was preferred attribution popup');
     this.cancelButton = this.node.getByRole('button', {
       name: 'Cancel',
       exact: true,
     });
-    this.okButton = this.node.getByRole('button', {
-      name: 'Ok',
+    this.saveButton = this.node.getByRole('button', {
+      name: 'Save',
+      exact: true,
+    });
+    this.saveGloballyButton = this.node.getByRole('button', {
+      name: 'Save globally',
       exact: true,
     });
   }
diff --git a/src/e2e-tests/utils/fixtures.ts b/src/e2e-tests/utils/fixtures.ts
index bfac347e..e87f2737 100644
--- a/src/e2e-tests/utils/fixtures.ts
+++ b/src/e2e-tests/utils/fixtures.ts
@@ -22,13 +22,13 @@ import { AttributionDetails } from '../page-objects/AttributionDetails';
 import { AttributionFilters } from '../page-objects/AttributionFilters';
 import { AttributionList } from '../page-objects/AttributionList';
 import { ChangePreferredStatusGloballyPopup } from '../page-objects/ChangePreferredStatusGloballyPopup';
-import { ConfirmationDialog } from '../page-objects/ConfirmationDialog';
 import { ConfirmationPopup } from '../page-objects/ConfirmationPopup';
 import { EditAttributionPopup } from '../page-objects/EditAttributionPopup';
 import { ErrorPopup } from '../page-objects/ErrorPopup';
 import { FileSearchPopup } from '../page-objects/FileSearchPopup';
 import { FileSupportPopup } from '../page-objects/FileSupportPopup';
 import { MenuBar } from '../page-objects/MenuBar';
+import { ModifyWasPreferredAttributionPopup } from '../page-objects/ModifyWasPreferredAttributionPopup';
 import { NotSavedPopup } from '../page-objects/NotSavedPopup';
 import { ProjectMetadataPopup } from '../page-objects/ProjectMetadataPopup';
 import { ProjectStatisticsPopup } from '../page-objects/ProjectStatisticsPopup';
@@ -58,7 +58,6 @@ export const test = base.extend<{
   attributionFilters: AttributionFilters;
   attributionList: AttributionList;
   changePreferredStatusGloballyPopup: ChangePreferredStatusGloballyPopup;
-  confirmationDialog: ConfirmationDialog;
   confirmationPopup: ConfirmationPopup;
   editAttributionPopup: EditAttributionPopup;
   errorPopup: ErrorPopup;
@@ -66,6 +65,7 @@ export const test = base.extend<{
   fileSupportPopup: FileSupportPopup;
   menuBar: MenuBar;
   modKey: string;
+  modifyWasPreferredAttributionPopup: ModifyWasPreferredAttributionPopup;
   notSavedPopup: NotSavedPopup;
   projectMetadataPopup: ProjectMetadataPopup;
   projectStatisticsPopup: ProjectStatisticsPopup;
@@ -166,8 +166,8 @@ export const test = base.extend<{
   reportView: async ({ window }, use) => {
     await use(new ReportView(window));
   },
-  confirmationDialog: async ({ window }, use) => {
-    await use(new ConfirmationDialog(window));
+  modifyWasPreferredAttributionPopup: async ({ window }, use) => {
+    await use(new ModifyWasPreferredAttributionPopup(window));
   },
   notSavedPopup: async ({ window }, use) => {
     await use(new NotSavedPopup(window));
diff --git a/src/shared/setupTests.ts b/src/shared/setupTests.ts
index 44cb233a..78fa0b2c 100644
--- a/src/shared/setupTests.ts
+++ b/src/shared/setupTests.ts
@@ -4,9 +4,7 @@
 //
 // SPDX-License-Identifier: Apache-2.0
 import '@testing-library/jest-dom';
-import { noop } from 'lodash';
 
-import { faker } from './Faker';
 import { ElectronAPI } from './shared-types';
 
 // We suppress the recharts warning that is due to our mocking in tests.
@@ -21,45 +19,60 @@ const SUBSTRINGS_TO_SUPPRESS_IN_CONSOLE_ERROR = [
 
 jest.mock('../ElectronBackend/main/logger.ts');
 
+const originalResizeObserver = window.ResizeObserver;
 const originalConsoleError = console.error;
 const originalConsoleWarn = console.warn;
 const originalConsoleInfo = console.info;
 
-class ResizeObserver {
-  observe = noop;
-  unobserve = noop;
-  disconnect = noop;
+function mockResizeObserver(): void {
+  window.ResizeObserver = jest.fn().mockImplementation(() => ({
+    observe: jest.fn(),
+    unobserve: jest.fn(),
+    disconnect: jest.fn(),
+  }));
 }
 
-global.window.electronAPI = {
-  openLink: jest.fn().mockReturnValue(Promise.resolve()),
-  openFile: jest.fn(),
-  deleteFile: jest.fn(),
-  keepFile: jest.fn(),
-  convertInputFileToDotOpossum: jest.fn(),
-  useOutdatedInputFileFormat: jest.fn(),
-  openDotOpossumFile: jest.fn(),
-  sendErrorInformation: jest.fn(),
-  exportFile: jest.fn(),
-  saveFile: jest.fn(),
-  on: jest.fn().mockReturnValue(jest.fn()),
-  getUserSetting: jest.fn().mockReturnValue(undefined),
-  setUserSetting: jest.fn(),
-} satisfies ElectronAPI;
+beforeAll(() => {
+  global.window.electronAPI = {
+    openLink: jest.fn().mockReturnValue(Promise.resolve()),
+    openFile: jest.fn(),
+    deleteFile: jest.fn(),
+    keepFile: jest.fn(),
+    convertInputFileToDotOpossum: jest.fn(),
+    useOutdatedInputFileFormat: jest.fn(),
+    openDotOpossumFile: jest.fn(),
+    sendErrorInformation: jest.fn(),
+    exportFile: jest.fn(),
+    saveFile: jest.fn(),
+    on: jest.fn().mockReturnValue(jest.fn()),
+    getUserSetting: jest.fn().mockReturnValue(undefined),
+    setUserSetting: jest.fn(),
+  } satisfies ElectronAPI;
 
-window.ResizeObserver = ResizeObserver;
+  mockResizeObserver();
 
-console.warn = getMockConsoleImplementation(
-  SUBSTRINGS_TO_SUPPRESS_IN_CONSOLE_WARN,
-  'warn',
-);
-console.error = getMockConsoleImplementation(
-  SUBSTRINGS_TO_SUPPRESS_IN_CONSOLE_ERROR,
-  'error',
-);
-faker.packageSearch.usePackageNames();
-faker.packageSearch.usePackageNamespaces();
-faker.packageSearch.usePackageVersions();
+  mockConsoleImplementation(SUBSTRINGS_TO_SUPPRESS_IN_CONSOLE_WARN, 'warn');
+  mockConsoleImplementation(SUBSTRINGS_TO_SUPPRESS_IN_CONSOLE_ERROR, 'error');
+});
+
+beforeEach(() => jest.clearAllMocks());
+
+afterAll(() => {
+  window.ResizeObserver = originalResizeObserver;
+  console.error = originalConsoleError;
+  console.warn = originalConsoleWarn;
+  console.info = originalConsoleInfo;
+  jest.restoreAllMocks();
+});
+
+function mockConsoleImplementation(
+  selectors: Array<string>,
+  type: 'info' | 'warn' | 'error',
+): void {
+  jest
+    .spyOn(console, type)
+    .mockImplementation(getMockConsoleImplementation(selectors, type));
+}
 
 function getMockConsoleImplementation(
   selectors: Array<string>,