Repository: opossum-tool/OpossumUI
Pull request #2520 (2024-01-23 10:49:17)

[STORY] Create Diff Popup
### Summary of changes

<!-- required -->

### Context and reason for change

#2488 

### How can the changes be tested

<!-- optional -->

Note: Please review the [guidelines for contributing](https://github.com/opossum-tool/OpossumUI/blob/main/CONTRIBUTING.md) to this repository.



Patch:
diff --git a/src/Frontend/Components/AttributionColumn/AttributionForm.tsx b/src/Frontend/Components/AttributionColumn/AttributionForm.tsx
index 7dcbd2bc..d73e22b4 100644
--- a/src/Frontend/Components/AttributionColumn/AttributionForm.tsx
+++ b/src/Frontend/Components/AttributionColumn/AttributionForm.tsx
@@ -13,7 +13,9 @@ import { text } from '../../../shared/text';
 import { AttributionType } from '../../enums/enums';
 import { setTemporaryDisplayPackageInfo } from '../../state/actions/resource-actions/all-views-simple-actions';
 import { useAppDispatch } from '../../state/hooks';
+import { FormAttribute } from '../../util/get-comparable-attributes';
 import { Confirm } from '../ConfirmationDialog/ConfirmationDialog';
+import { InputElementProps } from '../InputElements/shared';
 import { AuditingOptions } from './AuditingOptions';
 import { Comment } from './Comment';
 import { CopyrightSubPanel } from './CopyrightSubPanel';
@@ -29,81 +31,126 @@ const classes = {
     gap: '12px',
     overflow: 'hidden auto',
   },
+  attributionTypeContainer: {
+    position: 'relative',
+  },
 };
 
+export interface AttributeConfig
+  extends Pick<InputElementProps, 'color' | 'focused' | 'endIcon'> {}
+
+export type AttributionFormConfig = Partial<
+  Record<FormAttribute, AttributeConfig>
+>;
+
 interface AttributionFormProps {
   packageInfo: PackageInfo;
   showHighlight?: boolean;
   onEdit?: Confirm;
+  variant?: 'default' | 'diff';
+  label?: string;
+  config?: AttributionFormConfig;
 }
 
-export function AttributionForm(props: AttributionFormProps) {
+export function AttributionForm({
+  packageInfo,
+  label,
+  onEdit,
+  showHighlight,
+  variant = 'default',
+  config,
+}: AttributionFormProps) {
   const dispatch = useAppDispatch();
+  const isDiff = variant === 'diff';
 
   return (
-    <MuiBox sx={classes.formContainer}>
-      <AuditingOptions
-        packageInfo={props.packageInfo}
-        isEditable={!!props.onEdit}
-      />
+    <MuiBox sx={classes.formContainer} aria-label={label}>
+      {!isDiff && (
+        <AuditingOptions packageInfo={packageInfo} isEditable={!!onEdit} />
+      )}
       <MuiDivider variant={'middle'}>
         <MuiTypography>
           {text.attributionColumn.packageCoordinates}
         </MuiTypography>
       </MuiDivider>
       <PackageSubPanel
-        packageInfo={props.packageInfo}
-        showHighlight={props.showHighlight}
-        onEdit={props.onEdit}
+        packageInfo={packageInfo}
+        showHighlight={showHighlight}
+        onEdit={onEdit}
+        isDiff={isDiff}
+        config={config}
       />
       <MuiDivider variant={'middle'}>
         <MuiTypography>{text.attributionColumn.legalInformation}</MuiTypography>
       </MuiDivider>
       {renderAttributionType()}
-      {props.packageInfo.firstParty ? null : (
-        <>
-          <CopyrightSubPanel
-            packageInfo={props.packageInfo}
-            showHighlight={props.showHighlight}
-            onEdit={props.onEdit}
-          />
-          <LicenseSubPanel
-            packageInfo={props.packageInfo}
-            showHighlight={props.showHighlight}
-            onEdit={props.onEdit}
-          />
-        </>
-      )}
-      <Comment packageInfo={props.packageInfo} onEdit={props.onEdit} />
+      <CopyrightSubPanel
+        packageInfo={packageInfo}
+        showHighlight={showHighlight}
+        onEdit={onEdit}
+        expanded={isDiff}
+        hidden={isDiff ? false : packageInfo.firstParty}
+        config={config?.copyright}
+      />
+      <LicenseSubPanel
+        packageInfo={packageInfo}
+        showHighlight={showHighlight}
+        onEdit={onEdit}
+        expanded={isDiff}
+        hidden={isDiff ? false : packageInfo.firstParty}
+        config={config}
+      />
+      <Comment
+        packageInfo={packageInfo}
+        onEdit={onEdit}
+        expanded={isDiff}
+        config={config?.comment}
+      />
     </MuiBox>
   );
 
   function renderAttributionType() {
     return (
-      <MuiToggleButtonGroup
-        value={props.packageInfo.firstParty || false}
-        exclusive
-        onChange={(_, newValue) =>
-          props.onEdit?.(() =>
-            dispatch(
-              setTemporaryDisplayPackageInfo({
-                ...props.packageInfo,
-                firstParty: newValue,
-                wasPreferred: undefined,
-              }),
-            ),
-          )
-        }
-        size={'small'}
-        fullWidth
-      >
-        <MuiToggleButton value={false} disableRipple>
-          {AttributionType.ThirdParty}
-        </MuiToggleButton>
-        <MuiToggleButton value={true} disableRipple>
-          {AttributionType.FirstParty}
-        </MuiToggleButton>
-      </MuiToggleButtonGroup>
+      <MuiBox sx={classes.attributionTypeContainer}>
+        <MuiToggleButtonGroup
+          value={packageInfo.firstParty || false}
+          exclusive
+          onChange={(_, newValue) =>
+            onEdit?.(() =>
+              dispatch(
+                setTemporaryDisplayPackageInfo({
+                  ...packageInfo,
+                  firstParty: newValue,
+                  wasPreferred: undefined,
+                }),
+              ),
+            )
+          }
+          size={'small'}
+          fullWidth
+          disabled={!onEdit}
+          color={config?.firstParty?.color}
+        >
+          <MuiToggleButton value={false} disableRipple>
+            {AttributionType.ThirdParty}
+          </MuiToggleButton>
+          <MuiToggleButton value={true} disableRipple>
+            {AttributionType.FirstParty}
+          </MuiToggleButton>
+        </MuiToggleButtonGroup>
+        {!config?.firstParty?.endIcon ? null : (
+          <MuiBox
+            sx={{
+              position: 'absolute',
+              left: '50%',
+              top: '50%',
+              transform: 'translate(-50%, -50%)',
+            }}
+          >
+            {config.firstParty.endIcon}
+          </MuiBox>
+        )}
+      </MuiBox>
     );
   }
 }
diff --git a/src/Frontend/Components/AttributionColumn/AuditingOptions.tsx b/src/Frontend/Components/AttributionColumn/AuditingOptions.tsx
index 899b3d16..6adf9982 100644
--- a/src/Frontend/Components/AttributionColumn/AuditingOptions.tsx
+++ b/src/Frontend/Components/AttributionColumn/AuditingOptions.tsx
@@ -5,7 +5,7 @@
 import AddIcon from '@mui/icons-material/Add';
 import MuiBox from '@mui/material/Box';
 import MuiChip from '@mui/material/Chip';
-import { SxProps } from '@mui/system';
+import { SxProps, Theme } from '@mui/system';
 import { useState } from 'react';
 
 import { PackageInfo } from '../../../shared/shared-types';
@@ -21,9 +21,10 @@ const classes = {
 interface Props {
   packageInfo: PackageInfo;
   isEditable: boolean;
+  sx?: SxProps<Theme>;
 }
 
-export function AuditingOptions({ packageInfo, isEditable }: Props) {
+export function AuditingOptions({ packageInfo, isEditable, sx }: Props) {
   const options = useAuditingOptions({ packageInfo, isEditable });
   const [anchorEl, setAnchorEl] = useState<HTMLElement>();
   const unselectedOptions = options.filter(
@@ -32,7 +33,7 @@ export function AuditingOptions({ packageInfo, isEditable }: Props) {
 
   return options.length ? (
     <>
-      <MuiBox sx={classes.container}>
+      <MuiBox sx={{ ...classes.container, ...sx }}>
         {renderTriggerButton()}
         {renderSelectedOptions()}
       </MuiBox>
diff --git a/src/Frontend/Components/AttributionColumn/AuditingOptions.util.tsx b/src/Frontend/Components/AttributionColumn/AuditingOptions.util.tsx
index e026ede8..f07ae1b9 100644
--- a/src/Frontend/Components/AttributionColumn/AuditingOptions.util.tsx
+++ b/src/Frontend/Components/AttributionColumn/AuditingOptions.util.tsx
@@ -4,13 +4,13 @@
 // SPDX-License-Identifier: Apache-2.0
 
 /* eslint-disable @typescript-eslint/no-magic-numbers */
-import ReplayIcon from '@mui/icons-material/Replay';
 import SentimentDissatisfiedIcon from '@mui/icons-material/SentimentDissatisfied';
 import SentimentSatisfiedIcon from '@mui/icons-material/SentimentSatisfied';
 import SentimentSatisfiedAltIcon from '@mui/icons-material/SentimentSatisfiedAltOutlined';
 import SentimentVeryDissatisfiedIcon from '@mui/icons-material/SentimentVeryDissatisfied';
 import SentimentVerySatisfiedIcon from '@mui/icons-material/SentimentVerySatisfied';
 import MuiRating from '@mui/material/Rating';
+import { isEqual } from 'lodash';
 import { useMemo } from 'react';
 
 import { PackageInfo } from '../../../shared/shared-types';
@@ -24,6 +24,7 @@ import {
   getIsPreferenceFeatureEnabled,
   getTemporaryDisplayPackageInfo,
 } from '../../state/selectors/all-views-resource-selectors';
+import { getComparableAttributes } from '../../util/get-comparable-attributes';
 import { prettifySource } from '../../util/prettify-source';
 import { useUserSetting } from '../../util/use-user-setting';
 import {
@@ -92,12 +93,18 @@ export function useAuditingOptions({
     () =>
       !!packageInfo.originIds?.length && !packageInfo.wasPreferred
         ? Object.values(externalAttributions).find(
-            ({ originIds, wasPreferred }) =>
-              wasPreferred &&
-              originIds?.some((id) => packageInfo.originIds?.includes(id)),
+            (candidate) =>
+              candidate.wasPreferred &&
+              candidate.originIds?.some((id) =>
+                packageInfo.originIds?.includes(id),
+              ) &&
+              !isEqual(
+                getComparableAttributes(candidate),
+                getComparableAttributes(packageInfo),
+              ),
           )
         : undefined,
-    [externalAttributions, packageInfo.originIds, packageInfo.wasPreferred],
+    [externalAttributions, packageInfo],
   );
 
   return useMemo<Array<AuditingOption>>(
@@ -121,7 +128,7 @@ export function useAuditingOptions({
               preferred: false,
             }),
           ),
-        interactive: isPreferenceFeatureEnabled && qaMode,
+        interactive: isPreferenceFeatureEnabled && qaMode && isEditable,
       },
       {
         id: 'was-preferred',
@@ -135,18 +142,7 @@ export function useAuditingOptions({
         label: text.auditingOptions.modifiedPreferred,
         icon: <ModifiedPreferredIcon noTooltip />,
         selected: !!originalPreferred,
-        interactive: !!originalPreferred,
-        deleteIcon: <ReplayIcon aria-label={'undo modified preferred'} />,
-        onDelete: originalPreferred
-          ? () =>
-              dispatch(
-                setTemporaryDisplayPackageInfo({
-                  ...originalPreferred,
-                  id: packageInfo.id,
-                  preSelected: undefined,
-                }),
-              )
-          : undefined,
+        interactive: false,
       },
       {
         id: 'pre-selected',
@@ -278,7 +274,6 @@ export function useAuditingOptions({
       packageInfo.attributionConfidence,
       packageInfo.excludeFromNotice,
       packageInfo.followUp,
-      packageInfo.id,
       packageInfo.needsReview,
       packageInfo.preSelected,
       packageInfo.preferred,
diff --git a/src/Frontend/Components/AttributionColumn/ButtonRow.tsx b/src/Frontend/Components/AttributionColumn/ButtonRow.tsx
index 81ec97ea..c53311c8 100644
--- a/src/Frontend/Components/AttributionColumn/ButtonRow.tsx
+++ b/src/Frontend/Components/AttributionColumn/ButtonRow.tsx
@@ -3,19 +3,23 @@
 //
 // SPDX-License-Identifier: Apache-2.0
 import MuiBox from '@mui/material/Box';
+import { useMemo, useState } from 'react';
 
 import { PackageInfo } from '../../../shared/shared-types';
+import { text } from '../../../shared/text';
 import { ButtonText } from '../../enums/enums';
 import { EMPTY_DISPLAY_PACKAGE_INFO } from '../../shared-constants';
 import { setTemporaryDisplayPackageInfo } from '../../state/actions/resource-actions/all-views-simple-actions';
 import { useAppDispatch, useAppSelector } from '../../state/hooks';
 import {
+  getExternalAttributions,
   getIsGlobalSavingDisabled,
   getIsSavingDisabled,
   getManualDisplayPackageInfoOfSelected,
   wereTemporaryDisplayPackageInfoModified,
 } from '../../state/selectors/all-views-resource-selectors';
 import { Button, ButtonProps } from '../Button/Button';
+import { DiffPopup } from '../DiffPopup/DiffPopup';
 import { SplitButton } from '../SplitButton/SplitButton';
 
 interface ButtonRowProps {
@@ -50,6 +54,18 @@ export function ButtonRow({
   );
   const isSavingDisabled = useAppSelector(getIsSavingDisabled);
   const isGlobalSavingDisabled = useAppSelector(getIsGlobalSavingDisabled);
+  const externalAttributions = useAppSelector(getExternalAttributions);
+  const [isDiffPopupOpen, setIsDiffPopupOpen] = useState(false);
+
+  const originalDisplayPackageInfo = useMemo(
+    () =>
+      !!packageInfo.originIds?.length
+        ? Object.values(externalAttributions).find(({ originIds }) =>
+            originIds?.some((id) => packageInfo.originIds?.includes(id)),
+          )
+        : undefined,
+    [externalAttributions, packageInfo.originIds],
+  );
 
   return (
     !areButtonsHidden && (
@@ -64,7 +80,9 @@ export function ButtonRow({
         {renderSaveButton()}
         {renderDeleteButton()}
         {renderRevertButton()}
+        {renderCompareButton()}
         {renderAdditionalActions()}
+        {renderDiffPopup()}
       </MuiBox>
     )
   );
@@ -142,6 +160,16 @@ export function ButtonRow({
     );
   }
 
+  function renderCompareButton() {
+    return originalDisplayPackageInfo ? (
+      <Button
+        onClick={() => setIsDiffPopupOpen(true)}
+        color={'secondary'}
+        buttonText={text.buttons.compareToOriginal}
+      />
+    ) : null;
+  }
+
   function renderAdditionalActions() {
     return additionalActions.map((action) => (
       <Button
@@ -153,4 +181,16 @@ export function ButtonRow({
       />
     ));
   }
+
+  function renderDiffPopup() {
+    return originalDisplayPackageInfo ? (
+      <DiffPopup
+        original={originalDisplayPackageInfo}
+        current={packageInfo}
+        isOpen={isDiffPopupOpen}
+        setOpen={setIsDiffPopupOpen}
+        key={isDiffPopupOpen.toString()}
+      />
+    ) : null;
+  }
 }
diff --git a/src/Frontend/Components/AttributionColumn/Comment.tsx b/src/Frontend/Components/AttributionColumn/Comment.tsx
index 5ef35a82..cff35e91 100644
--- a/src/Frontend/Components/AttributionColumn/Comment.tsx
+++ b/src/Frontend/Components/AttributionColumn/Comment.tsx
@@ -9,25 +9,31 @@ import { setTemporaryDisplayPackageInfo } from '../../state/actions/resource-act
 import { useAppDispatch } from '../../state/hooks';
 import { Confirm } from '../ConfirmationDialog/ConfirmationDialog';
 import { TextBox } from '../InputElements/TextBox';
+import { AttributeConfig } from './AttributionForm';
 import { attributionColumnClasses } from './shared-attribution-column-styles';
 
 interface Props {
   packageInfo: PackageInfo;
   onEdit?: Confirm;
+  expanded?: boolean;
+  config?: AttributeConfig;
 }
 
-export function Comment({ packageInfo, onEdit }: Props) {
+export function Comment({ packageInfo, onEdit, config, expanded }: Props) {
   const dispatch = useAppDispatch();
 
   return (
     <MuiBox sx={attributionColumnClasses.panel}>
       <TextBox
-        isEditable={!!onEdit}
+        readOnly={!onEdit}
         title={'Comment'}
         text={packageInfo.comment}
         minRows={3}
         maxRows={10}
         multiline={true}
+        color={config?.color}
+        focused={config?.focused}
+        expanded={expanded}
         handleChange={(event) =>
           onEdit?.(() =>
             dispatch(
@@ -39,6 +45,7 @@ export function Comment({ packageInfo, onEdit }: Props) {
             ),
           )
         }
+        endIcon={config?.endIcon}
       />
     </MuiBox>
   );
diff --git a/src/Frontend/Components/AttributionColumn/CopyrightSubPanel.tsx b/src/Frontend/Components/AttributionColumn/CopyrightSubPanel.tsx
index c8787f03..4581020c 100644
--- a/src/Frontend/Components/AttributionColumn/CopyrightSubPanel.tsx
+++ b/src/Frontend/Components/AttributionColumn/CopyrightSubPanel.tsx
@@ -3,7 +3,6 @@
 //
 // SPDX-License-Identifier: Apache-2.0
 import MuiBox from '@mui/material/Box';
-import { ReactElement } from 'react';
 
 import { PackageInfo } from '../../../shared/shared-types';
 import { setTemporaryDisplayPackageInfo } from '../../state/actions/resource-actions/all-views-simple-actions';
@@ -11,31 +10,41 @@ import { useAppDispatch } from '../../state/hooks';
 import { isImportantAttributionInformationMissing } from '../../util/is-important-attribution-information-missing';
 import { Confirm } from '../ConfirmationDialog/ConfirmationDialog';
 import { TextBox } from '../InputElements/TextBox';
+import { AttributeConfig } from './AttributionForm';
 import { attributionColumnClasses } from './shared-attribution-column-styles';
 
 interface CopyrightSubPanelProps {
   packageInfo: PackageInfo;
   showHighlight?: boolean;
   onEdit?: Confirm;
+  expanded?: boolean;
+  hidden?: boolean;
+  config?: AttributeConfig;
 }
 
 export function CopyrightSubPanel({
   packageInfo,
   onEdit,
   showHighlight,
-}: CopyrightSubPanelProps): ReactElement {
+  expanded,
+  hidden,
+  config,
+}: CopyrightSubPanelProps) {
   const dispatch = useAppDispatch();
 
-  return (
+  return hidden ? null : (
     <MuiBox sx={attributionColumnClasses.panel}>
       <TextBox
-        isEditable={!!onEdit}
+        readOnly={!onEdit}
         sx={attributionColumnClasses.textBox}
         title={'Copyright'}
         text={packageInfo.copyright}
         minRows={3}
-        maxRows={10}
-        multiline={true}
+        maxRows={7}
+        color={config?.color}
+        focused={config?.focused}
+        multiline
+        expanded={expanded}
         handleChange={({ target: { value } }) =>
           onEdit?.(() =>
             dispatch(
@@ -51,6 +60,7 @@ export function CopyrightSubPanel({
           showHighlight &&
           isImportantAttributionInformationMissing('copyright', packageInfo)
         }
+        endIcon={config?.endIcon}
       />
     </MuiBox>
   );
diff --git a/src/Frontend/Components/AttributionColumn/LicenseSubPanel.tsx b/src/Frontend/Components/AttributionColumn/LicenseSubPanel.tsx
index 6fe7de75..f497e549 100644
--- a/src/Frontend/Components/AttributionColumn/LicenseSubPanel.tsx
+++ b/src/Frontend/Components/AttributionColumn/LicenseSubPanel.tsx
@@ -9,7 +9,7 @@ import MuiAccordionSummary from '@mui/material/AccordionSummary';
 import MuiBox from '@mui/material/Box';
 import MuiInputAdornment from '@mui/material/InputAdornment';
 import { sortBy } from 'lodash';
-import { ReactElement, useMemo, useState } from 'react';
+import { useMemo, useState } from 'react';
 
 import { PackageInfo } from '../../../shared/shared-types';
 import { text } from '../../../shared/text';
@@ -19,6 +19,7 @@ import { useAppDispatch, useAppSelector } from '../../state/hooks';
 import { getFrequentLicensesNameOrder } from '../../state/selectors/all-views-resource-selectors';
 import { Confirm } from '../ConfirmationDialog/ConfirmationDialog';
 import { TextBox } from '../InputElements/TextBox';
+import { AttributionFormConfig } from './AttributionForm';
 import { PackageAutocomplete } from './PackageAutocomplete';
 import { attributionColumnClasses } from './shared-attribution-column-styles';
 
@@ -45,6 +46,9 @@ const classes = {
     padding: '0px',
     width: 'calc(100% - 36px)',
   },
+  expansionPanelDetailsDiffView: {
+    padding: '0px',
+  },
   expandMoreIcon: {
     display: 'flex',
     alignItems: 'center',
@@ -65,15 +69,21 @@ interface LicenseSubPanelProps {
   packageInfo: PackageInfo;
   showHighlight?: boolean;
   onEdit?: Confirm;
+  expanded?: boolean;
+  hidden?: boolean;
+  config?: AttributionFormConfig;
 }
 
 export function LicenseSubPanel({
   packageInfo,
   showHighlight,
   onEdit,
-}: LicenseSubPanelProps): ReactElement {
+  expanded: expandedOverride,
+  hidden,
+  config,
+}: LicenseSubPanelProps) {
   const dispatch = useAppDispatch();
-  const [expanded, setExpanded] = useState(false);
+  const [expanded, setExpanded] = useState(expandedOverride);
   const frequentLicensesNames = useAppSelector(getFrequentLicensesNameOrder);
   const defaultLicenses = useMemo(
     () =>
@@ -109,7 +119,7 @@ export function LicenseSubPanel({
     [frequentLicensesNames, onEdit, packageInfo.licenseName],
   );
 
-  return (
+  return hidden ? null : (
     <MuiBox sx={classes.panel}>
       <MuiAccordion
         sx={classes.expansionPanel}
@@ -121,37 +131,52 @@ export function LicenseSubPanel({
         <MuiAccordionSummary
           sx={classes.expansionPanelSummary}
           expandIcon={
-            <MuiBox
-              sx={classes.expandMoreIcon}
-              onClick={() => setExpanded((prev) => !prev)}
-            >
-              <ExpandMoreIcon aria-label={'license text toggle'} />
-            </MuiBox>
+            expandedOverride ? null : (
+              <MuiBox
+                sx={classes.expandMoreIcon}
+                onClick={() => setExpanded((prev) => !prev)}
+              >
+                <ExpandMoreIcon aria-label={'license text toggle'} />
+              </MuiBox>
+            )
           }
         >
           <PackageAutocomplete
             attribute={'licenseName'}
             title={text.attributionColumn.licenseName}
-            disabled={!onEdit}
+            packageInfo={packageInfo}
+            readOnly={!onEdit}
             showHighlight={showHighlight}
             onEdit={onEdit}
             endAdornment={
-              packageInfo.licenseText ? (
+              config?.licenseName?.endIcon ||
+              (packageInfo.licenseText ? (
                 <MuiInputAdornment position="end" sx={classes.endAdornment}>
                   {text.attributionColumn.licenseTextModified}
                 </MuiInputAdornment>
-              ) : undefined
+              ) : undefined)
             }
             defaults={defaultLicenses}
+            color={config?.licenseName?.color}
+            focused={config?.licenseName?.focused}
           />
         </MuiAccordionSummary>
-        <MuiAccordionDetails sx={classes.expansionPanelDetails}>
+        <MuiAccordionDetails
+          sx={
+            expandedOverride
+              ? classes.expansionPanelDetailsDiffView
+              : classes.expansionPanelDetails
+          }
+        >
           <TextBox
-            isEditable={!!onEdit}
+            readOnly={!onEdit}
             sx={classes.licenseText}
+            maxRows={7}
             minRows={3}
-            maxRows={10}
-            multiline={true}
+            color={config?.licenseText?.color}
+            focused={config?.licenseText?.focused}
+            multiline
+            expanded={expandedOverride}
             title={label}
             text={packageInfo.licenseText}
             handleChange={({ target: { value } }) =>
@@ -165,6 +190,7 @@ export function LicenseSubPanel({
                 ),
               )
             }
+            endIcon={config?.licenseText?.endIcon}
           />
         </MuiAccordionDetails>
       </MuiAccordion>
diff --git a/src/Frontend/Components/AttributionColumn/PackageAutocomplete.tsx b/src/Frontend/Components/AttributionColumn/PackageAutocomplete.tsx
index ffc7bc79..61e58d2a 100644
--- a/src/Frontend/Components/AttributionColumn/PackageAutocomplete.tsx
+++ b/src/Frontend/Components/AttributionColumn/PackageAutocomplete.tsx
@@ -4,7 +4,7 @@
 // SPDX-License-Identifier: Apache-2.0
 import AddIcon from '@mui/icons-material/Add';
 import OpenInNewIcon from '@mui/icons-material/OpenInNew';
-import { createFilterOptions, styled } from '@mui/material';
+import { createFilterOptions, styled, TextFieldProps } from '@mui/material';
 import MuiBadge from '@mui/material/Badge';
 import MuiChip from '@mui/material/Chip';
 import MuiIconButton from '@mui/material/IconButton';
@@ -17,10 +17,7 @@ import { text } from '../../../shared/text';
 import { clickableIcon } from '../../shared-styles';
 import { setTemporaryDisplayPackageInfo } from '../../state/actions/resource-actions/all-views-simple-actions';
 import { useAppDispatch, useAppSelector } from '../../state/hooks';
-import {
-  getExternalAttributionSources,
-  getTemporaryDisplayPackageInfo,
-} from '../../state/selectors/all-views-resource-selectors';
+import { getExternalAttributionSources } from '../../state/selectors/all-views-resource-selectors';
 import { isAuditViewSelected } from '../../state/selectors/view-selector';
 import { useAutocompleteSignals } from '../../state/variables/use-autocomplete-signals';
 import { generatePurl } from '../../util/handle-purl';
@@ -47,12 +44,16 @@ type AutocompleteAttribute = Extract<
 interface Props {
   title: string;
   attribute: AutocompleteAttribute;
+  packageInfo: PackageInfo;
   highlight?: 'default' | 'dark';
-  endAdornment?: React.ReactElement;
+  endAdornment?: React.ReactElement | Array<React.ReactElement>;
   defaults?: Array<PackageInfo>;
-  disabled: boolean;
+  readOnly?: boolean;
+  disabled?: boolean;
   showHighlight: boolean | undefined;
   onEdit?: Confirm;
+  color?: TextFieldProps['color'];
+  focused?: boolean;
 }
 
 const Badge = styled(MuiBadge)({
@@ -71,16 +72,19 @@ const AddIconButton = styled(MuiIconButton)({
 export function PackageAutocomplete({
   attribute,
   title,
+  packageInfo,
   highlight = 'default',
   endAdornment,
   defaults = [],
+  readOnly,
   disabled,
   showHighlight,
   onEdit,
+  color,
+  focused,
 }: Props) {
   const dispatch = useAppDispatch();
-  const temporaryPackageInfo = useAppSelector(getTemporaryDisplayPackageInfo);
-  const attributeValue = temporaryPackageInfo[attribute] || '';
+  const attributeValue = packageInfo[attribute] || '';
   const [inputValue, setInputValue] = useState(attributeValue);
   const sources = useAppSelector(getExternalAttributionSources);
   const isAuditView = useAppSelector(isAuditViewSelected);
@@ -108,16 +112,15 @@ export function PackageAutocomplete({
     <Autocomplete
       title={title}
       disabled={disabled}
+      readOnly={readOnly}
       autoHighlight
       disableClearable
       freeSolo
       inputValue={inputValue}
+      inputProps={{ color, focused }}
       highlight={
         showHighlight &&
-        isImportantAttributionInformationMissing(
-          attribute,
-          temporaryPackageInfo,
-        )
+        isImportantAttributionInformationMissing(attribute, packageInfo)
           ? highlight
           : undefined
       }
@@ -137,7 +140,7 @@ export function PackageAutocomplete({
       }
       renderOptionStartIcon={renderOptionStartIcon}
       renderOptionEndIcon={renderOptionEndIcon}
-      value={temporaryPackageInfo}
+      value={packageInfo}
       filterOptions={createFilterOptions({
         stringify: (option) => {
           switch (attribute) {
@@ -185,11 +188,11 @@ export function PackageAutocomplete({
       }}
       onInputChange={(event, value) =>
         event &&
-        temporaryPackageInfo[attribute] !== value &&
+        packageInfo[attribute] !== value &&
         onEdit?.(() => {
           dispatch(
             setTemporaryDisplayPackageInfo({
-              ...temporaryPackageInfo,
+              ...packageInfo,
               [attribute]: value,
               wasPreferred: undefined,
             }),
@@ -258,7 +261,7 @@ export function PackageAutocomplete({
           onClick={async (event) => {
             event.stopPropagation();
             const merged: PackageInfo = {
-              ...temporaryPackageInfo,
+              ...packageInfo,
               ...omit(option, ['preSelected', 'id']),
             };
             dispatch(
diff --git a/src/Frontend/Components/AttributionColumn/PackageSubPanel.tsx b/src/Frontend/Components/AttributionColumn/PackageSubPanel.tsx
index 06068833..d824671d 100644
--- a/src/Frontend/Components/AttributionColumn/PackageSubPanel.tsx
+++ b/src/Frontend/Components/AttributionColumn/PackageSubPanel.tsx
@@ -24,6 +24,7 @@ import { Confirm } from '../ConfirmationDialog/ConfirmationDialog';
 import { IconButton } from '../IconButton/IconButton';
 import { TextBox } from '../InputElements/TextBox';
 import { toast } from '../Toaster';
+import { AttributionFormConfig } from './AttributionForm';
 import { PackageAutocomplete } from './PackageAutocomplete';
 import { attributionColumnClasses } from './shared-attribution-column-styles';
 
@@ -68,12 +69,16 @@ interface PackageSubPanelProps {
   packageInfo: PackageInfo;
   showHighlight?: boolean;
   onEdit?: Confirm;
+  isDiff?: boolean;
+  config?: AttributionFormConfig;
 }
 
 export function PackageSubPanel({
   packageInfo,
   showHighlight,
   onEdit,
+  isDiff,
+  config,
 }: PackageSubPanelProps) {
   const dispatch = useAppDispatch();
   const defaultPackageTypes = useMemo(
@@ -128,11 +133,15 @@ export function PackageSubPanel({
       <PackageAutocomplete
         attribute={'packageName'}
         title={text.attributionColumn.packageSubPanel.packageName}
+        packageInfo={packageInfo}
         highlight={'dark'}
-        disabled={!onEdit}
+        readOnly={!onEdit}
         showHighlight={showHighlight}
         defaults={packageNames}
         onEdit={onEdit}
+        color={config?.packageName?.color}
+        focused={config?.packageName?.focused}
+        endAdornment={config?.packageName?.endIcon}
       />
     );
   }
@@ -142,11 +151,15 @@ export function PackageSubPanel({
       <PackageAutocomplete
         attribute={'packageNamespace'}
         title={text.attributionColumn.packageSubPanel.packageNamespace}
+        packageInfo={packageInfo}
         highlight={'dark'}
-        disabled={!onEdit}
+        readOnly={!onEdit}
         showHighlight={showHighlight}
         defaults={packageNamespaces}
         onEdit={onEdit}
+        color={config?.packageNamespace?.color}
+        focused={config?.packageNamespace?.focused}
+        endAdornment={config?.packageNamespace?.endIcon}
       />
     );
   }
@@ -156,10 +169,14 @@ export function PackageSubPanel({
       <PackageAutocomplete
         attribute={'packageVersion'}
         title={text.attributionColumn.packageSubPanel.packageVersion}
-        disabled={!onEdit}
+        packageInfo={packageInfo}
+        readOnly={!onEdit}
         showHighlight={showHighlight}
         defaults={packageVersions}
         onEdit={onEdit}
+        color={config?.packageVersion?.color}
+        focused={config?.packageVersion?.focused}
+        endAdornment={config?.packageVersion?.endIcon}
       />
     );
   }
@@ -169,11 +186,15 @@ export function PackageSubPanel({
       <PackageAutocomplete
         attribute={'packageType'}
         title={text.attributionColumn.packageSubPanel.packageType}
+        packageInfo={packageInfo}
         highlight={'dark'}
-        disabled={!onEdit}
+        readOnly={!onEdit}
         showHighlight={showHighlight}
         defaults={defaultPackageTypes}
         onEdit={onEdit}
+        color={config?.packageType?.color}
+        focused={config?.packageType?.focused}
+        endAdornment={config?.packageType?.endIcon}
       />
     );
   }
@@ -186,55 +207,67 @@ export function PackageSubPanel({
         sx={attributionColumnClasses.textBox}
         title={text.attributionColumn.packageSubPanel.purl}
         text={purl}
-        isEditable={false}
+        disabled
         endIcon={
-          <>
-            <IconButton
-              tooltipTitle={
-                text.attributionColumn.packageSubPanel.copyToClipboard
-              }
-              tooltipPlacement="left"
-              onClick={async () => {
-                await navigator.clipboard.writeText(purl);
-                toast.success(text.attributionColumn.copyToClipboardSuccess);
-              }}
-              icon={<ContentCopyIcon sx={clickableIcon} />}
-              hidden={!purl}
-              aria-label={
-                text.attributionColumn.packageSubPanel.copyToClipboard
-              }
-            />
-            <IconButton
-              tooltipTitle={
-                text.attributionColumn.packageSubPanel.pasteFromClipboard
-              }
-              hidden={!onEdit}
-              tooltipPlacement="left"
-              onClick={async () => {
-                const parsedPurl = parsePurl(
-                  await navigator.clipboard.readText(),
-                );
-                if (parsedPurl) {
-                  dispatch(
-                    setTemporaryDisplayPackageInfo({
-                      ...packageInfo,
-                      packageName: parsedPurl.name,
-                      packageVersion: parsedPurl.version ?? undefined,
-                      packageType: parsedPurl.type,
-                      packageNamespace: parsedPurl.namespace ?? undefined,
-                    }),
-                  );
-                  toast.success(text.attributionColumn.copyToClipboardSuccess);
-                } else {
-                  toast.error(text.attributionColumn.pasteFromClipboardFailed);
-                }
-              }}
-              icon={<ContentPasteIcon sx={clickableIcon} />}
-              aria-label={
-                text.attributionColumn.packageSubPanel.pasteFromClipboard
-              }
-            />
-          </>
+          isDiff
+            ? undefined
+            : [
+                <IconButton
+                  tooltipTitle={
+                    text.attributionColumn.packageSubPanel.copyToClipboard
+                  }
+                  tooltipPlacement="left"
+                  onClick={async () => {
+                    await navigator.clipboard.writeText(purl);
+                    toast.success(
+                      text.attributionColumn.copyToClipboardSuccess,
+                    );
+                  }}
+                  icon={<ContentCopyIcon sx={clickableIcon} />}
+                  hidden={!purl}
+                  aria-label={
+                    text.attributionColumn.packageSubPanel.copyToClipboard
+                  }
+                  key={text.attributionColumn.packageSubPanel.copyToClipboard}
+                />,
+                <IconButton
+                  tooltipTitle={
+                    text.attributionColumn.packageSubPanel.pasteFromClipboard
+                  }
+                  hidden={!onEdit}
+                  tooltipPlacement="left"
+                  onClick={async () => {
+                    const parsedPurl = parsePurl(
+                      await navigator.clipboard.readText(),
+                    );
+                    if (parsedPurl) {
+                      dispatch(
+                        setTemporaryDisplayPackageInfo({
+                          ...packageInfo,
+                          packageName: parsedPurl.name,
+                          packageVersion: parsedPurl.version ?? undefined,
+                          packageType: parsedPurl.type,
+                          packageNamespace: parsedPurl.namespace ?? undefined,
+                        }),
+                      );
+                      toast.success(
+                        text.attributionColumn.copyToClipboardSuccess,
+                      );
+                    } else {
+                      toast.error(
+                        text.attributionColumn.pasteFromClipboardFailed,
+                      );
+                    }
+                  }}
+                  icon={<ContentPasteIcon sx={clickableIcon} />}
+                  aria-label={
+                    text.attributionColumn.packageSubPanel.pasteFromClipboard
+                  }
+                  key={
+                    text.attributionColumn.packageSubPanel.pasteFromClipboard
+                  }
+                />,
+              ]
         }
       />
     );
@@ -245,11 +278,14 @@ export function PackageSubPanel({
       <PackageAutocomplete
         attribute={'url'}
         title={text.attributionColumn.packageSubPanel.repositoryUrl}
-        disabled={!onEdit}
+        packageInfo={packageInfo}
+        readOnly={!onEdit}
         showHighlight={showHighlight}
         onEdit={onEdit}
+        color={config?.url?.color}
+        focused={config?.url?.focused}
         endAdornment={
-          <>
+          config?.url?.endIcon || [
             <IconButton
               tooltipTitle={text.attributionColumn.getUrlAndLegal}
               tooltipPlacement={'left'}
@@ -260,7 +296,8 @@ export function PackageSubPanel({
                   packageInfo.url &&
                   packageInfo.copyright &&
                   packageInfo.licenseName
-                )
+                ) ||
+                !onEdit
               }
               onClick={() =>
                 onEdit?.(async () => {
@@ -274,7 +311,8 @@ export function PackageSubPanel({
                 })
               }
               icon={<AutoFixHighIcon sx={clickableIcon} />}
-            />
+              key={text.attributionColumn.getUrlAndLegal}
+            />,
             <IconButton
               tooltipTitle={
                 text.attributionColumn.packageSubPanel.openLinkInBrowser
@@ -285,8 +323,9 @@ export function PackageSubPanel({
               icon={
                 <OpenInNewIcon aria-label={'Url icon'} sx={clickableIcon} />
               }
-            />
-          </>
+              key={text.attributionColumn.packageSubPanel.openLinkInBrowser}
+            />,
+          ]
         }
       />
     );
diff --git a/src/Frontend/Components/Autocomplete/Autocomplete.style.tsx b/src/Frontend/Components/Autocomplete/Autocomplete.style.tsx
index 2c9a24fe..e3430f77 100644
--- a/src/Frontend/Components/Autocomplete/Autocomplete.style.tsx
+++ b/src/Frontend/Components/Autocomplete/Autocomplete.style.tsx
@@ -60,6 +60,7 @@ export const Input = styled(MuiTextField, {
       display: 'none',
     },
   },
+  '& .Mui-readOnly:hover fieldset': { borderColor: 'rgba(0, 0, 0, 0.23)' },
 }));
 
 export const StyledPopper = styled((props: PopperProps) => (
diff --git a/src/Frontend/Components/Autocomplete/Autocomplete.tsx b/src/Frontend/Components/Autocomplete/Autocomplete.tsx
index 424db6bd..d366a068 100644
--- a/src/Frontend/Components/Autocomplete/Autocomplete.tsx
+++ b/src/Frontend/Components/Autocomplete/Autocomplete.tsx
@@ -6,7 +6,7 @@ import { SxProps } from '@mui/material';
 import MuiChip from '@mui/material/Chip';
 import MuiFade from '@mui/material/Fade';
 import { IconButtonProps as MuiIconButtonProps } from '@mui/material/IconButton';
-import { InputBaseComponentProps as MuiInputBaseComponentProps } from '@mui/material/InputBase';
+import { TextFieldProps as MuiInputProps } from '@mui/material/TextField';
 import useMuiAutocomplete, {
   AutocompleteHighlightChangeReason,
   AutocompleteInputChangeReason,
@@ -16,6 +16,7 @@ import { compact } from 'lodash';
 import { useCallback, useEffect, useRef, useState } from 'react';
 import { VirtuosoHandle } from 'react-virtuoso';
 
+import { ensureArray } from '../../util/ensure-array';
 import { ClearButton } from '../ClearButton/ClearButton';
 import { PopupIndicator } from '../PopupIndicator/PopupIndicator';
 import {
@@ -44,9 +45,9 @@ type AutocompleteProps<
     | 'renderOptionStartIcon'
   > & {
     ['aria-label']?: string;
-    endAdornment?: React.ReactNode;
+    endAdornment?: React.ReactNode | Array<React.ReactNode>;
     highlight?: 'default' | 'dark';
-    inputProps?: MuiInputBaseComponentProps;
+    inputProps?: MuiInputProps;
     onInputChange?: (
       event: React.SyntheticEvent | undefined,
       value: string,
@@ -75,6 +76,7 @@ export function Autocomplete<
   inputProps: customInputProps,
   multiple,
   optionText,
+  readOnly,
   renderOptionEndIcon,
   renderOptionStartIcon,
   sx,
@@ -137,6 +139,7 @@ export function Autocomplete<
     onHighlightChange,
     onOpen: () => setOpen(true),
     open,
+    readOnly,
     ...props,
   });
 
@@ -145,7 +148,8 @@ export function Autocomplete<
   }, [groupedOptions]);
 
   const hasPopupIndicator = !freeSolo;
-  const hasClearButton = !disableClearable && !disabled && containsValue;
+  const hasClearButton =
+    !disableClearable && !disabled && !readOnly && containsValue;
   const isPopupOpen = !!groupedOptions.length && popupOpen;
 
   const { ref, color, ...inputProps } = getInputProps();
@@ -160,19 +164,27 @@ export function Autocomplete<
       >
         <Input
           {...inputProps}
+          {...customInputProps}
           disabled={disabled}
           label={title}
           highlight={highlight}
           numberOfEndAdornments={
-            compact([hasClearButton, hasPopupIndicator, !!endAdornment]).length
+            compact([
+              hasClearButton,
+              hasPopupIndicator,
+              ...ensureArray(endAdornment),
+            ]).length
           }
           size={'small'}
           InputLabelProps={getInputLabelProps()}
           inputRef={ref}
-          inputProps={customInputProps}
           InputProps={{
             startAdornment: renderStartAdornment(),
             endAdornment: renderEndAdornment(),
+            readOnly,
+            inputProps: {
+              sx: { overflowX: 'hidden', textOverflow: 'ellipsis' },
+            },
           }}
           onKeyDown={(event) => {
             // https://github.com/mui/material-ui/issues/21129
diff --git a/src/Frontend/Components/DiffEndIcon/DiffEndIcon.tsx b/src/Frontend/Components/DiffEndIcon/DiffEndIcon.tsx
new file mode 100644
index 00000000..2e102adc
--- /dev/null
+++ b/src/Frontend/Components/DiffEndIcon/DiffEndIcon.tsx
@@ -0,0 +1,43 @@
+// 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 RedoIcon from '@mui/icons-material/Redo';
+import UndoIcon from '@mui/icons-material/Undo';
+
+import { clickableIcon } from '../../shared-styles';
+import { IconButton } from '../IconButton/IconButton';
+
+const classes = {
+  bigRevertIcon: {
+    ...clickableIcon,
+    width: 24,
+    height: 24,
+    margin: 0,
+  },
+};
+
+interface EndIconProps {
+  variant: 'undo' | 'redo';
+  onClick: () => void;
+  size?: 'small' | 'big';
+  'data-testid'?: string;
+}
+
+export function DiffEndIcon({
+  variant,
+  onClick,
+  'data-testid': dataTestId,
+  size = 'small',
+}: EndIconProps) {
+  const Icon = variant === 'undo' ? UndoIcon : RedoIcon;
+  return (
+    <IconButton
+      onClick={onClick}
+      icon={
+        <Icon sx={size === 'small' ? clickableIcon : classes.bigRevertIcon} />
+      }
+      data-testid={dataTestId}
+    />
+  );
+}
diff --git a/src/Frontend/Components/DiffPopup/DiffPopup.style.tsx b/src/Frontend/Components/DiffPopup/DiffPopup.style.tsx
new file mode 100644
index 00000000..01aa1c32
--- /dev/null
+++ b/src/Frontend/Components/DiffPopup/DiffPopup.style.tsx
@@ -0,0 +1,15 @@
+// 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 { styled } from '@mui/material';
+import MuiBox from '@mui/material/Box';
+
+export const DiffPopupContainer = styled(MuiBox)({
+  display: 'flex',
+  flexDirection: 'row',
+  flex: 1,
+  padding: '6px',
+  gap: '12px',
+  overflow: 'hidden auto',
+});
diff --git a/src/Frontend/Components/DiffPopup/DiffPopup.tsx b/src/Frontend/Components/DiffPopup/DiffPopup.tsx
new file mode 100644
index 00000000..3e8420d6
--- /dev/null
+++ b/src/Frontend/Components/DiffPopup/DiffPopup.tsx
@@ -0,0 +1,142 @@
+// 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 MuiDivider from '@mui/material/Divider';
+import { isEqual } from 'lodash';
+
+import { PackageInfo } from '../../../shared/shared-types';
+import { text } from '../../../shared/text';
+import { setTemporaryDisplayPackageInfo } from '../../state/actions/resource-actions/all-views-simple-actions';
+import { useAppDispatch } from '../../state/hooks';
+import {
+  FORM_ATTRIBUTES,
+  getComparableAttributes,
+} from '../../util/get-comparable-attributes';
+import { AttributionForm } from '../AttributionColumn/AttributionForm';
+import { NotificationPopup } from '../NotificationPopup/NotificationPopup';
+import { DiffPopupContainer } from './DiffPopup.style';
+import { useAttributionFormConfigs } from './DiffPopup.util';
+
+interface DiffPopupProps {
+  original: PackageInfo;
+  current: PackageInfo;
+  isOpen: boolean;
+  setOpen: React.Dispatch<React.SetStateAction<boolean>>;
+}
+
+export function DiffPopup({
+  current,
+  original,
+  isOpen,
+  setOpen,
+}: DiffPopupProps) {
+  const dispatch = useAppDispatch();
+  const {
+    originalFormConfig,
+    bufferFormConfig,
+    bufferPackageInfo,
+    setBufferPackageInfo,
+  } = useAttributionFormConfigs({
+    original,
+    current,
+  });
+
+  function handleApplyChanges({
+    buffer,
+    current,
+    original,
+  }: {
+    original: PackageInfo;
+    current: PackageInfo;
+    buffer: PackageInfo;
+  }) {
+    const markAsWasPreferred =
+      isEqual(
+        getComparableAttributes(buffer),
+        getComparableAttributes(original),
+      ) && original.wasPreferred;
+    const restoreLicenseAndCopyright = current.firstParty && buffer.firstParty;
+    dispatch(
+      setTemporaryDisplayPackageInfo({
+        ...buffer,
+        ...(markAsWasPreferred && { wasPreferred: true }),
+        ...(restoreLicenseAndCopyright && {
+          copyright: current.copyright,
+          licenseName: current.licenseName,
+          licenseText: current.licenseText,
+        }),
+      }),
+    );
+    setOpen(false);
+  }
+
+  return (
+    <NotificationPopup
+      header={text.diffPopup.title}
+      content={renderDiffView()}
+      leftButtonConfig={{
+        disabled: isEqual(
+          getComparableAttributes(bufferPackageInfo),
+          getComparableAttributes(current),
+        ),
+        buttonText: text.buttons.diffPopup.applyChanges,
+        onClick: () => {
+          handleApplyChanges({ original, current, buffer: bufferPackageInfo });
+        },
+      }}
+      centerRightButtonConfig={{
+        disabled: isEqual(
+          getComparableAttributes(bufferPackageInfo),
+          getComparableAttributes(original),
+        ),
+        buttonText: text.buttons.diffPopup.revertAll,
+        onClick: () => {
+          setBufferPackageInfo({
+            ...bufferPackageInfo,
+            ...FORM_ATTRIBUTES.reduce(
+              (acc, attribute) => ({
+                ...acc,
+                [attribute]: original[attribute],
+              }),
+              {},
+            ),
+          });
+        },
+      }}
+      rightButtonConfig={{
+        buttonText: text.buttons.cancel,
+        color: 'secondary',
+        onClick: () => setOpen(false),
+      }}
+      isOpen={isOpen}
+      background={'lightestBlue'}
+      fullWidth={true}
+      aria-label={'diff popup'}
+    />
+  );
+
+  function renderDiffView() {
+    return (
+      <DiffPopupContainer>
+        <AttributionForm
+          packageInfo={original}
+          variant={'diff'}
+          label={'original'}
+          config={originalFormConfig}
+        />
+        <MuiDivider
+          variant={'middle'}
+          flexItem={true}
+          orientation={'vertical'}
+        />
+        <AttributionForm
+          packageInfo={bufferPackageInfo}
+          variant={'diff'}
+          label={'current'}
+          config={bufferFormConfig}
+        />
+      </DiffPopupContainer>
+    );
+  }
+}
diff --git a/src/Frontend/Components/DiffPopup/DiffPopup.util.tsx b/src/Frontend/Components/DiffPopup/DiffPopup.util.tsx
new file mode 100644
index 00000000..433e9b4b
--- /dev/null
+++ b/src/Frontend/Components/DiffPopup/DiffPopup.util.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 { useCallback, useMemo, useState } from 'react';
+
+import { PackageInfo } from '../../../shared/shared-types';
+import {
+  FORM_ATTRIBUTES,
+  FormAttribute,
+} from '../../util/get-comparable-attributes';
+import { AttributionFormConfig } from '../AttributionColumn/AttributionForm';
+import { DiffEndIcon } from '../DiffEndIcon/DiffEndIcon';
+
+export function useAttributionFormConfigs({
+  original,
+  current,
+}: {
+  original: PackageInfo;
+  current: PackageInfo;
+}) {
+  const [bufferPackageInfo, setBufferPackageInfo] = useState(
+    current.firstParty
+      ? {
+          ...current,
+          copyright: undefined,
+          licenseName: undefined,
+          licenseText: undefined,
+        }
+      : current,
+  );
+
+  const getEndIcon = useCallback(
+    ({
+      isChanged,
+      wasChanged,
+      attribute,
+    }: {
+      isChanged: boolean;
+      wasChanged: boolean;
+      attribute: FormAttribute;
+    }) => {
+      if (isChanged && attribute !== 'firstParty') {
+        return (
+          <DiffEndIcon
+            variant={'undo'}
+            onClick={() => {
+              setBufferPackageInfo({
+                ...bufferPackageInfo,
+                [attribute]: original[attribute],
+              });
+            }}
+            data-testid={`${attribute}-undo`}
+          />
+        );
+      }
+      if (isChanged && attribute === 'firstParty') {
+        return (
+          <DiffEndIcon
+            variant={current?.firstParty ? 'undo' : 'redo'}
+            onClick={() => {
+              setBufferPackageInfo({
+                ...bufferPackageInfo,
+                [attribute]: original[attribute],
+                copyright: original['copyright'],
+                licenseName: original['licenseName'],
+                licenseText: original['licenseText'],
+              });
+            }}
+            size={'big'}
+            data-testid={`${attribute}-${current?.firstParty ? 'undo' : 'redo'}`}
+          />
+        );
+      }
+      if (wasChanged && attribute !== 'firstParty') {
+        return (
+          <DiffEndIcon
+            variant={'redo'}
+            onClick={() => {
+              setBufferPackageInfo({
+                ...bufferPackageInfo,
+                [attribute]: current[attribute],
+              });
+            }}
+            data-testid={`${attribute}-redo`}
+          />
+        );
+      }
+      if (wasChanged && attribute === 'firstParty') {
+        return (
+          <DiffEndIcon
+            variant={current?.firstParty ? 'redo' : 'undo'}
+            onClick={() => {
+              if (!bufferPackageInfo.firstParty && current.firstParty) {
+                setBufferPackageInfo({
+                  ...bufferPackageInfo,
+                  [attribute]: current[attribute],
+                  copyright: undefined,
+                  licenseName: undefined,
+                  licenseText: undefined,
+                });
+              } else {
+                setBufferPackageInfo({
+                  ...bufferPackageInfo,
+                  [attribute]: current[attribute],
+                  copyright: current['copyright'],
+                  licenseName: current['licenseName'],
+                  licenseText: current['licenseText'],
+                });
+              }
+            }}
+            size={'big'}
+            data-testid={`${attribute}-${current?.firstParty ? 'redo' : 'undo'}`}
+          />
+        );
+      }
+      return undefined;
+    },
+    [bufferPackageInfo, original, current],
+  );
+
+  const [originalFormConfig, bufferFormConfig] = useMemo(
+    () =>
+      FORM_ATTRIBUTES.reduce<[AttributionFormConfig, AttributionFormConfig]>(
+        ([originalFormConfig, bufferFormConfig], attribute) => {
+          switch (attribute) {
+            case 'copyright':
+            case 'licenseName':
+            case 'licenseText': {
+              const isThirdPartyUnchanged =
+                !original.firstParty === !bufferPackageInfo.firstParty &&
+                !original.firstParty;
+              const isAttributeValueChanged =
+                original[attribute] !== bufferPackageInfo[attribute];
+              const wasThirdPartyUnchanged =
+                !original.firstParty === !current.firstParty &&
+                !original.firstParty;
+              const wasAttributeValueChanged =
+                original[attribute] !== current[attribute];
+              const isChanged =
+                isThirdPartyUnchanged && isAttributeValueChanged;
+              const wasChanged =
+                wasThirdPartyUnchanged && wasAttributeValueChanged;
+              originalFormConfig[attribute] = {
+                color: isChanged ? 'error' : undefined,
+                focused: isChanged,
+              };
+              bufferFormConfig[attribute] = {
+                color: isChanged ? 'success' : undefined,
+                focused: isChanged,
+                endIcon: getEndIcon({
+                  isChanged,
+                  wasChanged,
+                  attribute,
+                }),
+              };
+              break;
+            }
+            case 'comment':
+            case 'packageName':
+            case 'packageNamespace':
+            case 'packageType':
+            case 'packageVersion':
+            case 'url': {
+              const isChanged =
+                (original[attribute] ?? '') !==
+                (bufferPackageInfo[attribute] ?? '');
+              const wasChanged =
+                (original[attribute] ?? '') !== (current[attribute] ?? '');
+              originalFormConfig[attribute] = {
+                color: isChanged ? 'error' : undefined,
+                focused: isChanged,
+              };
+              bufferFormConfig[attribute] = {
+                color: isChanged ? 'success' : undefined,
+                focused: isChanged,
+                endIcon: getEndIcon({
+                  isChanged,
+                  wasChanged,
+                  attribute,
+                }),
+              };
+              break;
+            }
+            case 'firstParty': {
+              const isChanged =
+                !!original.firstParty !== !!bufferPackageInfo.firstParty;
+              const wasChanged = !!original.firstParty !== !!current.firstParty;
+              originalFormConfig[attribute] = {
+                color: isChanged ? 'error' : undefined,
+              };
+              bufferFormConfig[attribute] = {
+                color: isChanged ? 'success' : undefined,
+                endIcon: getEndIcon({
+                  isChanged,
+                  wasChanged,
+                  attribute,
+                }),
+              };
+              break;
+            }
+          }
+          return [originalFormConfig, bufferFormConfig];
+        },
+        [{}, {}],
+      ),
+    [bufferPackageInfo, original, current, getEndIcon],
+  );
+  return {
+    originalFormConfig,
+    bufferFormConfig,
+    bufferPackageInfo,
+    setBufferPackageInfo,
+  };
+}
diff --git a/src/Frontend/Components/IconButton/IconButton.tsx b/src/Frontend/Components/IconButton/IconButton.tsx
index a2badfa5..e4073596 100644
--- a/src/Frontend/Components/IconButton/IconButton.tsx
+++ b/src/Frontend/Components/IconButton/IconButton.tsx
@@ -17,6 +17,7 @@ interface IconButtonProps {
   icon: ReactElement;
   disabled?: boolean;
   hidden?: boolean;
+  'data-testid'?: string;
 }
 
 export function IconButton(props: IconButtonProps) {
@@ -41,6 +42,7 @@ export function IconButton(props: IconButtonProps) {
             props.onClick?.();
           }}
           disabled={props.disabled}
+          data-testid={props['data-testid']}
         >
           {props.icon}
         </MuiButtonBase>
diff --git a/src/Frontend/Components/InputElements/Dropdown.tsx b/src/Frontend/Components/InputElements/Dropdown.tsx
index ad04ce4f..be21dee7 100644
--- a/src/Frontend/Components/InputElements/Dropdown.tsx
+++ b/src/Frontend/Components/InputElements/Dropdown.tsx
@@ -40,7 +40,9 @@ export function Dropdown(props: DropdownProps): ReactElement {
   return (
     <MuiBox sx={props.sx}>
       <MuiTextField
-        disabled={!props.isEditable}
+        disabled={props.disabled}
+        color={props.color}
+        focused={props.focused}
         sx={{
           ...inputElementClasses.textField,
           ...(props.isHighlighted
@@ -54,6 +56,7 @@ export function Dropdown(props: DropdownProps): ReactElement {
           inputProps: {
             'aria-label': props.title,
           },
+          readOnly: props.readOnly,
         }}
         id={`dropdown ${props.title}`}
         value={props.value}
diff --git a/src/Frontend/Components/InputElements/TextBox.tsx b/src/Frontend/Components/InputElements/TextBox.tsx
index ffc1a01f..2dd978d8 100644
--- a/src/Frontend/Components/InputElements/TextBox.tsx
+++ b/src/Frontend/Components/InputElements/TextBox.tsx
@@ -7,18 +7,19 @@ import MuiInputAdornment from '@mui/material/InputAdornment';
 import MuiTextField from '@mui/material/TextField';
 
 import { HighlightingColor } from '../../enums/enums';
+import { ensureArray } from '../../util/ensure-array';
 import { inputElementClasses, InputElementProps } from './shared';
 
-interface TextProps extends InputElementProps {
+interface TextBoxProps extends InputElementProps {
   minRows?: number;
   maxRows?: number;
-  endIcon?: React.ReactElement;
   multiline?: boolean;
   highlightingColor?: HighlightingColor;
   error?: boolean;
+  expanded?: boolean;
 }
 
-export function TextBox(props: TextProps) {
+export function TextBox(props: TextBoxProps) {
   const isDefaultHighlighting =
     props.highlightingColor === HighlightingColor.LightOrange ||
     props.highlightingColor === undefined;
@@ -32,14 +33,17 @@ export function TextBox(props: TextProps) {
   return (
     <MuiBox sx={props.sx}>
       <MuiTextField
-        disabled={!props.isEditable}
+        disabled={props.disabled}
         error={props.error}
         sx={{
           ...(props.isHighlighted ? highlightedStyling : {}),
           ...inputElementClasses.textField,
         }}
         label={props.title}
+        focused={props.focused}
+        color={props.color}
         InputProps={{
+          readOnly: props.readOnly,
           slotProps: {
             root: {
               sx: {
@@ -52,17 +56,23 @@ export function TextBox(props: TextProps) {
             sx: {
               overflowX: 'hidden',
               textOverflow: 'ellipsis',
-              padding: '8.5px 14px',
+              paddingTop: '8.5px',
+              paddingBottom: '8.5px',
+              paddingLeft: '14px',
+              paddingRight: `calc(14px + ${ensureArray(props.endIcon).length} * 28px)`,
             },
           },
           endAdornment: props.endIcon && (
-            <MuiInputAdornment sx={{ paddingRight: '14px' }} position="end">
+            <MuiInputAdornment
+              sx={inputElementClasses.endAdornmentRoot}
+              position="end"
+            >
               {props.endIcon}
             </MuiInputAdornment>
           ),
         }}
         multiline={props.multiline}
-        minRows={props.minRows}
+        minRows={props.expanded ? props.maxRows : props.minRows}
         maxRows={props.maxRows}
         variant="outlined"
         size="small"
diff --git a/src/Frontend/Components/InputElements/shared.ts b/src/Frontend/Components/InputElements/shared.ts
index 3ceed226..aa32a65d 100644
--- a/src/Frontend/Components/InputElements/shared.ts
+++ b/src/Frontend/Components/InputElements/shared.ts
@@ -2,7 +2,7 @@
 // SPDX-FileCopyrightText: TNG Technology Consulting GmbH <https://www.tngtech.com>
 //
 // SPDX-License-Identifier: Apache-2.0
-import { SxProps } from '@mui/material';
+import { SxProps, TextFieldProps } from '@mui/material';
 import { ChangeEvent } from 'react';
 
 import { OpossumColors } from '../../shared-styles';
@@ -27,6 +27,7 @@ export const inputElementClasses = {
         display: 'none',
       },
     },
+    '& .Mui-readOnly:hover fieldset': { borderColor: 'rgba(0, 0, 0, 0.23)' },
   },
   defaultHighlightedTextField: {
     '& div': {
@@ -48,15 +49,25 @@ export const inputElementClasses = {
       padding: '1px 3px',
     },
   },
-};
+  endAdornmentRoot: {
+    position: 'absolute',
+    right: 0,
+    marginRight: '14px',
+    height: 0,
+  },
+} satisfies SxProps;
 
 export interface InputElementProps {
   title?: string;
-  isEditable?: boolean;
+  readOnly?: boolean;
+  disabled?: boolean;
   text?: string;
   sx?: SxProps;
   handleChange?(
     event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
   ): void;
   isHighlighted?: boolean;
+  color?: TextFieldProps['color'];
+  focused?: boolean;
+  endIcon?: React.ReactElement | Array<React.ReactElement>;
 }
diff --git a/src/Frontend/Components/LocatorPopup/LocatorPopup.tsx b/src/Frontend/Components/LocatorPopup/LocatorPopup.tsx
index 263054e9..0977eea2 100644
--- a/src/Frontend/Components/LocatorPopup/LocatorPopup.tsx
+++ b/src/Frontend/Components/LocatorPopup/LocatorPopup.tsx
@@ -153,7 +153,6 @@ export function LocatorPopup(): ReactElement {
       </MuiBox>
       <Dropdown
         sx={classes.dropdown}
-        isEditable={true}
         title={'Criticality'}
         value={criticalityDropDownChoice}
         menuItems={criticalityMenuItems}
diff --git a/src/Frontend/Components/ResourceDetailsTabs/ResourceDetailsTabs.tsx b/src/Frontend/Components/ResourceDetailsTabs/ResourceDetailsTabs.tsx
index b843f3ac..f8b2af9d 100644
--- a/src/Frontend/Components/ResourceDetailsTabs/ResourceDetailsTabs.tsx
+++ b/src/Frontend/Components/ResourceDetailsTabs/ResourceDetailsTabs.tsx
@@ -186,7 +186,6 @@ export function ResourceDetailsTabs(props: ResourceDetailsTabsProps) {
       {showSortingSelect ? (
         <Dropdown
           sx={classes.dropdown}
-          isEditable
           title={text.buttons.sort}
           value={signalSorting}
           menuItems={sortingOptions}
diff --git a/src/Frontend/shared-constants.ts b/src/Frontend/shared-constants.ts
index 40c552d0..0b3ae985 100644
--- a/src/Frontend/shared-constants.ts
+++ b/src/Frontend/shared-constants.ts
@@ -49,3 +49,9 @@ export const sortings = Object.values(text.sortings);
 export type Sorting = (typeof sortings)[number];
 export const attributionDefaultSorting = text.sortings.name;
 export const signalDefaultSorting = text.sortings.occurrence;
+
+export const thirdPartyKeys: Array<keyof PackageInfo> = [
+  'copyright',
+  'licenseName',
+  'licenseText',
+];
diff --git a/src/Frontend/util/ensure-array.ts b/src/Frontend/util/ensure-array.ts
new file mode 100644
index 00000000..1ace4359
--- /dev/null
+++ b/src/Frontend/util/ensure-array.ts
@@ -0,0 +1,11 @@
+// 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 function ensureArray<T>(elementOrArray: T | Array<T>): Array<T> {
+  if (Array.isArray(elementOrArray)) {
+    return elementOrArray;
+  }
+  return [elementOrArray];
+}
diff --git a/src/Frontend/util/get-comparable-attributes.ts b/src/Frontend/util/get-comparable-attributes.ts
new file mode 100644
index 00000000..9dc5ffc1
--- /dev/null
+++ b/src/Frontend/util/get-comparable-attributes.ts
@@ -0,0 +1,35 @@
+// 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 { pickBy } from 'lodash';
+
+import { PackageInfo } from '../../shared/shared-types';
+import { thirdPartyKeys } from '../shared-constants';
+
+export const FORM_ATTRIBUTES = [
+  'packageName',
+  'packageVersion',
+  'packageNamespace',
+  'packageType',
+  'url',
+  'copyright',
+  'licenseName',
+  'licenseText',
+  'firstParty',
+  'comment',
+] satisfies Array<keyof PackageInfo>;
+
+export type FormAttribute = (typeof FORM_ATTRIBUTES)[number];
+
+export function getComparableAttributes(packageInfo: PackageInfo) {
+  return pickBy(
+    packageInfo,
+    (value, key) =>
+      FORM_ATTRIBUTES.some((attribute) => attribute === key) &&
+      (packageInfo.firstParty
+        ? !thirdPartyKeys.includes(key as keyof PackageInfo)
+        : true) &&
+      !!value,
+  );
+}
diff --git a/src/Frontend/util/get-stripped-package-info.ts b/src/Frontend/util/get-stripped-package-info.ts
index 55ca9ee1..b3bf7f90 100644
--- a/src/Frontend/util/get-stripped-package-info.ts
+++ b/src/Frontend/util/get-stripped-package-info.ts
@@ -5,6 +5,7 @@
 import { isEqual, pickBy } from 'lodash';
 
 import { PackageInfo } from '../../shared/shared-types';
+import { thirdPartyKeys } from '../shared-constants';
 
 export function getStrippedPackageInfo(packageInfo: PackageInfo) {
   return pickBy(
@@ -20,12 +21,6 @@ export function getStrippedPackageInfo(packageInfo: PackageInfo) {
   );
 }
 
-const thirdPartyKeys: Array<keyof PackageInfo> = [
-  'copyright',
-  'licenseName',
-  'licenseText',
-];
-
 const strippedPackageInfoTemplate: {
   [P in keyof Required<PackageInfo>]: boolean;
 } = {
diff --git a/src/shared/text.ts b/src/shared/text.ts
index b65c647a..a11d5cfe 100644
--- a/src/shared/text.ts
+++ b/src/shared/text.ts
@@ -55,9 +55,14 @@ export const text = {
   },
   buttons: {
     cancel: 'Cancel',
+    compareToOriginal: 'Compare to original',
     confirm: 'Confirm',
     filter: 'Filter',
     sort: 'Sort',
+    diffPopup: {
+      applyChanges: 'Apply changes',
+      revertAll: 'Revert all',
+    },
   },
   changePreferredStatusGloballyPopup: {
     markAsPreferred: 'Do you really want to prefer the attribution globally?',
@@ -133,4 +138,7 @@ export const text = {
     searchTooltip: 'Search',
     sortTooltip: 'Sort',
   },
+  diffPopup: {
+    title: 'Compare to Original Signal',
+  },
 } as const;


Test patch:
diff --git a/src/Frontend/Components/AttributionColumn/__tests__/AttributionColumn.test.tsx b/src/Frontend/Components/AttributionColumn/__tests__/AttributionColumn.test.tsx
index 2cd6473a..7faf14eb 100644
--- a/src/Frontend/Components/AttributionColumn/__tests__/AttributionColumn.test.tsx
+++ b/src/Frontend/Components/AttributionColumn/__tests__/AttributionColumn.test.tsx
@@ -390,9 +390,10 @@ describe('The AttributionColumn', () => {
     );
   });
 
-  it('renders a chip for modified preferred and reverts to original state', async () => {
+  it('renders a chip for modified preferred', () => {
     const originId = faker.string.uuid();
     const temporaryDisplayPackageInfo = faker.opossum.packageInfo({
+      packageName: faker.lorem.word(),
       originIds: [originId],
       wasPreferred: false,
     });
@@ -443,12 +444,6 @@ describe('The AttributionColumn', () => {
     expect(
       screen.getByText(text.auditingOptions.modifiedPreferred),
     ).toBeInTheDocument();
-
-    await userEvent.click(screen.getByLabelText('undo modified preferred'));
-
-    expect(
-      screen.queryByText(text.auditingOptions.modifiedPreferred),
-    ).not.toBeInTheDocument();
   });
 
   it('renders a URL icon and opens a link in browser', () => {
diff --git a/src/Frontend/Components/DiffPopup/__test__/DiffPopup.util.test.tsx b/src/Frontend/Components/DiffPopup/__test__/DiffPopup.util.test.tsx
new file mode 100644
index 00000000..bc8dca4a
--- /dev/null
+++ b/src/Frontend/Components/DiffPopup/__test__/DiffPopup.util.test.tsx
@@ -0,0 +1,146 @@
+// 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 { waitFor } from '@testing-library/react';
+import { without } from 'lodash';
+
+import { faker } from '../../../../testing/Faker';
+import { renderHook } from '../../../test-helpers/render';
+import {
+  FORM_ATTRIBUTES,
+  FormAttribute,
+} from '../../../util/get-comparable-attributes';
+import { AttributionFormConfig } from '../../AttributionColumn/AttributionForm';
+import { useAttributionFormConfigs } from '../DiffPopup.util';
+
+describe('useAttributionFormConfigs', () => {
+  const EMPTY_ORIGINAL_CONFIG = FORM_ATTRIBUTES.reduce<AttributionFormConfig>(
+    (acc, attribute) => {
+      return attribute !== 'firstParty'
+        ? { ...acc, [attribute]: { color: undefined, focused: false } }
+        : { ...acc, [attribute]: { color: undefined } };
+    },
+    {},
+  );
+
+  const EMPTY_BUFFER_CONFIG = FORM_ATTRIBUTES.reduce<AttributionFormConfig>(
+    (acc, attribute) => {
+      return attribute !== 'firstParty'
+        ? {
+            ...acc,
+            [attribute]: {
+              color: undefined,
+              focused: false,
+              endIcon: undefined,
+            },
+          }
+        : {
+            ...acc,
+            [attribute]: { color: undefined, endIcon: undefined },
+          };
+    },
+    {},
+  );
+
+  it.each(
+    without(FORM_ATTRIBUTES, 'firstParty') as Array<
+      Exclude<FormAttribute, 'firstParty'>
+    >,
+  )(
+    'computes attribution form config correctly when %s changes',
+    async (attribute) => {
+      const originalPackageInfo = faker.opossum.packageInfo();
+      const currentPackageInfo = faker.opossum.packageInfo({
+        ...originalPackageInfo,
+        [attribute]: faker.internet.domainWord(),
+      });
+
+      const { result } = renderHook(() =>
+        useAttributionFormConfigs({
+          original: originalPackageInfo,
+          current: currentPackageInfo,
+        }),
+      );
+
+      await waitFor(() => {
+        expect(
+          result.current.originalFormConfig,
+        ).toEqual<AttributionFormConfig>({
+          ...EMPTY_ORIGINAL_CONFIG,
+          [attribute]: { focused: true, color: 'error' },
+        });
+      });
+      await waitFor(() =>
+        expect(result.current.bufferFormConfig).toEqual<AttributionFormConfig>({
+          ...EMPTY_BUFFER_CONFIG,
+          [attribute]: {
+            focused: true,
+            color: 'success',
+            endIcon: expect.anything(),
+          },
+        }),
+      );
+    },
+  );
+
+  const EXPECTED_ORIGINAL_CONFIG =
+    FORM_ATTRIBUTES.reduce<AttributionFormConfig>((acc, attribute) => {
+      return attribute !== 'firstParty'
+        ? { ...acc, [attribute]: { color: undefined, focused: false } }
+        : { ...acc, [attribute]: { color: 'error' } };
+    }, {});
+
+  const EXPECTED_BUFFER_CONFIG = FORM_ATTRIBUTES.reduce<AttributionFormConfig>(
+    (acc, attribute) => {
+      return attribute !== 'firstParty'
+        ? {
+            ...acc,
+            [attribute]: {
+              color: undefined,
+              focused: false,
+              endIcon: undefined,
+            },
+          }
+        : {
+            ...acc,
+            [attribute]: { color: 'success', endIcon: expect.anything() },
+          };
+    },
+    {},
+  );
+
+  it.each<[boolean, boolean, [AttributionFormConfig, AttributionFormConfig]]>([
+    [true, true, [EMPTY_ORIGINAL_CONFIG, EMPTY_BUFFER_CONFIG]],
+    [true, false, [EXPECTED_ORIGINAL_CONFIG, EXPECTED_BUFFER_CONFIG]],
+    [false, true, [EXPECTED_ORIGINAL_CONFIG, EXPECTED_BUFFER_CONFIG]],
+    [false, false, [EMPTY_ORIGINAL_CONFIG, EMPTY_BUFFER_CONFIG]],
+  ])(
+    'computes attribution form config correctly if original is first party: %s and current is first party: %s',
+    async (originalFirstParty, currentFirstParty, expectedConfigs) => {
+      const originalPackageInfo = faker.opossum.packageInfo({
+        firstParty: originalFirstParty,
+      });
+      const bufferPackageInfo = faker.opossum.packageInfo({
+        ...originalPackageInfo,
+        ...{ firstParty: currentFirstParty },
+      });
+
+      const { result } = renderHook(() =>
+        useAttributionFormConfigs({
+          original: originalPackageInfo,
+          current: bufferPackageInfo,
+        }),
+      );
+
+      await waitFor(() =>
+        expect([
+          result.current.originalFormConfig,
+          result.current.bufferFormConfig,
+        ]).toEqual<[AttributionFormConfig, AttributionFormConfig]>(
+          expectedConfigs,
+        ),
+      );
+    },
+  );
+});
diff --git a/src/Frontend/Components/InputElements/__tests__/Dropdown.test.tsx b/src/Frontend/Components/InputElements/__tests__/Dropdown.test.tsx
index 1813158e..c474e75a 100644
--- a/src/Frontend/Components/InputElements/__tests__/Dropdown.test.tsx
+++ b/src/Frontend/Components/InputElements/__tests__/Dropdown.test.tsx
@@ -13,7 +13,6 @@ describe('The Dropdown', () => {
   it('renders value', () => {
     render(
       <Dropdown
-        isEditable={true}
         title={'Confidence'}
         value={DiscreteConfidence.High.toString()}
         menuItems={[
@@ -40,7 +39,6 @@ describe('The Dropdown', () => {
   it('renders value not in menuItems', () => {
     render(
       <Dropdown
-        isEditable={true}
         title={'Confidence'}
         value={'10'}
         menuItems={[
diff --git a/src/Frontend/Components/InputElements/__tests__/TextBox.test.tsx b/src/Frontend/Components/InputElements/__tests__/TextBox.test.tsx
index 35fab685..26323c0a 100644
--- a/src/Frontend/Components/InputElements/__tests__/TextBox.test.tsx
+++ b/src/Frontend/Components/InputElements/__tests__/TextBox.test.tsx
@@ -3,24 +3,12 @@
 //
 // SPDX-License-Identifier: Apache-2.0
 import { render, screen } from '@testing-library/react';
-import { ChangeEvent } from 'react';
 
-import { doNothing } from '../../../util/do-nothing';
 import { TextBox } from '../TextBox';
 
 describe('The TextBox', () => {
   it('renders text and label', () => {
-    render(
-      <TextBox
-        title={'Test Title'}
-        text={'Test Content'}
-        handleChange={
-          doNothing as unknown as (
-            event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
-          ) => void
-        }
-      />,
-    );
+    render(<TextBox title={'Test Title'} text={'Test Content'} />);
 
     expect(screen.queryAllByText('Test Title')).toHaveLength(2);
     expect(screen.getByDisplayValue('Test Content')).toBeInTheDocument();
@@ -31,11 +19,6 @@ describe('The TextBox', () => {
       <TextBox
         title={'Test Title'}
         text={'Test Content'}
-        handleChange={
-          doNothing as unknown as (
-            event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
-          ) => void
-        }
         endIcon={<div>Test Icon</div>}
       />,
     );
diff --git a/src/e2e-tests/__tests__/comparing-attribution-with-origin.test.ts b/src/e2e-tests/__tests__/comparing-attribution-with-origin.test.ts
new file mode 100644
index 00000000..59490622
--- /dev/null
+++ b/src/e2e-tests/__tests__/comparing-attribution-with-origin.test.ts
@@ -0,0 +1,179 @@
+// 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 { AttributionType } from '../../Frontend/enums/enums';
+import { faker, test } from '../utils';
+
+const [resourceName1, resourceName2] = faker.opossum.resourceNames({
+  count: 2,
+});
+const packageName = faker.lorem.word();
+const copyright = faker.lorem.sentence();
+const [attributionId1, manualPackageInfo1] = faker.opossum.rawAttribution();
+const [attributionId2, manualPackageInfo2] = faker.opossum.rawAttribution({
+  originIds: [faker.string.uuid()],
+  packageName,
+  copyright,
+  licenseText: faker.opossum.license().defaultText,
+  firstParty: false,
+});
+const [externalAttributionId, externalPackageInfo] =
+  faker.opossum.rawAttribution(manualPackageInfo2);
+
+test.use({
+  data: {
+    inputData: faker.opossum.inputData({
+      resources: faker.opossum.resources({
+        [resourceName1]: 1,
+        [resourceName2]: 1,
+      }),
+      externalAttributions: faker.opossum.rawAttributions({
+        [externalAttributionId]: externalPackageInfo,
+      }),
+      resourcesToAttributions: faker.opossum.resourcesToAttributions({
+        [faker.opossum.filePath(resourceName2)]: [externalAttributionId],
+      }),
+    }),
+    outputData: faker.opossum.outputData({
+      manualAttributions: faker.opossum.rawAttributions({
+        [attributionId1]: manualPackageInfo1,
+        [attributionId2]: manualPackageInfo2,
+      }),
+      resourcesToAttributions: faker.opossum.resourcesToAttributions({
+        [faker.opossum.filePath(resourceName1)]: [attributionId1],
+        [faker.opossum.filePath(resourceName2)]: [attributionId2],
+      }),
+    }),
+  },
+});
+
+test('opens the diff popup if attribution has original and compare button is clicked', async ({
+  attributionDetails,
+  diffPopup,
+  resourceBrowser,
+}) => {
+  await resourceBrowser.goto(resourceName1);
+  await attributionDetails.assert.compareButtonIsHidden();
+
+  await resourceBrowser.goto(resourceName2);
+  await attributionDetails.assert.compareButtonIsEnabled();
+
+  await attributionDetails.compareButton.click();
+  await diffPopup.assert.isVisible();
+});
+
+test('reverts all changes and applies reverted state to temporary package info', async ({
+  attributionDetails,
+  diffPopup,
+  resourceBrowser,
+}) => {
+  await resourceBrowser.goto(resourceName2);
+  const newPackageName = faker.lorem.word();
+  await attributionDetails.attributionForm.name.fill(newPackageName);
+  await attributionDetails.attributionForm.selectAttributionType(
+    AttributionType.FirstParty,
+  );
+  await attributionDetails.compareButton.click();
+  await diffPopup.currentAttributionForm.assert.nameIs(newPackageName);
+  await diffPopup.currentAttributionForm.assert.attributionTypeIs(
+    AttributionType.FirstParty,
+  );
+  await diffPopup.currentAttributionForm.assert.copyrightIs('');
+  await diffPopup.currentAttributionForm.assert.licenseTextIsVisible();
+  await diffPopup.currentAttributionForm.assert.licenseTextIs('');
+  await diffPopup.currentAttributionForm.assert.licenseNameIs('');
+  await diffPopup.currentAttributionForm.assert.nameUndoButtonIsVisible();
+  await diffPopup.currentAttributionForm.assert.nameRedoButtonIsHidden();
+  await diffPopup.currentAttributionForm.assert.attributionTypeUndoButtonIsVisible();
+  await diffPopup.currentAttributionForm.assert.attributionTypeRedoButtonIsHidden();
+  await diffPopup.currentAttributionForm.assert.copyrightUndoButtonIsHidden();
+  await diffPopup.currentAttributionForm.assert.copyrightRedoButtonIsHidden();
+
+  await diffPopup.assert.applyButtonIsDisabled();
+  await diffPopup.revertAllButton.click();
+  await diffPopup.applyButton.click();
+  await diffPopup.assert.isHidden();
+  await attributionDetails.attributionForm.assert.matchesPackageInfo(
+    manualPackageInfo2,
+  );
+});
+
+test('reverts single fields correctly', async ({
+  attributionDetails,
+  diffPopup,
+  resourceBrowser,
+}) => {
+  await resourceBrowser.goto(resourceName2);
+  const newPackageName = faker.lorem.word();
+  await attributionDetails.attributionForm.name.fill(newPackageName);
+  const newCopyright = faker.lorem.sentence();
+  await attributionDetails.attributionForm.copyright.fill(newCopyright);
+  await attributionDetails.compareButton.click();
+  await diffPopup.currentAttributionForm.assert.nameIs(newPackageName);
+  await diffPopup.currentAttributionForm.assert.nameUndoButtonIsVisible();
+  await diffPopup.currentAttributionForm.assert.nameRedoButtonIsHidden();
+  await diffPopup.currentAttributionForm.assert.copyrightIs(newCopyright);
+  await diffPopup.currentAttributionForm.assert.copyrightUndoButtonIsVisible();
+  await diffPopup.currentAttributionForm.assert.copyrightRedoButtonIsHidden();
+
+  await diffPopup.currentAttributionForm.nameUndoButton.click();
+  await diffPopup.currentAttributionForm.assert.nameRedoButtonIsVisible();
+  await diffPopup.currentAttributionForm.assert.nameUndoButtonIsHidden();
+  await diffPopup.currentAttributionForm.assert.nameIs(packageName);
+
+  await diffPopup.currentAttributionForm.nameRedoButton.click();
+  await diffPopup.currentAttributionForm.assert.nameIs(newPackageName);
+
+  await diffPopup.currentAttributionForm.copyrightUndoButton.click();
+  await diffPopup.currentAttributionForm.assert.copyrightIs(copyright);
+  await diffPopup.currentAttributionForm.assert.copyrightRedoButtonIsVisible();
+  await diffPopup.currentAttributionForm.assert.copyrightUndoButtonIsHidden();
+
+  await diffPopup.revertAllButton.click();
+  await diffPopup.applyButton.click();
+  await diffPopup.assert.isHidden();
+  await attributionDetails.attributionForm.assert.matchesPackageInfo(
+    manualPackageInfo2,
+  );
+});
+
+test('handles pending license and copyright changes in temporary package info correctly', async ({
+  attributionDetails,
+  diffPopup,
+  resourceBrowser,
+}) => {
+  await resourceBrowser.goto(resourceName2);
+  const newPackageName = faker.lorem.word();
+  await attributionDetails.attributionForm.name.fill(newPackageName);
+  await attributionDetails.attributionForm.selectAttributionType(
+    AttributionType.FirstParty,
+  );
+  await attributionDetails.compareButton.click();
+  await diffPopup.currentAttributionForm.assert.nameIs(newPackageName);
+  await diffPopup.currentAttributionForm.assert.attributionTypeIs(
+    AttributionType.FirstParty,
+  );
+  await diffPopup.currentAttributionForm.assert.nameUndoButtonIsVisible();
+  await diffPopup.currentAttributionForm.assert.attributionTypeUndoButtonIsVisible();
+  await diffPopup.assert.applyButtonIsDisabled();
+
+  await diffPopup.currentAttributionForm.nameUndoButton.click();
+  await diffPopup.currentAttributionForm.assert.nameRedoButtonIsVisible();
+
+  await diffPopup.currentAttributionForm.attributionTypeUndoButton.click();
+  await diffPopup.currentAttributionForm.assert.attributionTypeRedoButtonIsVisible();
+  await diffPopup.currentAttributionForm.assert.attributionTypeUndoButtonIsHidden();
+  await diffPopup.assert.revertAllButtonIsDisabled();
+
+  await diffPopup.currentAttributionForm.attributionTypeRedoButton.click();
+  await diffPopup.applyButton.click();
+  await attributionDetails.attributionForm.assert.licenseTextIsHidden();
+
+  await attributionDetails.attributionForm.selectAttributionType(
+    AttributionType.ThirdParty,
+  );
+  await attributionDetails.attributionForm.assert.matchesPackageInfo(
+    manualPackageInfo2,
+  );
+});
diff --git a/src/e2e-tests/page-objects/AttributionDetails.ts b/src/e2e-tests/page-objects/AttributionDetails.ts
index 5512fe2c..a34cdb80 100644
--- a/src/e2e-tests/page-objects/AttributionDetails.ts
+++ b/src/e2e-tests/page-objects/AttributionDetails.ts
@@ -4,6 +4,7 @@
 // SPDX-License-Identifier: Apache-2.0
 import { expect, type Locator, Page } from '@playwright/test';
 
+import { text } from '../../shared/text';
 import { AttributionForm } from './AttributionForm';
 
 export class AttributionDetails {
@@ -32,6 +33,7 @@ export class AttributionDetails {
     readonly deleteGlobally: Locator;
   };
   readonly revertButton: Locator;
+  readonly compareButton: Locator;
   readonly showHideSignalButton: Locator;
 
   constructor(window: Page) {
@@ -95,6 +97,10 @@ export class AttributionDetails {
       name: 'Revert',
       exact: true,
     });
+    this.compareButton = this.node.getByRole('button', {
+      name: text.buttons.compareToOriginal,
+      exact: true,
+    });
     this.showHideSignalButton = this.node.getByLabel('resolve attribution');
   }
 
@@ -165,6 +171,12 @@ export class AttributionDetails {
     revertButtonIsHidden: async (): Promise<void> => {
       await expect(this.revertButton).toBeHidden();
     },
+    compareButtonIsEnabled: async (): Promise<void> => {
+      await expect(this.compareButton).toBeEnabled();
+    },
+    compareButtonIsHidden: async (): Promise<void> => {
+      await expect(this.compareButton).toBeHidden();
+    },
     showHideSignalButtonIsVisible: async (): Promise<void> => {
       await expect(this.showHideSignalButton).toBeVisible();
     },
diff --git a/src/e2e-tests/page-objects/AttributionForm.ts b/src/e2e-tests/page-objects/AttributionForm.ts
index 72324907..099b6689 100644
--- a/src/e2e-tests/page-objects/AttributionForm.ts
+++ b/src/e2e-tests/page-objects/AttributionForm.ts
@@ -39,6 +39,12 @@ export class AttributionForm {
     readonly previouslyPreferredLabel: Locator;
     readonly sourceLabel: Locator;
   };
+  readonly nameUndoButton: Locator;
+  readonly nameRedoButton: Locator;
+  readonly copyrightUndoButton: Locator;
+  readonly copyrightRedoButton: Locator;
+  readonly attributionTypeUndoButton: Locator;
+  readonly attributionTypeRedoButton: Locator;
 
   constructor(context: Locator, window: Page) {
     this.window = window;
@@ -115,6 +121,12 @@ export class AttributionForm {
         .getByRole('menuitem')
         .getByText(text.auditingOptions.excludedFromNotice),
     };
+    this.nameUndoButton = this.node.getByTestId('packageName-undo');
+    this.nameRedoButton = this.node.getByTestId('packageName-redo');
+    this.copyrightUndoButton = this.node.getByTestId('copyright-undo');
+    this.copyrightRedoButton = this.node.getByTestId('copyright-redo');
+    this.attributionTypeUndoButton = this.node.getByTestId('firstParty-undo');
+    this.attributionTypeRedoButton = this.node.getByTestId('firstParty-redo');
   }
 
   public comment(number = 0): Locator {
@@ -245,6 +257,42 @@ export class AttributionForm {
     ): Promise<void> => {
       await expect(this.auditingLabels[label]).toBeHidden();
     },
+    nameUndoButtonIsVisible: async () => {
+      await expect(this.nameUndoButton).toBeVisible();
+    },
+    nameUndoButtonIsHidden: async () => {
+      await expect(this.nameUndoButton).toBeHidden();
+    },
+    nameRedoButtonIsVisible: async () => {
+      await expect(this.nameRedoButton).toBeVisible();
+    },
+    nameRedoButtonIsHidden: async () => {
+      await expect(this.nameRedoButton).toBeHidden();
+    },
+    copyrightUndoButtonIsVisible: async () => {
+      await expect(this.copyrightUndoButton).toBeVisible();
+    },
+    copyrightUndoButtonIsHidden: async () => {
+      await expect(this.copyrightUndoButton).toBeHidden();
+    },
+    copyrightRedoButtonIsVisible: async () => {
+      await expect(this.copyrightRedoButton).toBeVisible();
+    },
+    copyrightRedoButtonIsHidden: async () => {
+      await expect(this.copyrightRedoButton).toBeHidden();
+    },
+    attributionTypeUndoButtonIsVisible: async () => {
+      await expect(this.attributionTypeUndoButton).toBeVisible();
+    },
+    attributionTypeUndoButtonIsHidden: async () => {
+      await expect(this.attributionTypeUndoButton).toBeHidden();
+    },
+    attributionTypeRedoButtonIsVisible: async () => {
+      await expect(this.attributionTypeRedoButton).toBeVisible();
+    },
+    attributionTypeRedoButtonIsHidden: async () => {
+      await expect(this.attributionTypeRedoButton).toBeHidden();
+    },
   };
 
   async openAuditingOptionsMenu(): Promise<void> {
diff --git a/src/e2e-tests/page-objects/DiffPopup.ts b/src/e2e-tests/page-objects/DiffPopup.ts
new file mode 100644
index 00000000..c932e8ac
--- /dev/null
+++ b/src/e2e-tests/page-objects/DiffPopup.ts
@@ -0,0 +1,56 @@
+// 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 { expect, type Locator, type Page } from '@playwright/test';
+
+import { text } from '../../shared/text';
+import { AttributionForm } from './AttributionForm';
+
+export class DiffPopup {
+  private readonly node: Locator;
+  readonly originalAttributionForm: AttributionForm;
+  readonly currentAttributionForm: AttributionForm;
+  readonly applyButton: Locator;
+  readonly revertAllButton: Locator;
+  readonly cancelButton: Locator;
+
+  constructor(window: Page) {
+    this.node = window.getByLabel('diff popup');
+    this.originalAttributionForm = new AttributionForm(
+      this.node.getByLabel('original', { exact: true }),
+      window,
+    );
+    this.currentAttributionForm = new AttributionForm(
+      this.node.getByLabel('current', { exact: true }),
+      window,
+    );
+    this.applyButton = this.node.getByRole('button', {
+      name: text.buttons.diffPopup.applyChanges,
+      exact: true,
+    });
+    this.revertAllButton = this.node.getByRole('button', {
+      name: text.buttons.diffPopup.revertAll,
+      exact: true,
+    });
+    this.cancelButton = this.node.getByRole('button', {
+      name: text.buttons.cancel,
+      exact: true,
+    });
+  }
+
+  public assert = {
+    isVisible: async (): Promise<void> => {
+      await expect(this.node).toBeVisible();
+    },
+    isHidden: async (): Promise<void> => {
+      await expect(this.node).toBeHidden();
+    },
+    applyButtonIsDisabled: async () => {
+      await expect(this.applyButton).toBeDisabled();
+    },
+    revertAllButtonIsDisabled: async () => {
+      await expect(this.revertAllButton).toBeDisabled();
+    },
+  };
+}
diff --git a/src/e2e-tests/utils/fixtures.ts b/src/e2e-tests/utils/fixtures.ts
index 2165396e..30d4d570 100644
--- a/src/e2e-tests/utils/fixtures.ts
+++ b/src/e2e-tests/utils/fixtures.ts
@@ -23,6 +23,7 @@ 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 { DiffPopup } from '../page-objects/DiffPopup';
 import { ErrorPopup } from '../page-objects/ErrorPopup';
 import { FileSearchPopup } from '../page-objects/FileSearchPopup';
 import { FileSupportPopup } from '../page-objects/FileSupportPopup';
@@ -56,6 +57,7 @@ export const test = base.extend<{
   changePreferredStatusGloballyPopup: ChangePreferredStatusGloballyPopup;
   confirmationDialog: ConfirmationDialog;
   confirmationPopup: ConfirmationPopup;
+  diffPopup: DiffPopup;
   errorPopup: ErrorPopup;
   fileSearchPopup: FileSearchPopup;
   fileSupportPopup: FileSupportPopup;
@@ -164,6 +166,9 @@ export const test = base.extend<{
   notSavedPopup: async ({ window }, use) => {
     await use(new NotSavedPopup(window));
   },
+  diffPopup: async ({ window }, use) => {
+    await use(new DiffPopup(window));
+  },
 });
 
 function getLaunchProps(): [executablePath: string | undefined, main: string] {