From 246b9b6063b93d4964fa8ccfe0edc6e4f6da29df Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Mon, 17 Feb 2025 16:55:59 +0100 Subject: [PATCH 01/27] refactor: move all menu text to shared/text.ts --- src/ElectronBackend/main/menu/aboutMenu.ts | 9 ++- src/ElectronBackend/main/menu/editMenu.ts | 35 ++++----- src/ElectronBackend/main/menu/fileMenu.ts | 47 ++++++------ src/ElectronBackend/main/menu/helpMenu.ts | 9 ++- .../main/menu/initiallyDisabledMenuItems.ts | 73 ++++--------------- src/ElectronBackend/main/menu/viewMenu.ts | 15 ++-- src/shared/text.ts | 55 ++++++++++++++ 7 files changed, 131 insertions(+), 112 deletions(-) diff --git a/src/ElectronBackend/main/menu/aboutMenu.ts b/src/ElectronBackend/main/menu/aboutMenu.ts index 2d7bfcc77..426f200e8 100644 --- a/src/ElectronBackend/main/menu/aboutMenu.ts +++ b/src/ElectronBackend/main/menu/aboutMenu.ts @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 import { shell } from 'electron'; +import { text } from '../../../shared/text'; import { getIconBasedOnTheme } from '../iconHelpers'; import { getPathOfChromiumNoticeDocument, @@ -17,7 +18,7 @@ function getOpenOnGithub() { 'icons/github-white.png', 'icons/github-black.png', ), - label: 'Open on GitHub', + label: text.menu.aboutSubmenu.openOnGithub, click: () => shell.openExternal('https://github.com/opossum-tool/opossumUI'), }; @@ -29,7 +30,7 @@ function getOpossumUiNotices() { 'icons/notice-white.png', 'icons/notice-black.png', ), - label: 'OpossumUI Notices', + label: text.menu.aboutSubmenu.opossumUINotices, click: () => shell.openPath(getPathOfNoticeDocument()), }; } @@ -40,14 +41,14 @@ function getChromiumNotices() { 'icons/chromium-white.png', 'icons/chromium-black.png', ), - label: 'Chromium Notices', + label: text.menu.aboutSubmenu.chromiumNotices, click: () => shell.openPath(getPathOfChromiumNoticeDocument()), }; } export function getAboutMenu() { return { - label: 'About', + label: text.menu.about, submenu: [getOpenOnGithub(), getOpossumUiNotices(), getChromiumNotices()], }; } diff --git a/src/ElectronBackend/main/menu/editMenu.ts b/src/ElectronBackend/main/menu/editMenu.ts index e6ca81a14..7c39338e0 100644 --- a/src/ElectronBackend/main/menu/editMenu.ts +++ b/src/ElectronBackend/main/menu/editMenu.ts @@ -6,15 +6,16 @@ import { MenuItemConstructorOptions } from 'electron'; import { AllowedFrontendChannels } from '../../../shared/ipc-channels'; +import { text } from '../../../shared/text'; import { isFileLoaded } from '../../utils/getLoadedFile'; import { getGlobalBackendState } from '../globalBackendState'; import { getIconBasedOnTheme } from '../iconHelpers'; -import { INITIALLY_DISABLED_ITEMS_INFO } from './initiallyDisabledMenuItems'; +import { INITIALLY_DISABLED_ITEMS_IDS } from './initiallyDisabledMenuItems'; function getUndo(): MenuItemConstructorOptions { return { icon: getIconBasedOnTheme('icons/undo-white.png', 'icons/undo-black.png'), - label: 'Undo', + label: text.menu.editSubmenu.undo, accelerator: 'CmdOrCtrl+Z', role: 'undo', }; @@ -23,7 +24,7 @@ function getUndo(): MenuItemConstructorOptions { function getRedo(): MenuItemConstructorOptions { return { icon: getIconBasedOnTheme('icons/redo-white.png', 'icons/redo-black.png'), - label: 'Redo', + label: text.menu.editSubmenu.redo, accelerator: 'Shift+CmdOrCtrl+Z', role: 'redo', }; @@ -32,7 +33,7 @@ function getRedo(): MenuItemConstructorOptions { function getCut(): MenuItemConstructorOptions { return { icon: getIconBasedOnTheme('icons/cut-white.png', 'icons/cut-black.png'), - label: 'Cut', + label: text.menu.editSubmenu.cut, accelerator: 'CmdOrCtrl+X', role: 'cut', }; @@ -41,7 +42,7 @@ function getCut(): MenuItemConstructorOptions { function getCopy(): MenuItemConstructorOptions { return { icon: getIconBasedOnTheme('icons/copy-white.png', 'icons/copy-black.png'), - label: 'Copy', + label: text.menu.editSubmenu.copy, accelerator: 'CmdOrCtrl+C', role: 'copy', }; @@ -50,7 +51,7 @@ function getCopy(): MenuItemConstructorOptions { function getPaste(): MenuItemConstructorOptions { return { icon: getIconBasedOnTheme('icons/paste-white.png', 'icons/paste-black.png'), - label: 'Paste', + label: text.menu.editSubmenu.paste, accelerator: 'CmdOrCtrl+V', role: 'paste', }; @@ -62,10 +63,10 @@ function getSelectAll(): MenuItemConstructorOptions { 'icons/select-all-white.png', 'icons/select-all-black.png', ), - label: INITIALLY_DISABLED_ITEMS_INFO.selectAll.label, + label: text.menu.editSubmenu.selectAll, accelerator: 'CmdOrCtrl+A', role: 'selectAll', - id: INITIALLY_DISABLED_ITEMS_INFO.selectAll.id, + id: INITIALLY_DISABLED_ITEMS_IDS.selectAll, enabled: false, }; } @@ -76,14 +77,14 @@ function getSearchAttributions(webContents: Electron.WebContents) { 'icons/magnifying-glass-white.png', 'icons/magnifying-glass-black.png', ), - label: INITIALLY_DISABLED_ITEMS_INFO.searchAttributions.label, + label: text.menu.editSubmenu.searchAttributions, accelerator: 'CmdOrCtrl+Shift+A', click: () => { if (isFileLoaded(getGlobalBackendState())) { webContents.send(AllowedFrontendChannels.SearchAttributions); } }, - id: INITIALLY_DISABLED_ITEMS_INFO.searchAttributions.id, + id: INITIALLY_DISABLED_ITEMS_IDS.searchAttributions, enabled: false, }; } @@ -94,14 +95,14 @@ function getSearchSignals(webContents: Electron.WebContents) { 'icons/magnifying-glass-white.png', 'icons/magnifying-glass-black.png', ), - label: INITIALLY_DISABLED_ITEMS_INFO.searchSignals.label, + label: text.menu.editSubmenu.searchSignals, accelerator: 'CmdOrCtrl+Shift+S', click: () => { if (isFileLoaded(getGlobalBackendState())) { webContents.send(AllowedFrontendChannels.SearchSignals); } }, - id: INITIALLY_DISABLED_ITEMS_INFO.searchSignals.id, + id: INITIALLY_DISABLED_ITEMS_IDS.searchSignals, enabled: false, }; } @@ -112,14 +113,14 @@ function getSearchResources(webContents: Electron.WebContents) { 'icons/search-white.png', 'icons/search-black.png', ), - label: INITIALLY_DISABLED_ITEMS_INFO.searchResourcesAll.label, + label: text.menu.editSubmenu.searchResourcesAll, accelerator: 'CmdOrCtrl+Shift+R', click: () => { if (isFileLoaded(getGlobalBackendState())) { webContents.send(AllowedFrontendChannels.SearchResources); } }, - id: INITIALLY_DISABLED_ITEMS_INFO.searchResourcesAll.id, + id: INITIALLY_DISABLED_ITEMS_IDS.searchResourcesAll, enabled: false, }; } @@ -130,14 +131,14 @@ function getSearchLinkedResources(webContents: Electron.WebContents) { 'icons/search-white.png', 'icons/search-black.png', ), - label: INITIALLY_DISABLED_ITEMS_INFO.searchResourceLinked.label, + label: text.menu.editSubmenu.searchResourceLinked, accelerator: 'CmdOrCtrl+Shift+L', click: () => { if (isFileLoaded(getGlobalBackendState())) { webContents.send(AllowedFrontendChannels.SearchLinkedResources); } }, - id: INITIALLY_DISABLED_ITEMS_INFO.searchResourceLinked.id, + id: INITIALLY_DISABLED_ITEMS_IDS.searchResourceLinked, enabled: false, }; } @@ -146,7 +147,7 @@ export function getEditMenu( webContents: Electron.WebContents, ): MenuItemConstructorOptions { return { - label: 'Edit', + label: text.menu.edit, submenu: [ getUndo(), getRedo(), diff --git a/src/ElectronBackend/main/menu/fileMenu.ts b/src/ElectronBackend/main/menu/fileMenu.ts index 1fff1d49a..fef431137 100644 --- a/src/ElectronBackend/main/menu/fileMenu.ts +++ b/src/ElectronBackend/main/menu/fileMenu.ts @@ -11,6 +11,7 @@ import { FileFormatInfo, FileType, } from '../../../shared/shared-types'; +import { text } from '../../../shared/text'; import { isFileLoaded } from '../../utils/getLoadedFile'; import { getGlobalBackendState } from '../globalBackendState'; import { getIconBasedOnTheme } from '../iconHelpers'; @@ -43,7 +44,7 @@ export const importFileFormats: Array = [ function getOpenFile(mainWindow: Electron.CrossProcessExports.BrowserWindow) { return { icon: getIconBasedOnTheme('icons/open-white.png', 'icons/open-black.png'), - label: 'Open File', + label: text.menu.fileSubmenu.open, accelerator: 'CmdOrCtrl+O', click: () => mainWindow.webContents.send( @@ -58,9 +59,9 @@ function getImportFile(mainWindow: Electron.CrossProcessExports.BrowserWindow) { 'icons/import-white.png', 'icons/import-black.png', ), - label: 'Import File', + label: text.menu.fileSubmenu.import, submenu: importFileFormats.map((fileFormat) => ({ - label: `${fileFormat.name} (${fileFormat.extensions.map((ext) => `.${ext}`).join('/')})`, + label: text.menu.fileSubmenu.importSubmenu(fileFormat), click: importFileListener(mainWindow, fileFormat), })), }; @@ -69,14 +70,14 @@ function getImportFile(mainWindow: Electron.CrossProcessExports.BrowserWindow) { function getSaveFile(webContents: Electron.WebContents) { return { icon: getIconBasedOnTheme('icons/save-white.png', 'icons/save-black.png'), - label: INITIALLY_DISABLED_ITEMS_INFO.save.label, + label: text.menu.fileSubmenu.save, accelerator: 'CmdOrCtrl+S', click: () => { webContents.send(AllowedFrontendChannels.SaveFileRequest, { saveFile: true, }); }, - id: INITIALLY_DISABLED_ITEMS_INFO.save.id, + id: INITIALLY_DISABLED_ITEMS_IDS.save, enabled: false, }; } @@ -84,7 +85,7 @@ function getSaveFile(webContents: Electron.WebContents) { function getProjectMetadata(webContents: Electron.WebContents) { return { icon: getIconBasedOnTheme('icons/about-white.png', 'icons/about-black.png'), - label: INITIALLY_DISABLED_ITEMS_INFO.projectMetadata.label, + label: text.menu.fileSubmenu.projectMetadata, click: () => { if (isFileLoaded(getGlobalBackendState())) { webContents.send(AllowedFrontendChannels.ShowProjectMetadataPopup, { @@ -92,7 +93,7 @@ function getProjectMetadata(webContents: Electron.WebContents) { }); } }, - id: INITIALLY_DISABLED_ITEMS_INFO.projectMetadata.id, + id: INITIALLY_DISABLED_ITEMS_IDS.projectMetadata, enabled: false, }; } @@ -103,7 +104,7 @@ function getProjectStatistics(webContents: Electron.WebContents) { 'icons/statictics-white.png', 'icons/statictics-black.png', ), - label: INITIALLY_DISABLED_ITEMS_INFO.projectStatistics.label, + label: text.menu.fileSubmenu.projectStatistics, click: () => { if (isFileLoaded(getGlobalBackendState())) { webContents.send(AllowedFrontendChannels.ShowProjectStatisticsPopup, { @@ -111,7 +112,7 @@ function getProjectStatistics(webContents: Electron.WebContents) { }); } }, - id: INITIALLY_DISABLED_ITEMS_INFO.projectStatistics.id, + id: INITIALLY_DISABLED_ITEMS_IDS.projectStatistics, enabled: false, }; } @@ -122,7 +123,7 @@ function getSetBaseUrl(mainWindow: Electron.CrossProcessExports.BrowserWindow) { 'icons/restore-white.png', 'icons/restore-black.png', ), - label: 'Set Path to Sources', + label: text.menu.fileSubmenu.setBaseURL, click: selectBaseURLListener(mainWindow), }; } @@ -130,7 +131,7 @@ function getSetBaseUrl(mainWindow: Electron.CrossProcessExports.BrowserWindow) { function getQuit() { return { icon: getIconBasedOnTheme('icons/quit-white.png', 'icons/quit-black.png'), - label: 'Quit', + label: text.menu.fileSubmenu.quit, accelerator: 'CmdOrCtrl+Q', click: () => { app.quit(); @@ -140,7 +141,7 @@ function getQuit() { function getExportFollowUp(webContents: Electron.WebContents) { return { - label: INITIALLY_DISABLED_ITEMS_INFO.followUp.label, + label: text.menu.fileSubmenu.exportSubmenu.followUp, icon: getIconBasedOnTheme( 'icons/follow-up-white.png', 'icons/follow-up-black.png', @@ -153,7 +154,7 @@ function getExportFollowUp(webContents: Electron.WebContents) { ExportType.FollowUp, ); }, - id: INITIALLY_DISABLED_ITEMS_INFO.followUp.id, + id: INITIALLY_DISABLED_ITEMS_IDS.followUp, enabled: false, }; } @@ -164,7 +165,7 @@ function getExportCompactBom(webContents: Electron.WebContents) { 'icons/com-list-white.png', 'icons/com-list-black.png', ), - label: INITIALLY_DISABLED_ITEMS_INFO.compactComponentList.label, + label: text.menu.fileSubmenu.exportSubmenu.compactComponentList, click: () => { setLoadingState(webContents, true); logger.info('Preparing data for compact component list export'); @@ -173,7 +174,7 @@ function getExportCompactBom(webContents: Electron.WebContents) { ExportType.CompactBom, ); }, - id: INITIALLY_DISABLED_ITEMS_INFO.compactComponentList.id, + id: INITIALLY_DISABLED_ITEMS_IDS.compactComponentList, enabled: false, }; } @@ -184,7 +185,7 @@ function getExportDetailedBom(webContents: Electron.WebContents) { 'icons/det-list-white.png', 'icons/det-list-black.png', ), - label: INITIALLY_DISABLED_ITEMS_INFO.detailedComponentList.label, + label: text.menu.fileSubmenu.exportSubmenu.detailedComponentList, click: () => { setLoadingState(webContents, true); logger.info('Preparing data for detailed component list export'); @@ -193,7 +194,7 @@ function getExportDetailedBom(webContents: Electron.WebContents) { ExportType.DetailedBom, ); }, - id: INITIALLY_DISABLED_ITEMS_INFO.detailedComponentList.id, + id: INITIALLY_DISABLED_ITEMS_IDS.detailedComponentList, enabled: false, }; } @@ -201,7 +202,7 @@ function getExportDetailedBom(webContents: Electron.WebContents) { function getExportSpdxYaml(webContents: Electron.WebContents) { return { icon: getIconBasedOnTheme('icons/yaml-white.png', 'icons/yaml-black.png'), - label: INITIALLY_DISABLED_ITEMS_INFO.spdxYAML.label, + label: text.menu.fileSubmenu.exportSubmenu.spdxYAML, click: () => { setLoadingState(webContents, true); logger.info('Preparing data for SPDX (yaml) export'); @@ -210,7 +211,7 @@ function getExportSpdxYaml(webContents: Electron.WebContents) { ExportType.SpdxDocumentYaml, ); }, - id: INITIALLY_DISABLED_ITEMS_INFO.spdxYAML.id, + id: INITIALLY_DISABLED_ITEMS_IDS.spdxYAML, enabled: false, }; } @@ -218,7 +219,7 @@ function getExportSpdxYaml(webContents: Electron.WebContents) { function getExportSpdxJson(webContents: Electron.WebContents) { return { icon: getIconBasedOnTheme('icons/json-white.png', 'icons/json-black.png'), - label: INITIALLY_DISABLED_ITEMS_INFO.spdxJSON.label, + label: text.menu.fileSubmenu.exportSubmenu.spdxJSON, click: () => { setLoadingState(webContents, true); logger.info('Preparing data for SPDX (json) export'); @@ -227,14 +228,14 @@ function getExportSpdxJson(webContents: Electron.WebContents) { ExportType.SpdxDocumentJson, ); }, - id: INITIALLY_DISABLED_ITEMS_INFO.spdxJSON.id, + id: INITIALLY_DISABLED_ITEMS_IDS.spdxJSON, enabled: false, }; } function getExportSubMenu(webContents: Electron.WebContents) { return { - label: 'Export', + label: text.menu.fileSubmenu.export, icon: getIconBasedOnTheme( 'icons/export-white.png', 'icons/export-black.png', @@ -252,7 +253,7 @@ function getExportSubMenu(webContents: Electron.WebContents) { export function getFileMenu(mainWindow: BrowserWindow) { const webContents = mainWindow.webContents; return { - label: 'File', + label: text.menu.file, submenu: [ getOpenFile(mainWindow), getImportFile(mainWindow), diff --git a/src/ElectronBackend/main/menu/helpMenu.ts b/src/ElectronBackend/main/menu/helpMenu.ts index 577f90555..4dd78ec7c 100644 --- a/src/ElectronBackend/main/menu/helpMenu.ts +++ b/src/ElectronBackend/main/menu/helpMenu.ts @@ -6,12 +6,13 @@ import { app, shell } from 'electron'; import { AllowedFrontendChannels } from '../../../shared/ipc-channels'; +import { text } from '../../../shared/text'; import { getIconBasedOnTheme } from '../iconHelpers'; function getOpenlogfiles() { return { icon: getIconBasedOnTheme('icons/log-white.png', 'icons/log-black.png'), - label: 'Open log files folder', + label: text.menu.helpSubmenu.openLogFiles, click: () => shell.openPath(app.getPath('logs')), }; } @@ -22,7 +23,7 @@ function getCheckForUpdates(webContents: Electron.WebContents) { 'icons/update-white.png', 'icons/update-black.png', ), - label: 'Check for updates', + label: text.menu.helpSubmenu.checkForUpdates, click: () => { webContents.send(AllowedFrontendChannels.ShowUpdateAppPopup, { showUpdateAppPopup: true, @@ -37,7 +38,7 @@ function getUsersGuide() { 'icons/user-guide-white.png', 'icons/user-guide-black.png', ), - label: "User's Guide", + label: text.menu.helpSubmenu.userGuide, click: () => shell.openExternal( 'https://github.com/opossum-tool/OpossumUI/blob/main/USER_GUIDE.md', @@ -47,7 +48,7 @@ function getUsersGuide() { export function getHelpMenu(webContents: Electron.WebContents) { return { - label: 'Help', + label: text.menu.help, submenu: [ getUsersGuide(), getOpenlogfiles(), diff --git a/src/ElectronBackend/main/menu/initiallyDisabledMenuItems.ts b/src/ElectronBackend/main/menu/initiallyDisabledMenuItems.ts index 73a94a9f6..c32f78d95 100644 --- a/src/ElectronBackend/main/menu/initiallyDisabledMenuItems.ts +++ b/src/ElectronBackend/main/menu/initiallyDisabledMenuItems.ts @@ -4,67 +4,26 @@ // SPDX-License-Identifier: Apache-2.0 import { Menu } from 'electron'; -export const INITIALLY_DISABLED_MENU_ITEMS = [ - 'save', - 'projectMetadata', - 'projectStatistics', - 'followUp', - 'compactComponentList', - 'detailedComponentList', - 'spdxYAML', - 'spdxJSON', - 'selectAll', - 'searchAttributions', - 'searchSignals', - 'searchResourcesAll', - 'searchResourceLinked', -] as const; - -type Item = { label: string; id: string }; - -export const INITIALLY_DISABLED_ITEMS_INFO: Record< - (typeof INITIALLY_DISABLED_MENU_ITEMS)[number], - Item -> = { - save: { label: 'Save', id: 'save' }, - followUp: { label: 'Follow-Up', id: 'follow-up' }, - compactComponentList: { - label: 'Compact component list', - id: 'compact-list', - }, - detailedComponentList: { - label: 'Detailed component list', - id: 'detailed-list', - }, - spdxYAML: { label: 'SPDX (yaml)', id: 'spdx-yaml' }, - spdxJSON: { label: 'SPDX (json)', id: 'spdx-json' }, - projectMetadata: { label: 'Project Metadata', id: 'project-metadata' }, - projectStatistics: { - label: 'Project Statistics', - id: 'project-statistics', - }, - selectAll: { label: 'Select All', id: 'select-all' }, - searchAttributions: { - label: 'Search Attributions', - id: 'search-attributions', - }, - searchSignals: { label: 'Search Signals', id: 'search-signals' }, - searchResourcesAll: { - label: 'Search All Resources', - id: 'search-resources-all', - }, - searchResourceLinked: { - label: 'Search Linked Resources', - id: 'search-resources-linked', - }, +export const INITIALLY_DISABLED_ITEMS_IDS: Record = { + save: 'save', + followUp: 'follow-up', + compactComponentList: 'compact-list', + detailedComponentList: 'detailed-list', + spdxYAML: 'spdx-yaml', + spdxJSON: 'spdx-json', + projectMetadata: 'project-metadata', + projectStatistics: 'project-statistics', + selectAll: 'select-all', + searchAttributions: 'search-attributions', + searchSignals: 'search-signals', + searchResourcesAll: 'search-resources-all', + searchResourceLinked: 'search-resources-linked', }; export function activateMenuItems(): void { const menu = Menu.getApplicationMenu(); - INITIALLY_DISABLED_MENU_ITEMS.forEach((key) => { - const menuItem = menu?.getMenuItemById( - INITIALLY_DISABLED_ITEMS_INFO[key].id, - ); + Object.values(INITIALLY_DISABLED_ITEMS_IDS).forEach((id) => { + const menuItem = menu?.getMenuItemById(id); if (menuItem) { menuItem.enabled = true; } diff --git a/src/ElectronBackend/main/menu/viewMenu.ts b/src/ElectronBackend/main/menu/viewMenu.ts index faea0c513..59077837f 100644 --- a/src/ElectronBackend/main/menu/viewMenu.ts +++ b/src/ElectronBackend/main/menu/viewMenu.ts @@ -5,6 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 import { MenuItemConstructorOptions } from 'electron'; +import { text } from '../../../shared/text'; import { getIconBasedOnTheme, makeFirstIconVisibleAndSecondHidden, @@ -17,7 +18,7 @@ function getShowDevTools(): MenuItemConstructorOptions { 'icons/developer-tool-white.png', 'icons/developer-tool-black.png', ), - label: 'Show Developer Tools', + label: text.menu.viewSubmenu.showDevTools, role: 'toggleDevTools', }; } @@ -28,7 +29,7 @@ function getToggleFullScreen(): MenuItemConstructorOptions { 'icons/full-screen-white.png', 'icons/full-screen-black.png', ), - label: 'Full Screen', + label: text.menu.viewSubmenu.toggleFullScreen, role: 'togglefullscreen', }; } @@ -39,7 +40,7 @@ function getZoomIn(): MenuItemConstructorOptions { 'icons/zoom-in-white.png', 'icons/zoom-in-black.png', ), - label: 'Zoom In', + label: text.menu.viewSubmenu.zoomIn, role: 'zoomIn', }; } @@ -50,7 +51,7 @@ function getZoomOut(): MenuItemConstructorOptions { 'icons/zoom-out-white.png', 'icons/zoom-out-black.png', ), - label: 'Zoom Out', + label: text.menu.viewSubmenu.zoomOut, role: 'zoomOut', }; } @@ -61,7 +62,7 @@ function getEnableQaMode(qaMode: null | boolean) { 'icons/check-box-blank-white.png', 'icons/check-box-blank-black.png', ), - label: 'QA Mode', + label: text.menu.viewSubmenu.qaMode, id: 'disabled-qa-mode', click: () => { makeFirstIconVisibleAndSecondHidden( @@ -80,7 +81,7 @@ function getDisableQaMode(qaMode: null | boolean) { 'icons/check-box-white.png', 'icons/check-box-black.png', ), - label: 'QA Mode', + label: text.menu.viewSubmenu.qaMode, id: 'enabled-qa-mode', click: () => { makeFirstIconVisibleAndSecondHidden( @@ -97,7 +98,7 @@ export async function getViewMenu(): Promise { const qaMode = await UserSettings.get('qaMode'); return { - label: 'View', + label: text.menu.view, submenu: [ getShowDevTools(), getToggleFullScreen(), diff --git a/src/shared/text.ts b/src/shared/text.ts index 146a6ce66..09efe95b2 100644 --- a/src/shared/text.ts +++ b/src/shared/text.ts @@ -5,6 +5,61 @@ import { FileFormatInfo } from './shared-types'; export const text = { + menu: { + file: 'File', + fileSubmenu: { + open: 'Open...', + import: 'Import', + importSubmenu: (fileFormat: FileFormatInfo) => + `${fileFormat.name} (${fileFormat.extensions.map((ext) => `.${ext}`).join('/')})...`, + save: 'Save', + projectMetadata: 'Project Metadata', + projectStatistics: 'Project Statistics', + setBaseURL: 'Set Path to Sources', + quit: 'Quit', + export: 'Export', + exportSubmenu: { + followUp: 'Follow-Up', + compactComponentList: 'Compact component list', + detailedComponentList: 'Detailed component list', + spdxYAML: 'SPDX (yaml)', + spdxJSON: 'SPDX (json)', + }, + }, + edit: 'Edit', + editSubmenu: { + undo: 'Undo', + redo: 'Redo', + cut: 'Cut', + copy: 'Copy', + paste: 'Paste', + selectAll: 'Select All', + searchAttributions: 'Search Attributions', + searchSignals: 'Search Signals', + searchResourcesAll: 'Search All Resources', + searchResourceLinked: 'Search Linked Resources', + }, + view: 'View', + viewSubmenu: { + showDevTools: 'Show Developer Tools', + toggleFullScreen: 'Full Screen', + zoomIn: 'Zoom In', + zoomOut: 'Zoom Out', + qaMode: 'QA Mode', + }, + about: 'About', + aboutSubmenu: { + openOnGithub: 'Open on GitHub', + opossumUINotices: 'OpossumUI Notices', + chromiumNotices: 'Chromium Notices', + }, + help: 'Help', + helpSubmenu: { + openLogFiles: 'Open log files folder', + checkForUpdates: 'Check For Updates', + userGuide: 'User Guide', + }, + }, attributionColumn: { commonEcosystems: 'Common Ecosystems', commonLicenses: 'Common Licenses', From a393b46ee71dcaa154c9b3a34feaaa264a136e11 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Mon, 17 Feb 2025 17:25:06 +0100 Subject: [PATCH 02/27] refactor: simplify handling of initially disabled menu items * no more need to manually manage id strings in a record --- src/ElectronBackend/main/main.ts | 12 +++++-- .../main/menu/DisabledMenuItemHandler.ts | 32 +++++++++++++++++++ src/ElectronBackend/main/menu/editMenu.ts | 12 +++---- src/ElectronBackend/main/menu/fileMenu.ts | 18 +++++------ .../main/menu/initiallyDisabledMenuItems.ts | 31 ------------------ .../openFileFromCliOrEnvVariableIfProvided.ts | 8 +++-- 6 files changed, 62 insertions(+), 51 deletions(-) create mode 100644 src/ElectronBackend/main/menu/DisabledMenuItemHandler.ts delete mode 100644 src/ElectronBackend/main/menu/initiallyDisabledMenuItems.ts diff --git a/src/ElectronBackend/main/main.ts b/src/ElectronBackend/main/main.ts index 6bec7ad9b..2cf4009f6 100644 --- a/src/ElectronBackend/main/main.ts +++ b/src/ElectronBackend/main/main.ts @@ -18,7 +18,7 @@ import { saveFileListener, } from './listeners'; import { createMenu } from './menu'; -import { activateMenuItems } from './menu/initiallyDisabledMenuItems'; +import { DisabledMenuItemHandler } from './menu/DisabledMenuItemHandler'; import { openFileFromCliOrEnvVariableIfProvided } from './openFileFromCliOrEnvVariableIfProvided'; import { UserSettings } from './user-settings'; @@ -65,7 +65,10 @@ export async function main(): Promise { }); ipcMain.handle( IpcChannel.OpenFile, - openFileListener(mainWindow, activateMenuItems), + openFileListener( + mainWindow, + DisabledMenuItemHandler.activateMenuItems, + ), ); ipcMain.handle( IpcChannel.ImportFileSelectInput, @@ -77,7 +80,10 @@ export async function main(): Promise { ); ipcMain.handle( IpcChannel.ImportFileConvertAndLoad, - importFileConvertAndLoadListener(mainWindow, activateMenuItems), + importFileConvertAndLoadListener( + mainWindow, + DisabledMenuItemHandler.activateMenuItems, + ), ); ipcMain.handle(IpcChannel.SaveFile, saveFileListener(mainWindow)); ipcMain.handle(IpcChannel.ExportFile, exportFileListener(mainWindow)); diff --git a/src/ElectronBackend/main/menu/DisabledMenuItemHandler.ts b/src/ElectronBackend/main/menu/DisabledMenuItemHandler.ts new file mode 100644 index 000000000..54149e8bd --- /dev/null +++ b/src/ElectronBackend/main/menu/DisabledMenuItemHandler.ts @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 +import { Menu } from 'electron'; + +export class DisabledMenuItemHandler { + private static nextId: number = 0; + + static registerDisabledMenuItem(): string { + const idString = DisabledMenuItemHandler.nextId.toString(); + DisabledMenuItemHandler.nextId++; + return idString; + } + + private static disabledIds(): Array { + return Array(DisabledMenuItemHandler.nextId) + .keys() + .map((id) => id.toString()) + .toArray(); + } + + static activateMenuItems(): void { + const menu = Menu.getApplicationMenu(); + DisabledMenuItemHandler.disabledIds().forEach((id) => { + const menuItem = menu?.getMenuItemById(id); + if (menuItem) { + menuItem.enabled = true; + } + }); + } +} diff --git a/src/ElectronBackend/main/menu/editMenu.ts b/src/ElectronBackend/main/menu/editMenu.ts index 7c39338e0..73a0e7940 100644 --- a/src/ElectronBackend/main/menu/editMenu.ts +++ b/src/ElectronBackend/main/menu/editMenu.ts @@ -10,7 +10,7 @@ import { text } from '../../../shared/text'; import { isFileLoaded } from '../../utils/getLoadedFile'; import { getGlobalBackendState } from '../globalBackendState'; import { getIconBasedOnTheme } from '../iconHelpers'; -import { INITIALLY_DISABLED_ITEMS_IDS } from './initiallyDisabledMenuItems'; +import { DisabledMenuItemHandler } from './DisabledMenuItemHandler'; function getUndo(): MenuItemConstructorOptions { return { @@ -66,7 +66,7 @@ function getSelectAll(): MenuItemConstructorOptions { label: text.menu.editSubmenu.selectAll, accelerator: 'CmdOrCtrl+A', role: 'selectAll', - id: INITIALLY_DISABLED_ITEMS_IDS.selectAll, + id: DisabledMenuItemHandler.registerDisabledMenuItem(), enabled: false, }; } @@ -84,7 +84,7 @@ function getSearchAttributions(webContents: Electron.WebContents) { webContents.send(AllowedFrontendChannels.SearchAttributions); } }, - id: INITIALLY_DISABLED_ITEMS_IDS.searchAttributions, + id: DisabledMenuItemHandler.registerDisabledMenuItem(), enabled: false, }; } @@ -102,7 +102,7 @@ function getSearchSignals(webContents: Electron.WebContents) { webContents.send(AllowedFrontendChannels.SearchSignals); } }, - id: INITIALLY_DISABLED_ITEMS_IDS.searchSignals, + id: DisabledMenuItemHandler.registerDisabledMenuItem(), enabled: false, }; } @@ -120,7 +120,7 @@ function getSearchResources(webContents: Electron.WebContents) { webContents.send(AllowedFrontendChannels.SearchResources); } }, - id: INITIALLY_DISABLED_ITEMS_IDS.searchResourcesAll, + id: DisabledMenuItemHandler.registerDisabledMenuItem(), enabled: false, }; } @@ -138,7 +138,7 @@ function getSearchLinkedResources(webContents: Electron.WebContents) { webContents.send(AllowedFrontendChannels.SearchLinkedResources); } }, - id: INITIALLY_DISABLED_ITEMS_IDS.searchResourceLinked, + id: DisabledMenuItemHandler.registerDisabledMenuItem(), enabled: false, }; } diff --git a/src/ElectronBackend/main/menu/fileMenu.ts b/src/ElectronBackend/main/menu/fileMenu.ts index fef431137..a0693da6c 100644 --- a/src/ElectronBackend/main/menu/fileMenu.ts +++ b/src/ElectronBackend/main/menu/fileMenu.ts @@ -21,7 +21,7 @@ import { setLoadingState, } from '../listeners'; import logger from '../logger'; -import { INITIALLY_DISABLED_ITEMS_INFO } from './initiallyDisabledMenuItems'; +import { DisabledMenuItemHandler } from './DisabledMenuItemHandler'; export const importFileFormats: Array = [ { @@ -77,7 +77,7 @@ function getSaveFile(webContents: Electron.WebContents) { saveFile: true, }); }, - id: INITIALLY_DISABLED_ITEMS_IDS.save, + id: DisabledMenuItemHandler.registerDisabledMenuItem(), enabled: false, }; } @@ -93,7 +93,7 @@ function getProjectMetadata(webContents: Electron.WebContents) { }); } }, - id: INITIALLY_DISABLED_ITEMS_IDS.projectMetadata, + id: DisabledMenuItemHandler.registerDisabledMenuItem(), enabled: false, }; } @@ -112,7 +112,7 @@ function getProjectStatistics(webContents: Electron.WebContents) { }); } }, - id: INITIALLY_DISABLED_ITEMS_IDS.projectStatistics, + id: DisabledMenuItemHandler.registerDisabledMenuItem(), enabled: false, }; } @@ -154,7 +154,7 @@ function getExportFollowUp(webContents: Electron.WebContents) { ExportType.FollowUp, ); }, - id: INITIALLY_DISABLED_ITEMS_IDS.followUp, + id: DisabledMenuItemHandler.registerDisabledMenuItem(), enabled: false, }; } @@ -174,7 +174,7 @@ function getExportCompactBom(webContents: Electron.WebContents) { ExportType.CompactBom, ); }, - id: INITIALLY_DISABLED_ITEMS_IDS.compactComponentList, + id: DisabledMenuItemHandler.registerDisabledMenuItem(), enabled: false, }; } @@ -194,7 +194,7 @@ function getExportDetailedBom(webContents: Electron.WebContents) { ExportType.DetailedBom, ); }, - id: INITIALLY_DISABLED_ITEMS_IDS.detailedComponentList, + id: DisabledMenuItemHandler.registerDisabledMenuItem(), enabled: false, }; } @@ -211,7 +211,7 @@ function getExportSpdxYaml(webContents: Electron.WebContents) { ExportType.SpdxDocumentYaml, ); }, - id: INITIALLY_DISABLED_ITEMS_IDS.spdxYAML, + id: DisabledMenuItemHandler.registerDisabledMenuItem(), enabled: false, }; } @@ -228,7 +228,7 @@ function getExportSpdxJson(webContents: Electron.WebContents) { ExportType.SpdxDocumentJson, ); }, - id: INITIALLY_DISABLED_ITEMS_IDS.spdxJSON, + id: DisabledMenuItemHandler.registerDisabledMenuItem(), enabled: false, }; } diff --git a/src/ElectronBackend/main/menu/initiallyDisabledMenuItems.ts b/src/ElectronBackend/main/menu/initiallyDisabledMenuItems.ts deleted file mode 100644 index c32f78d95..000000000 --- a/src/ElectronBackend/main/menu/initiallyDisabledMenuItems.ts +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates -// SPDX-FileCopyrightText: TNG Technology Consulting GmbH -// -// SPDX-License-Identifier: Apache-2.0 -import { Menu } from 'electron'; - -export const INITIALLY_DISABLED_ITEMS_IDS: Record = { - save: 'save', - followUp: 'follow-up', - compactComponentList: 'compact-list', - detailedComponentList: 'detailed-list', - spdxYAML: 'spdx-yaml', - spdxJSON: 'spdx-json', - projectMetadata: 'project-metadata', - projectStatistics: 'project-statistics', - selectAll: 'select-all', - searchAttributions: 'search-attributions', - searchSignals: 'search-signals', - searchResourcesAll: 'search-resources-all', - searchResourceLinked: 'search-resources-linked', -}; - -export function activateMenuItems(): void { - const menu = Menu.getApplicationMenu(); - Object.values(INITIALLY_DISABLED_ITEMS_IDS).forEach((id) => { - const menuItem = menu?.getMenuItemById(id); - if (menuItem) { - menuItem.enabled = true; - } - }); -} diff --git a/src/ElectronBackend/main/openFileFromCliOrEnvVariableIfProvided.ts b/src/ElectronBackend/main/openFileFromCliOrEnvVariableIfProvided.ts index e3cf85c1d..d0150bb8b 100644 --- a/src/ElectronBackend/main/openFileFromCliOrEnvVariableIfProvided.ts +++ b/src/ElectronBackend/main/openFileFromCliOrEnvVariableIfProvided.ts @@ -6,7 +6,7 @@ import { BrowserWindow } from 'electron'; import log from 'electron-log'; import { handleOpeningFile } from './listeners'; -import { activateMenuItems } from './menu/initiallyDisabledMenuItems'; +import { DisabledMenuItemHandler } from './menu/DisabledMenuItemHandler'; export async function openFileFromCliOrEnvVariableIfProvided( mainWindow: BrowserWindow, @@ -37,6 +37,10 @@ export async function openFileFromCliOrEnvVariableIfProvided( } if (inputFileName) { - await handleOpeningFile(mainWindow, inputFileName, activateMenuItems); + await handleOpeningFile( + mainWindow, + inputFileName, + DisabledMenuItemHandler.activateMenuItems, + ); } } From 8b2ebe22d505de37ff5e6321a1fae722447a96bb Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Mon, 17 Feb 2025 17:35:09 +0100 Subject: [PATCH 03/27] feat: add menu entries for merge --- src/ElectronBackend/main/menu/fileMenu.ts | 17 +++++++++++++++++ src/shared/text.ts | 9 +++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/ElectronBackend/main/menu/fileMenu.ts b/src/ElectronBackend/main/menu/fileMenu.ts index a0693da6c..30b2ea55a 100644 --- a/src/ElectronBackend/main/menu/fileMenu.ts +++ b/src/ElectronBackend/main/menu/fileMenu.ts @@ -67,6 +67,22 @@ function getImportFile(mainWindow: Electron.CrossProcessExports.BrowserWindow) { }; } +function getMerge(_: Electron.CrossProcessExports.BrowserWindow) { + return { + icon: getIconBasedOnTheme( + 'icons/import-white.png', + 'icons/import-black.png', + ), + label: text.menu.fileSubmenu.merge, + submenu: importFileFormats.map((fileFormat) => ({ + label: text.menu.fileSubmenu.mergeSubmenu(fileFormat), + click: () => console.log(`merge ${fileFormat.name}`), + id: DisabledMenuItemHandler.registerDisabledMenuItem(), + enabled: false, + })), + }; +} + function getSaveFile(webContents: Electron.WebContents) { return { icon: getIconBasedOnTheme('icons/save-white.png', 'icons/save-black.png'), @@ -257,6 +273,7 @@ export function getFileMenu(mainWindow: BrowserWindow) { submenu: [ getOpenFile(mainWindow), getImportFile(mainWindow), + getMerge(mainWindow), getSaveFile(webContents), getExportSubMenu(webContents), getProjectMetadata(webContents), diff --git a/src/shared/text.ts b/src/shared/text.ts index 09efe95b2..235d3c835 100644 --- a/src/shared/text.ts +++ b/src/shared/text.ts @@ -4,14 +4,19 @@ // SPDX-License-Identifier: Apache-2.0 import { FileFormatInfo } from './shared-types'; +function menuLabelForFileFormat(fileFormat: FileFormatInfo): string { + return `${fileFormat.name} (${fileFormat.extensions.map((ext) => `.${ext}`).join('/')})...`; +} + export const text = { menu: { file: 'File', fileSubmenu: { open: 'Open...', import: 'Import', - importSubmenu: (fileFormat: FileFormatInfo) => - `${fileFormat.name} (${fileFormat.extensions.map((ext) => `.${ext}`).join('/')})...`, + importSubmenu: menuLabelForFileFormat, + merge: 'Merge', + mergeSubmenu: menuLabelForFileFormat, save: 'Save', projectMetadata: 'Project Metadata', projectStatistics: 'Project Statistics', From 947916b4f8632086a391da585a18fe3ef234a2d0 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Tue, 18 Feb 2025 09:56:25 +0100 Subject: [PATCH 04/27] feat: request opening merge dialog when menu item is clicked --- .../main/__tests__/listeners.test.ts | 2 +- src/ElectronBackend/main/listeners.ts | 14 +++++++++++++- src/ElectronBackend/main/menu/fileMenu.ts | 5 +++-- .../BackendCommunication/BackendCommunication.tsx | 9 ++++++++- src/Frontend/enums/enums.ts | 1 + .../state/actions/popup-actions/popup-actions.ts | 11 +++++++++++ src/Frontend/state/actions/view-actions/types.ts | 6 ++++++ .../state/actions/view-actions/view-actions.ts | 8 ++++++++ src/Frontend/util/use-ipc-renderer.ts | 8 +++++++- src/shared/ipc-channels.ts | 3 ++- 10 files changed, 60 insertions(+), 7 deletions(-) diff --git a/src/ElectronBackend/main/__tests__/listeners.test.ts b/src/ElectronBackend/main/__tests__/listeners.test.ts index 737775f41..782551ced 100644 --- a/src/ElectronBackend/main/__tests__/listeners.test.ts +++ b/src/ElectronBackend/main/__tests__/listeners.test.ts @@ -412,7 +412,7 @@ describe('getImportFileListener', () => { listener(); expect(mainWindow.webContents.send).toHaveBeenCalledWith( - AllowedFrontendChannels.ImportFileShowDialog, + AllowedFrontendChannels.ShowImportDialog, fileFormat, ); }); diff --git a/src/ElectronBackend/main/listeners.ts b/src/ElectronBackend/main/listeners.ts index 921b967b2..db5af08eb 100644 --- a/src/ElectronBackend/main/listeners.ts +++ b/src/ElectronBackend/main/listeners.ts @@ -132,7 +132,19 @@ export async function handleOpeningFile( export const importFileListener = (mainWindow: BrowserWindow, fileFormat: FileFormatInfo) => (): void => { mainWindow.webContents.send( - AllowedFrontendChannels.ImportFileShowDialog, + AllowedFrontendChannels.ShowImportDialog, + fileFormat, + ); + }); +} + +export function getMergeListener( + mainWindow: BrowserWindow, + fileFormat: FileFormatInfo, +): () => Promise { + return createVoidListenerCallbackWithErrorHandling(mainWindow, () => { + mainWindow.webContents.send( + AllowedFrontendChannels.ShowMergeDialog, fileFormat, ); }; diff --git a/src/ElectronBackend/main/menu/fileMenu.ts b/src/ElectronBackend/main/menu/fileMenu.ts index 30b2ea55a..307fc65f4 100644 --- a/src/ElectronBackend/main/menu/fileMenu.ts +++ b/src/ElectronBackend/main/menu/fileMenu.ts @@ -17,6 +17,7 @@ import { getGlobalBackendState } from '../globalBackendState'; import { getIconBasedOnTheme } from '../iconHelpers'; import { importFileListener, + getMergeListener, selectBaseURLListener, setLoadingState, } from '../listeners'; @@ -67,7 +68,7 @@ function getImportFile(mainWindow: Electron.CrossProcessExports.BrowserWindow) { }; } -function getMerge(_: Electron.CrossProcessExports.BrowserWindow) { +function getMerge(mainWindow: Electron.CrossProcessExports.BrowserWindow) { return { icon: getIconBasedOnTheme( 'icons/import-white.png', @@ -76,7 +77,7 @@ function getMerge(_: Electron.CrossProcessExports.BrowserWindow) { label: text.menu.fileSubmenu.merge, submenu: importFileFormats.map((fileFormat) => ({ label: text.menu.fileSubmenu.mergeSubmenu(fileFormat), - click: () => console.log(`merge ${fileFormat.name}`), + click: getMergeListener(mainWindow, fileFormat), id: DisabledMenuItemHandler.registerDisabledMenuItem(), enabled: false, })), diff --git a/src/Frontend/Components/BackendCommunication/BackendCommunication.tsx b/src/Frontend/Components/BackendCommunication/BackendCommunication.tsx index 4ea339162..6a0e5a774 100644 --- a/src/Frontend/Components/BackendCommunication/BackendCommunication.tsx +++ b/src/Frontend/Components/BackendCommunication/BackendCommunication.tsx @@ -17,6 +17,7 @@ import { exportFileOrOpenUnsavedPopup, openFileOrOpenUnsavedPopup, showImportDialogOrOpenUnsavedPopup, + showMergeDialogOrOpenUnsavedPopup, } from '../../state/actions/popup-actions/popup-actions'; import { resetResourceState, @@ -30,6 +31,7 @@ import { ExportFileRequestListener, LoggingListener, ShowImportDialogListener, + ShowMergeDialogListener, useIpcRenderer, } from '../../util/use-ipc-renderer'; @@ -142,10 +144,15 @@ export const BackendCommunication: React.FC = () => { [dispatch], ); useIpcRenderer( - AllowedFrontendChannels.ImportFileShowDialog, + AllowedFrontendChannels.ShowImportDialog, (_, fileFormat) => dispatch(showImportDialogOrOpenUnsavedPopup(fileFormat)), [dispatch], ); + useIpcRenderer( + AllowedFrontendChannels.ShowMergeDialog, + (_, fileFormat) => dispatch(showMergeDialogOrOpenUnsavedPopup(fileFormat)), + [dispatch], + ); return null; }; diff --git a/src/Frontend/enums/enums.ts b/src/Frontend/enums/enums.ts index 3fa77f4aa..6bc89a97b 100644 --- a/src/Frontend/enums/enums.ts +++ b/src/Frontend/enums/enums.ts @@ -16,6 +16,7 @@ export enum PopupType { ProjectStatisticsPopup = 'ProjectStatisticsPopup', UpdateAppPopup = 'UpdateAppPopup', ImportDialog = 'ImportDialog', + MergeDialog = 'MergeDialog', } export enum ButtonText { diff --git a/src/Frontend/state/actions/popup-actions/popup-actions.ts b/src/Frontend/state/actions/popup-actions/popup-actions.ts index 9e721cbd4..0e4beb5cc 100644 --- a/src/Frontend/state/actions/popup-actions/popup-actions.ts +++ b/src/Frontend/state/actions/popup-actions/popup-actions.ts @@ -40,6 +40,7 @@ import { openPopup, setExportFileRequest, setImportFileRequest, + setMergeRequest, setOpenFileRequest, setTargetView, } from '../view-actions/view-actions'; @@ -121,6 +122,16 @@ export function showImportDialogOrOpenUnsavedPopup( }); } +export function showMergeDialogOrOpenUnsavedPopup( + fileFormat: FileFormatInfo, +): AppThunkAction { + return withUnsavedCheck({ + executeImmediately: (dispatch) => + dispatch(openPopup(PopupType.MergeDialog, undefined, fileFormat)), + requestContinuation: (dispatch) => dispatch(setMergeRequest(fileFormat)), + }); +} + export function openFileOrOpenUnsavedPopup(): AppThunkAction { return withUnsavedCheck({ executeImmediately: () => void window.electronAPI.openFile(), diff --git a/src/Frontend/state/actions/view-actions/types.ts b/src/Frontend/state/actions/view-actions/types.ts index 24cb6a57e..a4d48e486 100644 --- a/src/Frontend/state/actions/view-actions/types.ts +++ b/src/Frontend/state/actions/view-actions/types.ts @@ -13,6 +13,7 @@ export const ACTION_CLOSE_POPUP = 'ACTION_CLOSE_POPUP'; export const ACTION_RESET_VIEW_STATE = 'ACTION_RESET_VIEW_STATE'; export const ACTION_SET_OPEN_FILE_REQUEST = 'ACTION_SET_OPEN_FILE_REQUEST'; export const ACTION_SET_IMPORT_FILE_REQUEST = 'ACTION_SET_IMPORT_FILE_REQUEST'; +export const ACTION_SET_MERGE_REQUEST = 'ACTION_SET_MERGE_REQUEST'; export const ACTION_SET_EXPORT_FILE_REQUEST = 'ACTION_SET_EXPORT_FILE_REQUEST'; export type ViewAction = @@ -58,6 +59,11 @@ export interface SetImportFileRequestAction { payload: FileFormatInfo | null; } +export interface SetMergeRequestAction { + type: typeof ACTION_SET_MERGE_REQUEST; + payload: FileFormatInfo | null; +} + export interface SetExportFileRequestAction { type: typeof ACTION_SET_EXPORT_FILE_REQUEST; payload: ExportType | null; diff --git a/src/Frontend/state/actions/view-actions/view-actions.ts b/src/Frontend/state/actions/view-actions/view-actions.ts index 927536532..958cd0383 100644 --- a/src/Frontend/state/actions/view-actions/view-actions.ts +++ b/src/Frontend/state/actions/view-actions/view-actions.ts @@ -16,6 +16,7 @@ import { ACTION_RESET_VIEW_STATE, ACTION_SET_EXPORT_FILE_REQUEST, ACTION_SET_IMPORT_FILE_REQUEST, + ACTION_SET_MERGE_REQUEST, ACTION_SET_OPEN_FILE_REQUEST, ACTION_SET_TARGET_VIEW, ACTION_SET_VIEW, @@ -24,6 +25,7 @@ import { ResetViewStateAction, SetExportFileRequestAction, SetImportFileRequestAction, + SetMergeRequestAction, SetOpenFileRequestAction, SetTargetView, SetView, @@ -96,6 +98,12 @@ export function setImportFileRequest( return { type: ACTION_SET_IMPORT_FILE_REQUEST, payload: fileFormat }; } +export function setMergeRequest( + fileFormat: FileFormatInfo | null, +): SetMergeRequestAction { + return { type: ACTION_SET_MERGE_REQUEST, payload: fileFormat }; +} + export function setExportFileRequest( exportFileRequest: ExportType | null, ): SetExportFileRequestAction { diff --git a/src/Frontend/util/use-ipc-renderer.ts b/src/Frontend/util/use-ipc-renderer.ts index e3cd9022e..46e9bb2ef 100644 --- a/src/Frontend/util/use-ipc-renderer.ts +++ b/src/Frontend/util/use-ipc-renderer.ts @@ -48,6 +48,11 @@ export type ShowImportDialogListener = ( fileFormat: FileFormatInfo, ) => void; +export type ShowMergeDialogListener = ( + event: IpcRendererEvent, + fileFormat: FileFormatInfo, +) => void; + export type Listener = | ResetStateListener | SetStateListener @@ -55,7 +60,8 @@ export type Listener = | ExportFileRequestListener | SetBaseURLForRootListener | IsLoadingListener - | ShowImportDialogListener; + | ShowImportDialogListener + | ShowMergeDialogListener; export function useIpcRenderer( channel: AllowedFrontendChannels, diff --git a/src/shared/ipc-channels.ts b/src/shared/ipc-channels.ts index 51fcabc55..d0e5ef18f 100644 --- a/src/shared/ipc-channels.ts +++ b/src/shared/ipc-channels.ts @@ -36,7 +36,8 @@ export enum AllowedFrontendChannels { SearchSignals = 'search-signals', SetBaseURLForRoot = 'set-base-url-for-root', OpenFileWithUnsavedCheck = 'open-file-with-unsaved-check', - ImportFileShowDialog = 'import-file-show-dialog', + ShowImportDialog = 'show-import-dialog', + ShowMergeDialog = 'show-merge-dialog', ShowProjectMetadataPopup = 'show-project-metadata-pop-up', ShowProjectStatisticsPopup = 'show-project-statistics-pop-up', ShowUpdateAppPopup = 'show-update-app-pop-up', From 1960bf36bbd36a256a0200e4f47cdab51c7a572f Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Tue, 18 Feb 2025 11:12:12 +0100 Subject: [PATCH 05/27] refactor: change names related to file selection to uncouple it from import process --- src/ElectronBackend/main/__tests__/listeners.test.ts | 6 +++--- src/ElectronBackend/main/listeners.ts | 5 +++-- src/ElectronBackend/main/main.ts | 7 ++----- src/ElectronBackend/main/menu/fileMenu.ts | 2 +- src/ElectronBackend/preload.ts | 4 ++-- src/Frontend/Components/ImportDialog/ImportDialog.tsx | 2 +- src/shared/ipc-channels.ts | 2 +- src/shared/shared-types.ts | 2 +- src/testing/setup-tests.ts | 2 +- 9 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/ElectronBackend/main/__tests__/listeners.test.ts b/src/ElectronBackend/main/__tests__/listeners.test.ts index 782551ced..a406b845c 100644 --- a/src/ElectronBackend/main/__tests__/listeners.test.ts +++ b/src/ElectronBackend/main/__tests__/listeners.test.ts @@ -29,11 +29,11 @@ import { setGlobalBackendState } from '../globalBackendState'; import { exportFileListener, importFileListener, - importFileSelectInputListener, importFileSelectSaveLocationListener, linkHasHttpSchema, openLinkListener, selectBaseURLListener, + selectFileListener } from '../listeners'; import { importFileFormats } from '../menu/fileMenu'; @@ -424,7 +424,7 @@ describe('getImportFileSelectInputListener', () => { const fileFormat = importFileFormats[0]; const selectedFilePath = '/home/input.json'; - const listener = importFileSelectInputListener(mainWindow); + const listener = selectFileListener(mainWindow); jest.mocked(openNonOpossumFileDialog).mockReturnValue([selectedFilePath]); @@ -440,7 +440,7 @@ describe('getImportFileSelectInputListener', () => { const mainWindow = await initWindowAndBackendState(); const fileFormat = importFileFormats[0]; - const listener = importFileSelectInputListener(mainWindow); + const listener = selectFileListener(mainWindow); jest .mocked(openNonOpossumFileDialog) diff --git a/src/ElectronBackend/main/listeners.ts b/src/ElectronBackend/main/listeners.ts index db5af08eb..bd465ca85 100644 --- a/src/ElectronBackend/main/listeners.ts +++ b/src/ElectronBackend/main/listeners.ts @@ -147,9 +147,10 @@ export function getMergeListener( AllowedFrontendChannels.ShowMergeDialog, fileFormat, ); - }; + }); +} -export const importFileSelectInputListener = +export const selectFileListener = (mainWindow: BrowserWindow) => async ( _: Electron.IpcMainInvokeEvent, diff --git a/src/ElectronBackend/main/main.ts b/src/ElectronBackend/main/main.ts index 2cf4009f6..7ef26d446 100644 --- a/src/ElectronBackend/main/main.ts +++ b/src/ElectronBackend/main/main.ts @@ -11,11 +11,11 @@ import { createWindow } from './createWindow'; import { exportFileListener, importFileConvertAndLoadListener, - importFileSelectInputListener, importFileSelectSaveLocationListener, openFileListener, openLinkListener, saveFileListener, + selectInputListener, } from './listeners'; import { createMenu } from './menu'; import { DisabledMenuItemHandler } from './menu/DisabledMenuItemHandler'; @@ -70,10 +70,7 @@ export async function main(): Promise { DisabledMenuItemHandler.activateMenuItems, ), ); - ipcMain.handle( - IpcChannel.ImportFileSelectInput, - importFileSelectInputListener(mainWindow), - ); + ipcMain.handle(IpcChannel.SelectFile, selectFileListener(mainWindow)); ipcMain.handle( IpcChannel.ImportFileSelectSaveLocation, importFileSelectSaveLocationListener(mainWindow), diff --git a/src/ElectronBackend/main/menu/fileMenu.ts b/src/ElectronBackend/main/menu/fileMenu.ts index 307fc65f4..fcb790066 100644 --- a/src/ElectronBackend/main/menu/fileMenu.ts +++ b/src/ElectronBackend/main/menu/fileMenu.ts @@ -37,7 +37,7 @@ export const importFileFormats: Array = [ }, { fileType: FileType.OWASP_JSON, - name: 'OWASP Dependency-Check', + name: 'OWASP Dependency-Check File', extensions: ['json'], }, ]; diff --git a/src/ElectronBackend/preload.ts b/src/ElectronBackend/preload.ts index bba0cb3af..7c30e367c 100644 --- a/src/ElectronBackend/preload.ts +++ b/src/ElectronBackend/preload.ts @@ -13,8 +13,8 @@ const electronAPI: ElectronAPI = { relaunch: () => ipcRenderer.invoke(IpcChannel.Relaunch), openLink: (link) => ipcRenderer.invoke(IpcChannel.OpenLink, { link }), openFile: () => ipcRenderer.invoke(IpcChannel.OpenFile), - importFileSelectInput: (fileFormat) => - ipcRenderer.invoke(IpcChannel.ImportFileSelectInput, fileFormat), + selectFile: (fileFormat) => + ipcRenderer.invoke(IpcChannel.SelectFile, fileFormat), importFileSelectSaveLocation: (defaultPath) => ipcRenderer.invoke(IpcChannel.ImportFileSelectSaveLocation, defaultPath), importFileConvertAndLoad: (inputFilePath, fileType, opossumFilePath) => diff --git a/src/Frontend/Components/ImportDialog/ImportDialog.tsx b/src/Frontend/Components/ImportDialog/ImportDialog.tsx index 528c34787..12b70300b 100644 --- a/src/Frontend/Components/ImportDialog/ImportDialog.tsx +++ b/src/Frontend/Components/ImportDialog/ImportDialog.tsx @@ -52,7 +52,7 @@ export const ImportDialog: React.FC = ({ fileFormat }) => { ); function selectInputFilePath(): void { - window.electronAPI.importFileSelectInput(fileFormat).then( + window.electronAPI.selectFile(fileFormat).then( (filePath) => { if (filePath) { setInputFilePath(filePath); diff --git a/src/shared/ipc-channels.ts b/src/shared/ipc-channels.ts index d0e5ef18f..768681d31 100644 --- a/src/shared/ipc-channels.ts +++ b/src/shared/ipc-channels.ts @@ -7,7 +7,7 @@ export enum IpcChannel { ExportFile = 'export-file', OpenFile = 'open-file', - ImportFileSelectInput = 'import-file-select-input', + SelectFile = 'select-file', ImportFileSelectSaveLocation = 'import-file-select-save-location', ImportFileConvertAndLoad = 'import-file-convert-and-load', OpenLink = 'open-link', diff --git a/src/shared/shared-types.ts b/src/shared/shared-types.ts index 2db387e69..711acf419 100644 --- a/src/shared/shared-types.ts +++ b/src/shared/shared-types.ts @@ -259,7 +259,7 @@ export interface ElectronAPI { relaunch: () => void; openLink: (link: string) => Promise; openFile: () => Promise; - importFileSelectInput: (fileFormat: FileFormatInfo) => Promise; + selectFile: (fileFormat: FileFormatInfo) => Promise; importFileSelectSaveLocation: (defaultPath: string) => Promise; importFileConvertAndLoad: ( inputFilePath: string, diff --git a/src/testing/setup-tests.ts b/src/testing/setup-tests.ts index 436fb1307..97fc5436b 100644 --- a/src/testing/setup-tests.ts +++ b/src/testing/setup-tests.ts @@ -36,7 +36,7 @@ global.window.electronAPI = { relaunch: jest.fn(), openLink: jest.fn().mockReturnValue(Promise.resolve()), openFile: jest.fn(), - importFileSelectInput: jest.fn(), + selectFile: jest.fn(), importFileSelectSaveLocation: jest.fn(), importFileConvertAndLoad: jest.fn(), exportFile: jest.fn(), From 7071b9136425ffc41e1fd94f80029316d27445f8 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Tue, 18 Feb 2025 11:27:43 +0100 Subject: [PATCH 06/27] feat: prepare IPC channel for commencing merge and load in the backend --- src/ElectronBackend/main/listeners.ts | 41 +++++++++++++++++++++++++++ src/ElectronBackend/main/main.ts | 6 ++++ src/ElectronBackend/preload.ts | 2 ++ src/shared/ipc-channels.ts | 1 + src/shared/shared-types.ts | 4 +++ src/testing/setup-tests.ts | 1 + 6 files changed, 55 insertions(+) diff --git a/src/ElectronBackend/main/listeners.ts b/src/ElectronBackend/main/listeners.ts index bd465ca85..f842bc01c 100644 --- a/src/ElectronBackend/main/listeners.ts +++ b/src/ElectronBackend/main/listeners.ts @@ -228,6 +228,47 @@ export const importFileConvertAndLoadListener = } }; +export function getMergeFileAndLoadListener( + mainWindow: BrowserWindow, +): ( + _: Electron.IpcMainInvokeEvent, + inputFilePath: string, + fileType: FileType, +) => Promise { + return createListenerCallbackWithErrorHandling( + mainWindow, + false, + async ( + _: Electron.IpcMainInvokeEvent, + inputFilePath: string, + fileType: FileType, + ) => { + if (!inputFilePath.trim() || !fs.existsSync(inputFilePath)) { + throw new Error('Input file does not exist'); + } + + const currentlyOpenOpossumFilePath = + getGlobalBackendState().opossumFilePath; + + if (!currentlyOpenOpossumFilePath) { + throw new Error('No open file to merge into'); + } + + logger.info('Merging input file into current .opossum file'); + await convertToOpossum( + inputFilePath, + currentlyOpenOpossumFilePath, + fileType, + ); + + await openFile(mainWindow, currentlyOpenOpossumFilePath, () => {}, true); + + return true; + }, + ListenerErrorReporting.SendToFrontend, + ); +} + function initializeGlobalBackendState( filePath: string, isOpossumFormat: boolean, diff --git a/src/ElectronBackend/main/main.ts b/src/ElectronBackend/main/main.ts index 7ef26d446..6c271ea31 100644 --- a/src/ElectronBackend/main/main.ts +++ b/src/ElectronBackend/main/main.ts @@ -10,11 +10,13 @@ import { getMessageBoxContentForErrorsWrapper } from '../errorHandling/errorHand import { createWindow } from './createWindow'; import { exportFileListener, + getMergeFileAndLoadListener, importFileConvertAndLoadListener, importFileSelectSaveLocationListener, openFileListener, openLinkListener, saveFileListener, + selectFileListener, selectInputListener, } from './listeners'; import { createMenu } from './menu'; @@ -82,6 +84,10 @@ export async function main(): Promise { DisabledMenuItemHandler.activateMenuItems, ), ); + ipcMain.handle( + IpcChannel.MergeFileAndLoad, + getMergeFileAndLoadListener(mainWindow), + ); ipcMain.handle(IpcChannel.SaveFile, saveFileListener(mainWindow)); ipcMain.handle(IpcChannel.ExportFile, exportFileListener(mainWindow)); ipcMain.handle(IpcChannel.StopLoading, () => diff --git a/src/ElectronBackend/preload.ts b/src/ElectronBackend/preload.ts index 7c30e367c..0b0e2e58e 100644 --- a/src/ElectronBackend/preload.ts +++ b/src/ElectronBackend/preload.ts @@ -24,6 +24,8 @@ const electronAPI: ElectronAPI = { fileType, opossumFilePath, ), + mergeFileAndLoad: (inputFilePath, fileType) => + ipcRenderer.invoke(IpcChannel.MergeFileAndLoad, inputFilePath, fileType), exportFile: (args) => ipcRenderer.invoke(IpcChannel.ExportFile, args), saveFile: (saveFileArgs) => ipcRenderer.invoke(IpcChannel.SaveFile, saveFileArgs), diff --git a/src/shared/ipc-channels.ts b/src/shared/ipc-channels.ts index 768681d31..61e04fe97 100644 --- a/src/shared/ipc-channels.ts +++ b/src/shared/ipc-channels.ts @@ -10,6 +10,7 @@ export enum IpcChannel { SelectFile = 'select-file', ImportFileSelectSaveLocation = 'import-file-select-save-location', ImportFileConvertAndLoad = 'import-file-convert-and-load', + MergeFileAndLoad = 'merge-file-and-load', OpenLink = 'open-link', SaveFile = 'save-file', /** diff --git a/src/shared/shared-types.ts b/src/shared/shared-types.ts index 711acf419..943b79308 100644 --- a/src/shared/shared-types.ts +++ b/src/shared/shared-types.ts @@ -266,6 +266,10 @@ export interface ElectronAPI { fileType: FileType, opossumFilePath: string, ) => Promise; + mergeFileAndLoad: ( + inputFilePath: string, + fileType: FileType, + ) => Promise; exportFile: (args: ExportArgsType) => void; saveFile: (saveFileArgs: SaveFileArgs) => void; /** diff --git a/src/testing/setup-tests.ts b/src/testing/setup-tests.ts index 97fc5436b..e5e68d8e1 100644 --- a/src/testing/setup-tests.ts +++ b/src/testing/setup-tests.ts @@ -39,6 +39,7 @@ global.window.electronAPI = { selectFile: jest.fn(), importFileSelectSaveLocation: jest.fn(), importFileConvertAndLoad: jest.fn(), + mergeFileAndLoad: jest.fn(), exportFile: jest.fn(), saveFile: jest.fn(), stopLoading: jest.fn(), From 1d40e531903e7276dd2edba61061e70122170a1d Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Tue, 18 Feb 2025 13:56:29 +0100 Subject: [PATCH 07/27] feat: add merge dialog * extract log subscription and display into separate component to reduce code duplication in import and merge dialogs --- src/ElectronBackend/main/listeners.ts | 7 +- .../Components/GlobalPopup/GlobalPopup.tsx | 5 + .../Components/ImportDialog/ImportDialog.tsx | 27 +---- .../LogDisplay/LogDisplayForDialog.tsx | 47 +++++++++ .../Components/MergeDialog/MergeDialog.tsx | 99 +++++++++++++++++++ src/shared/text.ts | 17 +++- 6 files changed, 175 insertions(+), 27 deletions(-) create mode 100644 src/Frontend/Components/LogDisplay/LogDisplayForDialog.tsx create mode 100644 src/Frontend/Components/MergeDialog/MergeDialog.tsx diff --git a/src/ElectronBackend/main/listeners.ts b/src/ElectronBackend/main/listeners.ts index f842bc01c..759cfba3e 100644 --- a/src/ElectronBackend/main/listeners.ts +++ b/src/ElectronBackend/main/listeners.ts @@ -32,7 +32,10 @@ import { } from '../errorHandling/errorHandling'; import { loadInputAndOutputFromFilePath } from '../input/importFromFile'; import { serializeAttributions } from '../input/parseInputData'; -import { convertToOpossum } from '../opossum-file/opossum-file'; +import { + convertToOpossum, + mergeFileIntoOpossum, +} from '../opossum-file/opossum-file'; import { writeCsvToFile } from '../output/writeCsvToFile'; import { writeSpdxFile } from '../output/writeSpdxFile'; import { GlobalBackendState, OpossumOutputFile } from '../types/types'; @@ -255,7 +258,7 @@ export function getMergeFileAndLoadListener( } logger.info('Merging input file into current .opossum file'); - await convertToOpossum( + await mergeFileIntoOpossum( inputFilePath, currentlyOpenOpossumFilePath, fileType, diff --git a/src/Frontend/Components/GlobalPopup/GlobalPopup.tsx b/src/Frontend/Components/GlobalPopup/GlobalPopup.tsx index 0d27b4cbd..b8f327ddf 100644 --- a/src/Frontend/Components/GlobalPopup/GlobalPopup.tsx +++ b/src/Frontend/Components/GlobalPopup/GlobalPopup.tsx @@ -9,6 +9,7 @@ import { getOpenPopup } from '../../state/selectors/view-selector'; import { PopupInfo } from '../../types/types'; import { ErrorPopup } from '../ErrorPopup/ErrorPopup'; import { ImportDialog } from '../ImportDialog/ImportDialog'; +import { MergeDialog } from '../MergeDialog/MergeDialog'; import { NotSavedPopup } from '../NotSavedPopup/NotSavedPopup'; import { ProjectMetadataPopup } from '../ProjectMetadataPopup/ProjectMetadataPopup'; import { ProjectStatisticsPopup } from '../ProjectStatisticsPopup/ProjectStatisticsPopup'; @@ -30,6 +31,10 @@ function getPopupComponent(popupInfo: PopupInfo | null) { return popupInfo?.fileFormat ? ( ) : null; + case PopupType.MergeDialog: + return popupInfo?.fileFormat ? ( + + ) : null; default: return null; } diff --git a/src/Frontend/Components/ImportDialog/ImportDialog.tsx b/src/Frontend/Components/ImportDialog/ImportDialog.tsx index 12b70300b..169d01738 100644 --- a/src/Frontend/Components/ImportDialog/ImportDialog.tsx +++ b/src/Frontend/Components/ImportDialog/ImportDialog.tsx @@ -2,7 +2,6 @@ // SPDX-FileCopyrightText: TNG Technology Consulting GmbH // // SPDX-License-Identifier: Apache-2.0 -import MuiBox from '@mui/material/Box'; import MuiTypography from '@mui/material/Typography'; import { useState } from 'react'; @@ -18,7 +17,7 @@ import { useIpcRenderer, } from '../../util/use-ipc-renderer'; import { FilePathInput } from '../FilePathInput/FilePathInput'; -import { LogDisplay } from '../LogDisplay/LogDisplay'; +import { LogDisplayForDialog } from '../LogDisplay/LogDisplayForDialog'; import { NotificationPopup } from '../NotificationPopup/NotificationPopup'; export interface ImportDialogProps { @@ -56,7 +55,7 @@ export const ImportDialog: React.FC = ({ fileFormat }) => { (filePath) => { if (filePath) { setInputFilePath(filePath); - setLogToDisplay(null); + dispatch(clearLogMessage()); } }, () => {}, @@ -80,7 +79,7 @@ export const ImportDialog: React.FC = ({ fileFormat }) => { (filePath) => { if (filePath) { setOpossumFilePath(filePath); - setLogToDisplay(null); + dispatch(clearLogMessage()); } }, () => {}, @@ -134,25 +133,7 @@ export const ImportDialog: React.FC = ({ fileFormat }) => { } isOpen={true} - customAction={ - logToDisplay ? ( - - - - ) : undefined - } + customAction={} leftButtonConfig={{ onClick: onConfirm, buttonText: text.buttons.import, diff --git a/src/Frontend/Components/LogDisplay/LogDisplayForDialog.tsx b/src/Frontend/Components/LogDisplay/LogDisplayForDialog.tsx new file mode 100644 index 000000000..ab2c396df --- /dev/null +++ b/src/Frontend/Components/LogDisplay/LogDisplayForDialog.tsx @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 +import MuiBox from '@mui/material/Box'; +import { useState } from 'react'; + +import { Log } from '../../../shared/shared-types'; +import { useStateEffect } from '../../state/hooks'; +import { getLogMessage } from '../../state/selectors/view-selector'; +import { LogDisplay } from './LogDisplay'; + +interface LogDisplayForDialogProps { + isLoading: boolean; +} + +export function LogDisplayForDialog({ isLoading }: LogDisplayForDialogProps) { + const [logToDisplay, setLogToDisplay] = useState(null); + + useStateEffect( + getLogMessage, + (log) => { + if (isLoading) { + setLogToDisplay(log); + } + }, + [isLoading], + ); + + return logToDisplay ? ( + + + + ) : undefined; +} diff --git a/src/Frontend/Components/MergeDialog/MergeDialog.tsx b/src/Frontend/Components/MergeDialog/MergeDialog.tsx new file mode 100644 index 000000000..da119502b --- /dev/null +++ b/src/Frontend/Components/MergeDialog/MergeDialog.tsx @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 +import MuiTypography from '@mui/material/Typography'; +import { useState } from 'react'; + +import { FileFormatInfo } from '../../../shared/shared-types'; +import { text } from '../../../shared/text'; +import { + clearLogMessage, + closePopup, +} from '../../state/actions/view-actions/view-actions'; +import { useAppDispatch } from '../../state/hooks'; +import { FilePathInput } from '../FilePathInput/FilePathInput'; +import { LogDisplayForDialog } from '../LogDisplay/LogDisplayForDialog'; +import { NotificationPopup } from '../NotificationPopup/NotificationPopup'; + +export interface MergeDialogProps { + fileFormat: FileFormatInfo; +} + +export const MergeDialog: React.FC = ({ fileFormat }) => { + const dispatch = useAppDispatch(); + + const [inputFilePath, setInputFilePath] = useState(''); + + const [isLoading, setIsLoading] = useState(false); + + function selectInputFilePath(): void { + window.electronAPI.selectFile(fileFormat).then( + (filePath) => { + if (filePath) { + setInputFilePath(filePath); + dispatch(clearLogMessage()); + } + }, + () => {}, + ); + } + + function onCancel(): void { + dispatch(closePopup()); + } + + async function onConfirm(): Promise { + setIsLoading(true); + + const success = await window.electronAPI.mergeFileAndLoad( + inputFilePath, + fileFormat.fileType, + ); + + if (success) { + dispatch(closePopup()); + } + + setIsLoading(false); + } + + return ( + + {text.mergeDialog.explanationText[0]} + + {text.mergeDialog.explanationText[1]} + + + + } + isOpen={true} + customAction={} + leftButtonConfig={{ + onClick: onConfirm, + buttonText: text.buttons.merge, + disabled: isLoading, + }} + rightButtonConfig={{ + onClick: onCancel, + buttonText: text.buttons.cancel, + color: 'secondary', + disabled: isLoading, + }} + aria-label={'merge dialog'} + /> + ); +}; diff --git a/src/shared/text.ts b/src/shared/text.ts index 235d3c835..e475d4b95 100644 --- a/src/shared/text.ts +++ b/src/shared/text.ts @@ -153,6 +153,7 @@ export const text = { search: 'Search', sort: 'Sort', import: 'Import', + merge: 'Merge', }, modifyWasPreferredPopup: { title: 'Modifying Previously Preferred Attribution', @@ -313,14 +314,26 @@ export const text = { hasBeenSelected ? `File to import (${fileFormat.extensions.map((ext) => `.${ext}`).join('/')})` : `Select file to import (${fileFormat.extensions.map((ext) => `.${ext}`).join('/')})`, - buttonTooltip: 'Select file', }, opossumFilePath: { textFieldLabel: (hasBeenSelected: boolean) => hasBeenSelected ? 'Opossum file save location' : 'Select opossum file save location', - buttonTooltip: 'Select save location', + }, + }, + mergeDialog: { + title: (fileFormat: FileFormatInfo) => + `Merge ${fileFormat.name} into current file`, + explanationText: [ + 'OpossumUI will merge the selected file into the currently open opossum file.', + 'THIS ACTION CANNOT BE UNDONE!', + ], + inputFilePath: { + textFieldLabel: (fileFormat: FileFormatInfo, hasBeenSelected: boolean) => + hasBeenSelected + ? `File to merge (${fileFormat.extensions.map((ext) => `.${ext}`).join('/')})` + : `Select file to merge (${fileFormat.extensions.map((ext) => `.${ext}`).join('/')})`, }, }, } as const; From 289a064bc81c36b9532be4d5eaa815ee105bf0eb Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Tue, 18 Feb 2025 14:04:19 +0100 Subject: [PATCH 08/27] feat: fully integrate merge dialog with unsaved check mechanism --- src/Frontend/state/actions/popup-actions/popup-actions.ts | 7 +++++++ src/Frontend/state/actions/view-actions/types.ts | 1 + src/Frontend/state/reducers/view-reducer.ts | 8 ++++++++ src/Frontend/state/selectors/view-selector.ts | 4 ++++ 4 files changed, 20 insertions(+) diff --git a/src/Frontend/state/actions/popup-actions/popup-actions.ts b/src/Frontend/state/actions/popup-actions/popup-actions.ts index 0e4beb5cc..99dae039e 100644 --- a/src/Frontend/state/actions/popup-actions/popup-actions.ts +++ b/src/Frontend/state/actions/popup-actions/popup-actions.ts @@ -18,6 +18,7 @@ import { import { getExportFileRequest, getImportFileRequest, + getMergeRequest, getOpenFileRequest, getTargetView, } from '../../selectors/view-selector'; @@ -154,6 +155,7 @@ export function proceedFromUnsavedPopup(): AppThunkAction { const targetView = getTargetView(getState()); const openFileRequest = getOpenFileRequest(getState()); const importFileRequest = getImportFileRequest(getState()); + const mergeRequest = getMergeRequest(getState()); const exportFileRequest = getExportFileRequest(getState()); dispatch(closePopup()); @@ -168,6 +170,11 @@ export function proceedFromUnsavedPopup(): AppThunkAction { dispatch(setImportFileRequest(null)); } + if (mergeRequest) { + dispatch(openPopup(PopupType.MergeDialog, undefined, mergeRequest)); + dispatch(setMergeRequest(null)); + } + if (exportFileRequest) { dispatch(exportFile(exportFileRequest)); dispatch(setExportFileRequest(null)); diff --git a/src/Frontend/state/actions/view-actions/types.ts b/src/Frontend/state/actions/view-actions/types.ts index a4d48e486..db64955e4 100644 --- a/src/Frontend/state/actions/view-actions/types.ts +++ b/src/Frontend/state/actions/view-actions/types.ts @@ -24,6 +24,7 @@ export type ViewAction = | OpenPopupAction | SetOpenFileRequestAction | SetImportFileRequestAction + | SetMergeRequestAction | SetExportFileRequestAction; export interface ResetViewStateAction { diff --git a/src/Frontend/state/reducers/view-reducer.ts b/src/Frontend/state/reducers/view-reducer.ts index 0ec1bff0c..d29677fb1 100644 --- a/src/Frontend/state/reducers/view-reducer.ts +++ b/src/Frontend/state/reducers/view-reducer.ts @@ -12,6 +12,7 @@ import { ACTION_RESET_VIEW_STATE, ACTION_SET_EXPORT_FILE_REQUEST, ACTION_SET_IMPORT_FILE_REQUEST, + ACTION_SET_MERGE_REQUEST, ACTION_SET_OPEN_FILE_REQUEST, ACTION_SET_TARGET_VIEW, ACTION_SET_VIEW, @@ -24,6 +25,7 @@ export interface ViewState { popupInfo: Array; openFileRequest: boolean; importFileRequest: FileFormatInfo | null; + mergeRequest: FileFormatInfo | null; exportFileRequest: ExportType | null; } @@ -33,6 +35,7 @@ export const initialViewState: ViewState = { popupInfo: [], openFileRequest: false, importFileRequest: null, + mergeRequest: null, exportFileRequest: null, }; @@ -77,6 +80,11 @@ export function viewState( ...state, importFileRequest: action.payload, }; + case ACTION_SET_MERGE_REQUEST: + return { + ...state, + mergeRequest: action.payload, + }; case ACTION_SET_EXPORT_FILE_REQUEST: return { ...state, diff --git a/src/Frontend/state/selectors/view-selector.ts b/src/Frontend/state/selectors/view-selector.ts index 4222d52f6..157222dec 100644 --- a/src/Frontend/state/selectors/view-selector.ts +++ b/src/Frontend/state/selectors/view-selector.ts @@ -43,6 +43,10 @@ export function getImportFileRequest(state: State): FileFormatInfo | null { return state.viewState.importFileRequest; } +export function getMergeRequest(state: State): FileFormatInfo | null { + return state.viewState.mergeRequest; +} + export function getExportFileRequest(state: State): ExportType | null { return state.viewState.exportFileRequest; } From bc639d58f0c5e84e5716b04c2537e9f2f032f1fa Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Tue, 18 Feb 2025 15:49:11 +0100 Subject: [PATCH 09/27] fix: update menu descriptions in MenuBar --- src/e2e-tests/page-objects/MenuBar.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/e2e-tests/page-objects/MenuBar.ts b/src/e2e-tests/page-objects/MenuBar.ts index 5f2f5f5c1..a86daf8ff 100644 --- a/src/e2e-tests/page-objects/MenuBar.ts +++ b/src/e2e-tests/page-objects/MenuBar.ts @@ -23,7 +23,7 @@ export class MenuBar { } async openFile(): Promise { - await clickMenuItem(this.window.app, 'label', 'Open File'); + await clickMenuItem(this.window.app, 'label', 'Open...'); } async openProjectStatistics(): Promise { @@ -34,19 +34,19 @@ export class MenuBar { await clickMenuItem( this.window.app, 'label', - 'Legacy Opossum File (.json/.json.gz)', + 'Legacy Opossum File (.json/.json.gz)...', ); } async importScanCodeFile(): Promise { - await clickMenuItem(this.window.app, 'label', 'ScanCode File (.json)'); + await clickMenuItem(this.window.app, 'label', 'ScanCode File (.json)...'); } async importOwaspDependencyScanFile(): Promise { await clickMenuItem( this.window.app, 'label', - 'OWASP Dependency-Check (.json)', + 'OWASP Dependency-Check File (.json)...', ); } From e07822dc834130a63094fa04382a53bcf8cd90d3 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Tue, 18 Feb 2025 17:20:26 +0100 Subject: [PATCH 10/27] test: add e2e tests for merge dialog --- src/ElectronBackend/main/menu/fileMenu.ts | 1 + .../Components/MergeDialog/MergeDialog.tsx | 4 + src/e2e-tests/__tests__/merge-dialog.test.ts | 111 ++++++++++++++++++ .../__tests__/updating-attributions.test.ts | 14 +++ src/e2e-tests/page-objects/MenuBar.ts | 53 +++++++-- src/e2e-tests/page-objects/MergeDialog.ts | 54 +++++++++ src/e2e-tests/utils/fixtures.ts | 11 +- 7 files changed, 238 insertions(+), 10 deletions(-) create mode 100644 src/e2e-tests/__tests__/merge-dialog.test.ts create mode 100644 src/e2e-tests/page-objects/MergeDialog.ts diff --git a/src/ElectronBackend/main/menu/fileMenu.ts b/src/ElectronBackend/main/menu/fileMenu.ts index fcb790066..5f2c67ec3 100644 --- a/src/ElectronBackend/main/menu/fileMenu.ts +++ b/src/ElectronBackend/main/menu/fileMenu.ts @@ -64,6 +64,7 @@ function getImportFile(mainWindow: Electron.CrossProcessExports.BrowserWindow) { submenu: importFileFormats.map((fileFormat) => ({ label: text.menu.fileSubmenu.importSubmenu(fileFormat), click: importFileListener(mainWindow, fileFormat), + id: `import ${fileFormat.name}`, })), }; } diff --git a/src/Frontend/Components/MergeDialog/MergeDialog.tsx b/src/Frontend/Components/MergeDialog/MergeDialog.tsx index da119502b..7ed3dacbb 100644 --- a/src/Frontend/Components/MergeDialog/MergeDialog.tsx +++ b/src/Frontend/Components/MergeDialog/MergeDialog.tsx @@ -55,6 +55,10 @@ export const MergeDialog: React.FC = ({ fileFormat }) => { dispatch(closePopup()); } + // NOTE: without a tiny delay, isLoading is sometimes set to false before the + // final log message is processed by LogDisplayForDialog, causing it to be + // missed. This seems to only happen when running the app with yarn start + await new Promise((resolve) => setTimeout(resolve, 1)); setIsLoading(false); } diff --git a/src/e2e-tests/__tests__/merge-dialog.test.ts b/src/e2e-tests/__tests__/merge-dialog.test.ts new file mode 100644 index 000000000..8f2cf95b9 --- /dev/null +++ b/src/e2e-tests/__tests__/merge-dialog.test.ts @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 +import { stubDialog } from 'electron-playwright-helpers'; + +import { faker, test } from '../utils'; + +const [resourceName] = faker.opossum.resourceName(); + +test.use({ + data: { + inputData: faker.opossum.inputData({ + resources: faker.opossum.resources({ + [resourceName]: 1, + }), + metadata: faker.opossum.metadata({ + projectId: 'test_project', + projectTitle: 'Test Project', + }), + }), + outputData: faker.opossum.outputData({}), + provideImportFiles: true, + }, +}); + +test('opens, displays and closes merge dialog', async ({ + menuBar, + mergeDialog, +}) => { + await menuBar.mergeLegacyOpossumFile(); + await mergeDialog.assert.titleIsVisible(); + + await mergeDialog.cancelButton.click(); + + await mergeDialog.assert.titleIsHidden(); +}); + +test('merges legacy opossum file', async ({ + menuBar, + mergeDialog, + resourcesTree, + window, +}) => { + await stubDialog(window.app, 'showOpenDialogSync', [ + mergeDialog.legacyFilePath, + ]); + + await menuBar.mergeLegacyOpossumFile(); + await mergeDialog.assert.titleIsVisible(); + + await mergeDialog.inputFileSelection.click(); + await mergeDialog.mergeButton.click(); + + await mergeDialog.assert.titleIsHidden(); + await resourcesTree.assert.resourceIsVisible(resourceName); +}); + +test('merges scancode file', async ({ + menuBar, + mergeDialog, + resourcesTree, + window, +}) => { + await stubDialog(window.app, 'showOpenDialogSync', [ + mergeDialog.scancodeFilePath, + ]); + + await menuBar.mergeScanCodeFile(); + await mergeDialog.assert.titleIsVisible(); + + await mergeDialog.inputFileSelection.click(); + await mergeDialog.mergeButton.click(); + + await mergeDialog.assert.titleIsHidden(); + await resourcesTree.assert.resourceIsVisible(resourceName); + await resourcesTree.assert.resourceIsVisible('src'); +}); + +test('merges OWASP file', async ({ + menuBar, + mergeDialog, + resourcesTree, + window, +}) => { + await stubDialog(window.app, 'showOpenDialogSync', [ + mergeDialog.owaspFilePath, + ]); + + await menuBar.mergeOwaspDependencyScanFile(); + await mergeDialog.assert.titleIsVisible(); + + await mergeDialog.inputFileSelection.click(); + await mergeDialog.mergeButton.click(); + + await mergeDialog.assert.titleIsHidden(); + await resourcesTree.assert.resourceIsVisible(resourceName); + await resourcesTree.assert.resourceIsVisible('contrib'); +}); + +test('shows error when no file path is set', async ({ + menuBar, + mergeDialog, +}) => { + await menuBar.mergeLegacyOpossumFile(); + await mergeDialog.assert.titleIsVisible(); + + await mergeDialog.mergeButton.click(); + + await mergeDialog.assert.showsError(); +}); diff --git a/src/e2e-tests/__tests__/updating-attributions.test.ts b/src/e2e-tests/__tests__/updating-attributions.test.ts index cae3f7e29..72793323c 100644 --- a/src/e2e-tests/__tests__/updating-attributions.test.ts +++ b/src/e2e-tests/__tests__/updating-attributions.test.ts @@ -124,6 +124,20 @@ test('warns user of unsaved changes if user attempts to import new file before s await notSavedPopup.assert.isVisible(); }); +test('warns user of unsaved changes if user attempts to merge file before saving', async ({ + attributionDetails, + notSavedPopup, + resourcesTree, + menuBar, +}) => { + const comment = faker.lorem.sentences(); + await resourcesTree.goto(resourceName1); + await attributionDetails.attributionForm.comment.fill(comment); + + await menuBar.mergeLegacyOpossumFile(); + await notSavedPopup.assert.isVisible(); +}); + test('warns user of unsaved changes if user attempts to export data before saving', async ({ attributionDetails, notSavedPopup, diff --git a/src/e2e-tests/page-objects/MenuBar.ts b/src/e2e-tests/page-objects/MenuBar.ts index a86daf8ff..3ac69287d 100644 --- a/src/e2e-tests/page-objects/MenuBar.ts +++ b/src/e2e-tests/page-objects/MenuBar.ts @@ -3,7 +3,11 @@ // // SPDX-License-Identifier: Apache-2.0 import { ElectronApplication, expect, Page } from '@playwright/test'; -import { clickMenuItem } from 'electron-playwright-helpers'; +import { + clickMenuItem, + clickMenuItemById, + findMenuItem, +} from 'electron-playwright-helpers'; export class MenuBar { private readonly window: Page & { app: ElectronApplication }; @@ -26,26 +30,59 @@ export class MenuBar { await clickMenuItem(this.window.app, 'label', 'Open...'); } + private async clickSubmenuItem( + submenuLabel: string, + itemLabel: string, + ): Promise { + const submenu = (await findMenuItem(this.window.app, 'label', submenuLabel)) + ?.submenu; + const menuItem = await findMenuItem( + this.window.app, + 'label', + itemLabel, + submenu, + ); + if (menuItem?.id) { + await clickMenuItemById(this.window.app, menuItem.id); + } + } + async openProjectStatistics(): Promise { await clickMenuItem(this.window.app, 'label', 'Project Statistics'); } async importLegacyOpossumFile(): Promise { - await clickMenuItem( - this.window.app, - 'label', + await this.clickSubmenuItem( + 'Import', 'Legacy Opossum File (.json/.json.gz)...', ); } async importScanCodeFile(): Promise { - await clickMenuItem(this.window.app, 'label', 'ScanCode File (.json)...'); + await this.clickSubmenuItem('Import', 'ScanCode File (.json...'); } async importOwaspDependencyScanFile(): Promise { - await clickMenuItem( - this.window.app, - 'label', + await this.clickSubmenuItem( + 'Import', + 'OWASP Dependency-Check File (.json)...', + ); + } + + async mergeLegacyOpossumFile(): Promise { + await this.clickSubmenuItem( + 'Merge', + 'Legacy Opossum File (.json/.json.gz)...', + ); + } + + async mergeScanCodeFile(): Promise { + await this.clickSubmenuItem('Merge', 'ScanCode File (.json)...'); + } + + async mergeOwaspDependencyScanFile(): Promise { + await this.clickSubmenuItem( + 'Merge', 'OWASP Dependency-Check File (.json)...', ); } diff --git a/src/e2e-tests/page-objects/MergeDialog.ts b/src/e2e-tests/page-objects/MergeDialog.ts new file mode 100644 index 000000000..83dc91db8 --- /dev/null +++ b/src/e2e-tests/page-objects/MergeDialog.ts @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 +import { expect, Locator, Page, TestInfo } from '@playwright/test'; +import path from 'path'; + +export class MergeDialog { + private readonly node: Locator; + readonly title: Locator; + readonly inputFileSelection: Locator; + readonly mergeButton: Locator; + readonly cancelButton: Locator; + readonly errorIcon: Locator; + + readonly legacyFilePath: string; + readonly scancodeFilePath: string; + readonly owaspFilePath: string; + + constructor( + window: Page, + legacyFilename: string | undefined, + info: TestInfo, + ) { + this.node = window.getByLabel('merge dialog'); + this.title = this.node.getByRole('heading').getByText('Merge'); + this.inputFileSelection = this.node + .getByLabel('Select file to merge') + .locator('..'); + this.mergeButton = this.node.getByRole('button', { name: 'Merge' }); + this.cancelButton = this.node.getByRole('button', { name: 'Cancel' }); + this.errorIcon = this.node.getByTestId('ErrorIcon').locator('path'); + + this.legacyFilePath = info.outputPath(`${legacyFilename}.json`); + this.scancodeFilePath = path.resolve(__dirname, '..', 'scancode.json'); + this.owaspFilePath = path.resolve( + __dirname, + '..', + 'owasp-dependency-check-report.json', + ); + } + + public assert = { + titleIsVisible: async (): Promise => { + await expect(this.title).toBeVisible(); + }, + titleIsHidden: async (): Promise => { + await expect(this.title).toBeHidden({ timeout: 10000 }); + }, + showsError: async (): Promise => { + await expect(this.errorIcon).toBeVisible(); + }, + }; +} diff --git a/src/e2e-tests/utils/fixtures.ts b/src/e2e-tests/utils/fixtures.ts index f28ed5bea..60f4dcb2c 100644 --- a/src/e2e-tests/utils/fixtures.ts +++ b/src/e2e-tests/utils/fixtures.ts @@ -30,6 +30,7 @@ import { FileSupportPopup } from '../page-objects/FileSupportPopup'; import { ImportDialog } from '../page-objects/ImportDialog'; import { LinkedResourcesTree } from '../page-objects/LinkedResourcesTree'; import { MenuBar } from '../page-objects/MenuBar'; +import { MergeDialog } from '../page-objects/MergeDialog'; import { NotSavedPopup } from '../page-objects/NotSavedPopup'; import { PathBar } from '../page-objects/PathBar'; import { ProjectMetadataPopup } from '../page-objects/ProjectMetadataPopup'; @@ -64,6 +65,7 @@ export const test = base.extend<{ errorPopup: ErrorPopup; fileSupportPopup: FileSupportPopup; importDialog: ImportDialog; + mergeDialog: MergeDialog; linkedResourcesTree: LinkedResourcesTree; menuBar: MenuBar; notSavedPopup: NotSavedPopup; @@ -186,6 +188,11 @@ export const test = base.extend<{ new ImportDialog(window, data?.inputData.metadata.projectId, info), ); }, + mergeDialog: async ({ window, data }, use, info) => { + await use( + new MergeDialog(window, data?.inputData.metadata.projectId, info), + ); + }, }); function getLaunchProps(): [executablePath: string | undefined, main: string] { @@ -212,7 +219,7 @@ function getReleasePath(): string { throw new Error('Unsupported platform'); } -function createTestFile({ +async function createTestFile({ data: { inputData, outputData, provideImportFiles }, info, }: { @@ -222,7 +229,7 @@ function createTestFile({ const filename = inputData.metadata.projectId; if (provideImportFiles) { - return writeFile({ + await writeFile({ path: info.outputPath(`${filename}.json`), content: inputData, }); From e0534fb643ed5aa77d9a732be00d43e7a1670157 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Tue, 18 Feb 2025 17:30:06 +0100 Subject: [PATCH 11/27] fix: fix typo in MenuBar --- src/e2e-tests/page-objects/MenuBar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/e2e-tests/page-objects/MenuBar.ts b/src/e2e-tests/page-objects/MenuBar.ts index 3ac69287d..00981dcf5 100644 --- a/src/e2e-tests/page-objects/MenuBar.ts +++ b/src/e2e-tests/page-objects/MenuBar.ts @@ -59,7 +59,7 @@ export class MenuBar { } async importScanCodeFile(): Promise { - await this.clickSubmenuItem('Import', 'ScanCode File (.json...'); + await this.clickSubmenuItem('Import', 'ScanCode File (.json)...'); } async importOwaspDependencyScanFile(): Promise { From acbed86af15e4b8545880f3b744665f83f7f78b3 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Wed, 19 Feb 2025 17:06:16 +0100 Subject: [PATCH 12/27] feat: update icon for merge menu item --- public/assets/icons/merge-black.png | Bin 0 -> 190 bytes public/assets/icons/merge-white.png | Bin 0 -> 208 bytes src/ElectronBackend/main/menu/fileMenu.ts | 5 +---- 3 files changed, 1 insertion(+), 4 deletions(-) create mode 100644 public/assets/icons/merge-black.png create mode 100644 public/assets/icons/merge-white.png diff --git a/public/assets/icons/merge-black.png b/public/assets/icons/merge-black.png new file mode 100644 index 0000000000000000000000000000000000000000..adde56a450d5746747e2ae30fd90f0e8dcd36bd3 GIT binary patch literal 190 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@RheR!T z25*j-iugtzu4a)0M&542BIatopfBeS%HL?=?PN=IY7;bJQR~Z;NNj#nsA{*wC97Yr mU3-}bhYJID+m8j>{S5BQ%5F)YN+M#8Zq>e( ({ label: text.menu.fileSubmenu.mergeSubmenu(fileFormat), From 3414ed3be22f1ea031acc3ed3fb8271ff7c4c604 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Wed, 19 Feb 2025 17:27:25 +0100 Subject: [PATCH 13/27] refactor: use async/await instead of Promise.then in import and merge dialog --- .../Components/ImportDialog/ImportDialog.tsx | 35 ++++++++----------- .../Components/MergeDialog/MergeDialog.tsx | 17 ++++----- 2 files changed, 22 insertions(+), 30 deletions(-) diff --git a/src/Frontend/Components/ImportDialog/ImportDialog.tsx b/src/Frontend/Components/ImportDialog/ImportDialog.tsx index 169d01738..3e9ce509f 100644 --- a/src/Frontend/Components/ImportDialog/ImportDialog.tsx +++ b/src/Frontend/Components/ImportDialog/ImportDialog.tsx @@ -50,19 +50,16 @@ export const ImportDialog: React.FC = ({ fileFormat }) => { [isLoading], ); - function selectInputFilePath(): void { - window.electronAPI.selectFile(fileFormat).then( - (filePath) => { - if (filePath) { - setInputFilePath(filePath); - dispatch(clearLogMessage()); - } - }, - () => {}, - ); + async function selectInputFilePath(): Promise { + const filePath = await window.electronAPI.selectFile(fileFormat); + + if (filePath) { + setInputFilePath(filePath); + dispatch(clearLogMessage()); + } } - function selectOpossumFilePath(): void { + async function selectOpossumFilePath(): Promise { let defaultPath = 'imported.opossum'; const derivedPath = getDotOpossumFilePath( inputFilePath, @@ -75,15 +72,13 @@ export const ImportDialog: React.FC = ({ fileFormat }) => { defaultPath = derivedPath; } - window.electronAPI.importFileSelectSaveLocation(defaultPath).then( - (filePath) => { - if (filePath) { - setOpossumFilePath(filePath); - dispatch(clearLogMessage()); - } - }, - () => {}, - ); + const filePath = + await window.electronAPI.importFileSelectSaveLocation(defaultPath); + + if (filePath) { + setOpossumFilePath(filePath); + dispatch(clearLogMessage()); + } } function onCancel(): void { diff --git a/src/Frontend/Components/MergeDialog/MergeDialog.tsx b/src/Frontend/Components/MergeDialog/MergeDialog.tsx index 7ed3dacbb..055bfa47d 100644 --- a/src/Frontend/Components/MergeDialog/MergeDialog.tsx +++ b/src/Frontend/Components/MergeDialog/MergeDialog.tsx @@ -27,16 +27,13 @@ export const MergeDialog: React.FC = ({ fileFormat }) => { const [isLoading, setIsLoading] = useState(false); - function selectInputFilePath(): void { - window.electronAPI.selectFile(fileFormat).then( - (filePath) => { - if (filePath) { - setInputFilePath(filePath); - dispatch(clearLogMessage()); - } - }, - () => {}, - ); + async function selectInputFilePath(): Promise { + const filePath = await window.electronAPI.selectFile(fileFormat); + + if (filePath) { + setInputFilePath(filePath); + dispatch(clearLogMessage()); + } } function onCancel(): void { From 92b2045af9011702998b847af70583553b98325c Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Wed, 19 Feb 2025 18:17:26 +0100 Subject: [PATCH 14/27] feat: implement some review suggestions --- .../Components/LogDisplay/LogDisplayForDialog.tsx | 1 + .../Components/MergeDialog/MergeDialog.tsx | 8 +++----- src/shared/text.ts | 14 +++++++------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/Frontend/Components/LogDisplay/LogDisplayForDialog.tsx b/src/Frontend/Components/LogDisplay/LogDisplayForDialog.tsx index ab2c396df..dfc1eaaed 100644 --- a/src/Frontend/Components/LogDisplay/LogDisplayForDialog.tsx +++ b/src/Frontend/Components/LogDisplay/LogDisplayForDialog.tsx @@ -31,6 +31,7 @@ export function LogDisplayForDialog({ isLoading }: LogDisplayForDialogProps) { = ({ fileFormat }) => { header={text.mergeDialog.title(fileFormat)} width={'80vw'} minWidth={'300px'} - maxWidth={'700px'} + maxWidth={'730px'} content={
- {text.mergeDialog.explanationText[0]} - - {text.mergeDialog.explanationText[1]} - + {text.mergeDialog.explanationText} + {text.mergeDialog.warningText} `Import ${fileFormat.name}`, explanationText: [ - 'OpossumUI will convert the selected file into a new opossum file.', - 'All changes made to the project in OpossumUI will be saved in this opossum file.', + 'OpossumUI will convert the selected file into a new Opossum file.', + 'All changes made to the project in OpossumUI will be saved in this Opossum file.', ], inputFilePath: { textFieldLabel: (fileFormat: FileFormatInfo, hasBeenSelected: boolean) => @@ -319,16 +319,16 @@ export const text = { textFieldLabel: (hasBeenSelected: boolean) => hasBeenSelected ? 'Opossum file save location' - : 'Select opossum file save location', + : 'Select Opossum file save location', }, }, mergeDialog: { title: (fileFormat: FileFormatInfo) => `Merge ${fileFormat.name} into current file`, - explanationText: [ - 'OpossumUI will merge the selected file into the currently open opossum file.', - 'THIS ACTION CANNOT BE UNDONE!', - ], + explanationText: + 'OpossumUI will merge the selected file into the currently open Opossum file.', + warningText: + 'As this action cannot be undone, OpossumUI will also create a backup of the currently open Opossum file.', inputFilePath: { textFieldLabel: (fileFormat: FileFormatInfo, hasBeenSelected: boolean) => hasBeenSelected From 475159a22ae01dcbc4053a80dbe4ee759a547dc6 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Wed, 19 Feb 2025 18:26:17 +0100 Subject: [PATCH 15/27] fix: capitalization issues --- src/ElectronBackend/main/dialogs.ts | 2 +- src/ElectronBackend/main/menu/fileMenu.ts | 6 +++--- src/shared/text.ts | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/ElectronBackend/main/dialogs.ts b/src/ElectronBackend/main/dialogs.ts index 88ceb64ad..079dc8380 100644 --- a/src/ElectronBackend/main/dialogs.ts +++ b/src/ElectronBackend/main/dialogs.ts @@ -32,7 +32,7 @@ export function openNonOpossumFileDialog( ): Array | undefined { return openFileDialog([ { - name: `${fileFormat.name}s (${fileFormat.extensions.map((ext) => `.${ext}`).join('/')})`, + name: `${fileFormat.name} Files (${fileFormat.extensions.map((ext) => `.${ext}`).join('/')})`, extensions: fileFormat.extensions, }, ]); diff --git a/src/ElectronBackend/main/menu/fileMenu.ts b/src/ElectronBackend/main/menu/fileMenu.ts index 524192542..3a8c8f38d 100644 --- a/src/ElectronBackend/main/menu/fileMenu.ts +++ b/src/ElectronBackend/main/menu/fileMenu.ts @@ -27,17 +27,17 @@ import { DisabledMenuItemHandler } from './DisabledMenuItemHandler'; export const importFileFormats: Array = [ { fileType: FileType.LEGACY_OPOSSUM, - name: 'Legacy Opossum File', + name: 'Legacy Opossum', extensions: ['json', 'json.gz'], }, { fileType: FileType.SCANCODE_JSON, - name: 'ScanCode File', + name: 'ScanCode', extensions: ['json'], }, { fileType: FileType.OWASP_JSON, - name: 'OWASP Dependency-Check File', + name: 'OWASP Dependency-Check', extensions: ['json'], }, ]; diff --git a/src/shared/text.ts b/src/shared/text.ts index bec25be83..e254e24bb 100644 --- a/src/shared/text.ts +++ b/src/shared/text.ts @@ -5,7 +5,7 @@ import { FileFormatInfo } from './shared-types'; function menuLabelForFileFormat(fileFormat: FileFormatInfo): string { - return `${fileFormat.name} (${fileFormat.extensions.map((ext) => `.${ext}`).join('/')})...`; + return `${fileFormat.name} File (${fileFormat.extensions.map((ext) => `.${ext}`).join('/')})...`; } export const text = { @@ -25,8 +25,8 @@ export const text = { export: 'Export', exportSubmenu: { followUp: 'Follow-Up', - compactComponentList: 'Compact component list', - detailedComponentList: 'Detailed component list', + compactComponentList: 'Compact Component List', + detailedComponentList: 'Detailed Component List', spdxYAML: 'SPDX (yaml)', spdxJSON: 'SPDX (json)', }, @@ -60,8 +60,8 @@ export const text = { }, help: 'Help', helpSubmenu: { - openLogFiles: 'Open log files folder', - checkForUpdates: 'Check For Updates', + openLogFiles: 'Open Log Files Folder', + checkForUpdates: 'Check for Updates', userGuide: 'User Guide', }, }, @@ -304,7 +304,7 @@ export const text = { quit: 'Quit App', }, importDialog: { - title: (fileFormat: FileFormatInfo) => `Import ${fileFormat.name}`, + title: (fileFormat: FileFormatInfo) => `Import ${fileFormat.name} file`, explanationText: [ 'OpossumUI will convert the selected file into a new Opossum file.', 'All changes made to the project in OpossumUI will be saved in this Opossum file.', @@ -324,7 +324,7 @@ export const text = { }, mergeDialog: { title: (fileFormat: FileFormatInfo) => - `Merge ${fileFormat.name} into current file`, + `Merge ${fileFormat.name} file into current file`, explanationText: 'OpossumUI will merge the selected file into the currently open Opossum file.', warningText: From 101cdecec9aecf12c29d9edf5de832386c26b55e Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Wed, 19 Feb 2025 18:40:35 +0100 Subject: [PATCH 16/27] feat: improve error reporting during import/merge * check relevant file permissions before running opossum-file * assume that all errors happening during opossum-file execution can be interpreted as the input file being invalid --- src/ElectronBackend/main/listeners.ts | 18 ++++++++++++++++++ .../opossum-file/ExternalFileConverter.ts | 4 +--- .../opossum-file/FileConverter.ts | 12 +++++------- .../opossum-file/LegacyFileConverter.ts | 18 ++++++++---------- .../opossum-file/opossum-file.ts | 2 +- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/ElectronBackend/main/listeners.ts b/src/ElectronBackend/main/listeners.ts index 759cfba3e..9e47c6531 100644 --- a/src/ElectronBackend/main/listeners.ts +++ b/src/ElectronBackend/main/listeners.ts @@ -202,6 +202,12 @@ export const importFileConvertAndLoadListener = throw new Error('Input file does not exist'); } + try { + fs.accessSync(resourceFilePath, fs.constants.R_OK); + } catch (error) { + throw new Error('Permission error: cannot read input file'); + } + if (!opossumFilePath.trim()) { throw new Error('No .opossum save location selected'); } @@ -214,6 +220,12 @@ export const importFileConvertAndLoadListener = throw new Error('Output directory does not exist'); } + try { + fs.accessSync(path.dirname(opossumFilePath), fs.constants.W_OK); + } catch (error) { + throw new Error('Permission error: cannot write to output directory'); + } + logger.info('Converting input file to .opossum format'); await convertToOpossum(resourceFilePath, opossumFilePath, fileType); @@ -250,6 +262,12 @@ export function getMergeFileAndLoadListener( throw new Error('Input file does not exist'); } + try { + fs.accessSync(inputFilePath, fs.constants.R_OK); + } catch (error) { + throw new Error('Permission error: cannot read input file'); + } + const currentlyOpenOpossumFilePath = getGlobalBackendState().opossumFilePath; diff --git a/src/ElectronBackend/opossum-file/ExternalFileConverter.ts b/src/ElectronBackend/opossum-file/ExternalFileConverter.ts index 72a5ccb0f..a23e4cf8c 100644 --- a/src/ElectronBackend/opossum-file/ExternalFileConverter.ts +++ b/src/ElectronBackend/opossum-file/ExternalFileConverter.ts @@ -22,9 +22,7 @@ export abstract class ExternalFileConverter extends FileConverter { toBeConvertedFilePath, ]); } catch (error) { - throw new Error( - `Conversion of ${this.fileTypeName} file to .opossum file failed`, - ); + throw new Error(`Input file is not a valid ${this.fileTypeName} file`); } } } diff --git a/src/ElectronBackend/opossum-file/FileConverter.ts b/src/ElectronBackend/opossum-file/FileConverter.ts index 60f942a35..29562c9d0 100644 --- a/src/ElectronBackend/opossum-file/FileConverter.ts +++ b/src/ElectronBackend/opossum-file/FileConverter.ts @@ -47,14 +47,12 @@ export abstract class FileConverter { '--opossum', opossumFilePath, ]); - - if (preConvertedFilePath) { - fs.rmSync(preConvertedFilePath); - } } catch (error) { - throw new Error( - `Merging ${this.fileTypeName} file into current file failed`, - ); + throw new Error(`Input file is not a valid ${this.fileTypeName} file`); + } + + if (preConvertedFilePath) { + fs.rmSync(preConvertedFilePath); } } } diff --git a/src/ElectronBackend/opossum-file/LegacyFileConverter.ts b/src/ElectronBackend/opossum-file/LegacyFileConverter.ts index 627f9adce..5753acfcc 100644 --- a/src/ElectronBackend/opossum-file/LegacyFileConverter.ts +++ b/src/ElectronBackend/opossum-file/LegacyFileConverter.ts @@ -67,24 +67,22 @@ export class LegacyFileConverter extends FileConverter { toBeConvertedFilePath: string, opossumSaveLocation: string, ): Promise { - try { - let pathToInputJson = toBeConvertedFilePath; + let pathToInputJson = toBeConvertedFilePath; - if (toBeConvertedFilePath.endsWith(legacyOutputFileEnding)) { - pathToInputJson = this.tryToGetLegacyInputJsonFromLegacyOutputJson( - toBeConvertedFilePath, - ); - } + if (toBeConvertedFilePath.endsWith(legacyOutputFileEnding)) { + pathToInputJson = this.tryToGetLegacyInputJsonFromLegacyOutputJson( + toBeConvertedFilePath, + ); + } + try { await writeOpossumFile({ path: opossumSaveLocation, input: this.readInputJson(pathToInputJson), output: this.readOutputJson(pathToInputJson), }); } catch (error) { - throw new Error( - 'Conversion of Legacy Opossum file to .opossum file failed', - ); + throw new Error('Input file is not a valid Legacy Opossum file'); } } diff --git a/src/ElectronBackend/opossum-file/opossum-file.ts b/src/ElectronBackend/opossum-file/opossum-file.ts index 79a5c5138..169417ef5 100644 --- a/src/ElectronBackend/opossum-file/opossum-file.ts +++ b/src/ElectronBackend/opossum-file/opossum-file.ts @@ -15,7 +15,7 @@ const fileTypeToConverter: Record = { })(), [FileType.OWASP_JSON]: new (class extends ExternalFileConverter { readonly fileTypeSwitch: string = '--owasp-json'; - readonly fileTypeName: string = 'OWASP Dependency-check'; + readonly fileTypeName: string = 'OWASP Dependency-Check'; })(), }; From 91354232d905310ee0ccaa29b64801dad8c09d59 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Wed, 19 Feb 2025 18:53:45 +0100 Subject: [PATCH 17/27] feat: automatically create backup of current file before merging --- src/ElectronBackend/main/listeners.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/ElectronBackend/main/listeners.ts b/src/ElectronBackend/main/listeners.ts index 9e47c6531..4ca57eca6 100644 --- a/src/ElectronBackend/main/listeners.ts +++ b/src/ElectronBackend/main/listeners.ts @@ -275,6 +275,17 @@ export function getMergeFileAndLoadListener( throw new Error('No open file to merge into'); } + try { + fs.copyFileSync( + currentlyOpenOpossumFilePath, + `${currentlyOpenOpossumFilePath}.backup`, + ); + } catch (error) { + throw new Error( + 'Unable to create backup of currently open Opossum file', + ); + } + logger.info('Merging input file into current .opossum file'); await mergeFileIntoOpossum( inputFilePath, From f6396ff95bda1458547db8b75876cbb59fa2fd69 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Thu, 20 Feb 2025 10:30:27 +0100 Subject: [PATCH 18/27] refactor: variable renaming --- src/ElectronBackend/main/listeners.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/ElectronBackend/main/listeners.ts b/src/ElectronBackend/main/listeners.ts index 4ca57eca6..c559152ce 100644 --- a/src/ElectronBackend/main/listeners.ts +++ b/src/ElectronBackend/main/listeners.ts @@ -268,17 +268,16 @@ export function getMergeFileAndLoadListener( throw new Error('Permission error: cannot read input file'); } - const currentlyOpenOpossumFilePath = - getGlobalBackendState().opossumFilePath; + const currentOpossumFilePath = getGlobalBackendState().opossumFilePath; - if (!currentlyOpenOpossumFilePath) { + if (!currentOpossumFilePath) { throw new Error('No open file to merge into'); } try { fs.copyFileSync( - currentlyOpenOpossumFilePath, - `${currentlyOpenOpossumFilePath}.backup`, + currentOpossumFilePath, + `${currentOpossumFilePath}.backup`, ); } catch (error) { throw new Error( @@ -289,11 +288,11 @@ export function getMergeFileAndLoadListener( logger.info('Merging input file into current .opossum file'); await mergeFileIntoOpossum( inputFilePath, - currentlyOpenOpossumFilePath, + currentOpossumFilePath, fileType, ); - await openFile(mainWindow, currentlyOpenOpossumFilePath, () => {}, true); + await openFile(mainWindow, currentOpossumFilePath, () => {}, true); return true; }, From 7865c57dc685ff254936e245d872accb56c72045 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Thu, 20 Feb 2025 14:08:51 +0100 Subject: [PATCH 19/27] refactor: remove LogDisplayForDialog component --- .../Components/ImportDialog/ImportDialog.tsx | 32 ++++++++++++- .../Components/LogDisplay/LogDisplay.tsx | 11 +++-- .../LogDisplay/LogDisplayForDialog.tsx | 48 ------------------- .../Components/MergeDialog/MergeDialog.tsx | 37 ++++++++++++-- 4 files changed, 70 insertions(+), 58 deletions(-) delete mode 100644 src/Frontend/Components/LogDisplay/LogDisplayForDialog.tsx diff --git a/src/Frontend/Components/ImportDialog/ImportDialog.tsx b/src/Frontend/Components/ImportDialog/ImportDialog.tsx index 3e9ce509f..2dedbf5e9 100644 --- a/src/Frontend/Components/ImportDialog/ImportDialog.tsx +++ b/src/Frontend/Components/ImportDialog/ImportDialog.tsx @@ -17,7 +17,7 @@ import { useIpcRenderer, } from '../../util/use-ipc-renderer'; import { FilePathInput } from '../FilePathInput/FilePathInput'; -import { LogDisplayForDialog } from '../LogDisplay/LogDisplayForDialog'; +import { LogDisplay } from '../LogDisplay/LogDisplay'; import { NotificationPopup } from '../NotificationPopup/NotificationPopup'; export interface ImportDialogProps { @@ -50,6 +50,18 @@ export const ImportDialog: React.FC = ({ fileFormat }) => { [isLoading], ); + const [logToDisplay, setLogToDisplay] = useState(null); + + useStateEffect( + getLogMessage, + (log) => { + if (isLoading) { + setLogToDisplay(log); + } + }, + [isLoading], + ); + async function selectInputFilePath(): Promise { const filePath = await window.electronAPI.selectFile(fileFormat); @@ -128,7 +140,23 @@ export const ImportDialog: React.FC = ({ fileFormat }) => {
} isOpen={true} - customAction={} + customAction={ + logToDisplay ? ( + + ) : undefined + } leftButtonConfig={{ onClick: onConfirm, buttonText: text.buttons.import, diff --git a/src/Frontend/Components/LogDisplay/LogDisplay.tsx b/src/Frontend/Components/LogDisplay/LogDisplay.tsx index fdffa9007..e58e919ce 100644 --- a/src/Frontend/Components/LogDisplay/LogDisplay.tsx +++ b/src/Frontend/Components/LogDisplay/LogDisplay.tsx @@ -5,10 +5,12 @@ import CheckIcon from '@mui/icons-material/Check'; import ErrorIcon from '@mui/icons-material/Error'; import WarningIcon from '@mui/icons-material/Warning'; +import MuiBox from '@mui/material/Box'; import { SvgIconProps } from '@mui/material/SvgIcon'; import MuiTypography from '@mui/material/Typography'; +import { SxProps } from '@mui/system'; import dayjs from 'dayjs'; -import { Fragment, useMemo } from 'react'; +import { useMemo } from 'react'; import { Log } from '../../../shared/shared-types'; import { baseIcon } from '../../shared-styles'; @@ -29,10 +31,11 @@ interface LogDisplayProps { isInProgress: boolean; showDate: boolean; useEllipsis?: boolean; + sx?: SxProps; } export function LogDisplay(props: LogDisplayProps) { - const { log, isInProgress, showDate, useEllipsis } = props; + const { log, isInProgress, showDate, useEllipsis, sx } = props; const icon = useMemo(() => { const { color, Component } = icons[log.level]; @@ -44,7 +47,7 @@ export function LogDisplay(props: LogDisplayProps) { }, [log, isInProgress]); return ( - + {icon} {showDate ? ( @@ -56,6 +59,6 @@ export function LogDisplay(props: LogDisplayProps) { ) : ( {log.message} )} - +
); } diff --git a/src/Frontend/Components/LogDisplay/LogDisplayForDialog.tsx b/src/Frontend/Components/LogDisplay/LogDisplayForDialog.tsx deleted file mode 100644 index dfc1eaaed..000000000 --- a/src/Frontend/Components/LogDisplay/LogDisplayForDialog.tsx +++ /dev/null @@ -1,48 +0,0 @@ -// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates -// SPDX-FileCopyrightText: TNG Technology Consulting GmbH -// -// SPDX-License-Identifier: Apache-2.0 -import MuiBox from '@mui/material/Box'; -import { useState } from 'react'; - -import { Log } from '../../../shared/shared-types'; -import { useStateEffect } from '../../state/hooks'; -import { getLogMessage } from '../../state/selectors/view-selector'; -import { LogDisplay } from './LogDisplay'; - -interface LogDisplayForDialogProps { - isLoading: boolean; -} - -export function LogDisplayForDialog({ isLoading }: LogDisplayForDialogProps) { - const [logToDisplay, setLogToDisplay] = useState(null); - - useStateEffect( - getLogMessage, - (log) => { - if (isLoading) { - setLogToDisplay(log); - } - }, - [isLoading], - ); - - return logToDisplay ? ( - - - - ) : undefined; -} diff --git a/src/Frontend/Components/MergeDialog/MergeDialog.tsx b/src/Frontend/Components/MergeDialog/MergeDialog.tsx index fa4150039..9f9f5dc30 100644 --- a/src/Frontend/Components/MergeDialog/MergeDialog.tsx +++ b/src/Frontend/Components/MergeDialog/MergeDialog.tsx @@ -5,15 +5,16 @@ import MuiTypography from '@mui/material/Typography'; import { useState } from 'react'; -import { FileFormatInfo } from '../../../shared/shared-types'; +import { FileFormatInfo, Log } from '../../../shared/shared-types'; import { text } from '../../../shared/text'; import { clearLogMessage, closePopup, } from '../../state/actions/view-actions/view-actions'; -import { useAppDispatch } from '../../state/hooks'; +import { useAppDispatch, useStateEffect } from '../../state/hooks'; +import { getLogMessage } from '../../state/selectors/view-selector'; import { FilePathInput } from '../FilePathInput/FilePathInput'; -import { LogDisplayForDialog } from '../LogDisplay/LogDisplayForDialog'; +import { LogDisplay } from '../LogDisplay/LogDisplay'; import { NotificationPopup } from '../NotificationPopup/NotificationPopup'; export interface MergeDialogProps { @@ -27,6 +28,18 @@ export const MergeDialog: React.FC = ({ fileFormat }) => { const [isLoading, setIsLoading] = useState(false); + const [logToDisplay, setLogToDisplay] = useState(null); + + useStateEffect( + getLogMessage, + (log) => { + if (isLoading) { + setLogToDisplay(log); + } + }, + [isLoading], + ); + async function selectInputFilePath(): Promise { const filePath = await window.electronAPI.selectFile(fileFormat); @@ -80,7 +93,23 @@ export const MergeDialog: React.FC = ({ fileFormat }) => { } isOpen={true} - customAction={} + customAction={ + logToDisplay ? ( + + ) : undefined + } leftButtonConfig={{ onClick: onConfirm, buttonText: text.buttons.merge, From db6f6c40a9cdd6ae99c3419fbfc79b716c9152c0 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Thu, 20 Feb 2025 14:25:54 +0100 Subject: [PATCH 20/27] refactor: collect error message strings in text.ts --- src/ElectronBackend/main/listeners.ts | 23 +++++++++---------- .../opossum-file/ExternalFileConverter.ts | 3 ++- .../opossum-file/FileConverter.ts | 4 +++- .../opossum-file/LegacyFileConverter.ts | 3 ++- src/shared/text.ts | 13 +++++++++++ 5 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/ElectronBackend/main/listeners.ts b/src/ElectronBackend/main/listeners.ts index c559152ce..27e420a13 100644 --- a/src/ElectronBackend/main/listeners.ts +++ b/src/ElectronBackend/main/listeners.ts @@ -24,6 +24,7 @@ import { PackageInfo, SaveFileArgs, } from '../../shared/shared-types'; +import { text } from '../../shared/text'; import { writeFile, writeOpossumFile } from '../../shared/write-file'; import { LoadedFileFormat } from '../enums/enums'; import { @@ -199,31 +200,31 @@ export const importFileConvertAndLoadListener = try { if (!resourceFilePath.trim() || !fs.existsSync(resourceFilePath)) { - throw new Error('Input file does not exist'); + throw new Error(text.backendError.inputFileDoesNotExist); } try { fs.accessSync(resourceFilePath, fs.constants.R_OK); } catch (error) { - throw new Error('Permission error: cannot read input file'); + throw new Error(text.backendError.inputFilePermissionError); } if (!opossumFilePath.trim()) { - throw new Error('No .opossum save location selected'); + throw new Error(text.backendError.opossumFileNotSelected); } if (!opossumFilePath.endsWith('.opossum')) { - throw new Error('Output file name must have .opossum extension'); + throw new Error(text.backendError.opossumFileWrongExtension); } if (!fs.existsSync(path.dirname(opossumFilePath))) { - throw new Error('Output directory does not exist'); + throw new Error(text.backendError.opossumFileDirectoryDoesNotExist); } try { fs.accessSync(path.dirname(opossumFilePath), fs.constants.W_OK); } catch (error) { - throw new Error('Permission error: cannot write to output directory'); + throw new Error(text.backendError.opossumFilePermissionError); } logger.info('Converting input file to .opossum format'); @@ -259,19 +260,19 @@ export function getMergeFileAndLoadListener( fileType: FileType, ) => { if (!inputFilePath.trim() || !fs.existsSync(inputFilePath)) { - throw new Error('Input file does not exist'); + throw new Error(text.backendError.inputFileDoesNotExist); } try { fs.accessSync(inputFilePath, fs.constants.R_OK); } catch (error) { - throw new Error('Permission error: cannot read input file'); + throw new Error(text.backendError.inputFilePermissionError); } const currentOpossumFilePath = getGlobalBackendState().opossumFilePath; if (!currentOpossumFilePath) { - throw new Error('No open file to merge into'); + throw new Error(text.backendError.noOpenFileToMergeInto); } try { @@ -280,9 +281,7 @@ export function getMergeFileAndLoadListener( `${currentOpossumFilePath}.backup`, ); } catch (error) { - throw new Error( - 'Unable to create backup of currently open Opossum file', - ); + throw new Error(text.backendError.cantCreateBackup); } logger.info('Merging input file into current .opossum file'); diff --git a/src/ElectronBackend/opossum-file/ExternalFileConverter.ts b/src/ElectronBackend/opossum-file/ExternalFileConverter.ts index a23e4cf8c..19072eda1 100644 --- a/src/ElectronBackend/opossum-file/ExternalFileConverter.ts +++ b/src/ElectronBackend/opossum-file/ExternalFileConverter.ts @@ -2,6 +2,7 @@ // SPDX-FileCopyrightText: TNG Technology Consulting GmbH // // SPDX-License-Identifier: Apache-2.0 +import { text } from '../../shared/text'; import { FileConverter } from './FileConverter'; export abstract class ExternalFileConverter extends FileConverter { @@ -22,7 +23,7 @@ export abstract class ExternalFileConverter extends FileConverter { toBeConvertedFilePath, ]); } catch (error) { - throw new Error(`Input file is not a valid ${this.fileTypeName} file`); + throw new Error(text.backendError.inputFileInvalid(this.fileTypeName)); } } } diff --git a/src/ElectronBackend/opossum-file/FileConverter.ts b/src/ElectronBackend/opossum-file/FileConverter.ts index 29562c9d0..797acd1dc 100644 --- a/src/ElectronBackend/opossum-file/FileConverter.ts +++ b/src/ElectronBackend/opossum-file/FileConverter.ts @@ -8,6 +8,8 @@ import fs from 'fs'; import { join } from 'path'; import { promisify } from 'util'; +import { text } from '../../shared/text'; + export abstract class FileConverter { protected abstract readonly fileTypeSwitch: string; protected abstract readonly fileTypeName: string; @@ -48,7 +50,7 @@ export abstract class FileConverter { opossumFilePath, ]); } catch (error) { - throw new Error(`Input file is not a valid ${this.fileTypeName} file`); + throw new Error(text.backendError.inputFileInvalid(this.fileTypeName)); } if (preConvertedFilePath) { diff --git a/src/ElectronBackend/opossum-file/LegacyFileConverter.ts b/src/ElectronBackend/opossum-file/LegacyFileConverter.ts index 5753acfcc..60bcd518e 100644 --- a/src/ElectronBackend/opossum-file/LegacyFileConverter.ts +++ b/src/ElectronBackend/opossum-file/LegacyFileConverter.ts @@ -8,6 +8,7 @@ import path from 'path'; import zlib from 'zlib'; import { legacyOutputFileEnding } from '../../Frontend/shared-constants'; +import { text } from '../../shared/text'; import { writeOpossumFile } from '../../shared/write-file'; import { getFilePathWithAppendix } from '../utils/getFilePathWithAppendix'; import { FileConverter } from './FileConverter'; @@ -82,7 +83,7 @@ export class LegacyFileConverter extends FileConverter { output: this.readOutputJson(pathToInputJson), }); } catch (error) { - throw new Error('Input file is not a valid Legacy Opossum file'); + throw new Error(text.backendError.inputFileInvalid(this.fileTypeName)); } } diff --git a/src/shared/text.ts b/src/shared/text.ts index e254e24bb..d50ca0561 100644 --- a/src/shared/text.ts +++ b/src/shared/text.ts @@ -336,4 +336,17 @@ export const text = { : `Select file to merge (${fileFormat.extensions.map((ext) => `.${ext}`).join('/')})`, }, }, + backendError: { + inputFileDoesNotExist: 'Input file does not exist', + inputFilePermissionError: 'Permission error: cannot read input file', + inputFileInvalid: (fileTypeName: string) => + `Input file is not a valid ${fileTypeName} file`, + opossumFileNotSelected: 'No .opossum save location selected', + opossumFileWrongExtension: 'Output file name must have .opossum extension', + opossumFileDirectoryDoesNotExist: 'Output directory does not exist', + opossumFilePermissionError: + 'Permission error: cannot write to output directory', + noOpenFileToMergeInto: 'No open file to merge into', + cantCreateBackup: 'Unable to create backup of currently open Opossum file', + }, } as const; From 9fc151bb7b9d1f00ab5bb2cb20f0f5cbd6aa9932 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Mon, 24 Feb 2025 18:51:25 +0100 Subject: [PATCH 21/27] fix: post-rebase cleanup --- src/ElectronBackend/main/listeners.ts | 51 ++++++++----------- src/ElectronBackend/main/main.ts | 6 +-- .../Components/ImportDialog/ImportDialog.tsx | 16 +----- .../Components/MergeDialog/MergeDialog.tsx | 18 +++---- 4 files changed, 33 insertions(+), 58 deletions(-) diff --git a/src/ElectronBackend/main/listeners.ts b/src/ElectronBackend/main/listeners.ts index 27e420a13..e43603fff 100644 --- a/src/ElectronBackend/main/listeners.ts +++ b/src/ElectronBackend/main/listeners.ts @@ -139,20 +139,15 @@ export const importFileListener = AllowedFrontendChannels.ShowImportDialog, fileFormat, ); - }); -} + }; -export function getMergeListener( - mainWindow: BrowserWindow, - fileFormat: FileFormatInfo, -): () => Promise { - return createVoidListenerCallbackWithErrorHandling(mainWindow, () => { +export const getMergeListener = + (mainWindow: BrowserWindow, fileFormat: FileFormatInfo) => (): void => { mainWindow.webContents.send( AllowedFrontendChannels.ShowMergeDialog, fileFormat, ); - }); -} + }; export const selectFileListener = (mainWindow: BrowserWindow) => @@ -244,21 +239,16 @@ export const importFileConvertAndLoadListener = } }; -export function getMergeFileAndLoadListener( - mainWindow: BrowserWindow, -): ( - _: Electron.IpcMainInvokeEvent, - inputFilePath: string, - fileType: FileType, -) => Promise { - return createListenerCallbackWithErrorHandling( - mainWindow, - false, - async ( - _: Electron.IpcMainInvokeEvent, - inputFilePath: string, - fileType: FileType, - ) => { +export const getMergeFileAndLoadListener = + (mainWindow: BrowserWindow) => + async ( + _: Electron.IpcMainInvokeEvent, + inputFilePath: string, + fileType: FileType, + ): Promise => { + setLoadingState(mainWindow.webContents, true); + + try { if (!inputFilePath.trim() || !fs.existsSync(inputFilePath)) { throw new Error(text.backendError.inputFileDoesNotExist); } @@ -291,13 +281,16 @@ export function getMergeFileAndLoadListener( fileType, ); - await openFile(mainWindow, currentOpossumFilePath, () => {}, true); + await openFile(mainWindow, currentOpossumFilePath, () => {}); return true; - }, - ListenerErrorReporting.SendToFrontend, - ); -} + } catch (error) { + sendListenerErrorToFrontend(mainWindow, error); + return false; + } finally { + setLoadingState(mainWindow.webContents, false); + } + }; function initializeGlobalBackendState( filePath: string, diff --git a/src/ElectronBackend/main/main.ts b/src/ElectronBackend/main/main.ts index 6c271ea31..9a64e2888 100644 --- a/src/ElectronBackend/main/main.ts +++ b/src/ElectronBackend/main/main.ts @@ -17,7 +17,6 @@ import { openLinkListener, saveFileListener, selectFileListener, - selectInputListener, } from './listeners'; import { createMenu } from './menu'; import { DisabledMenuItemHandler } from './menu/DisabledMenuItemHandler'; @@ -67,10 +66,7 @@ export async function main(): Promise { }); ipcMain.handle( IpcChannel.OpenFile, - openFileListener( - mainWindow, - DisabledMenuItemHandler.activateMenuItems, - ), + openFileListener(mainWindow, DisabledMenuItemHandler.activateMenuItems), ); ipcMain.handle(IpcChannel.SelectFile, selectFileListener(mainWindow)); ipcMain.handle( diff --git a/src/Frontend/Components/ImportDialog/ImportDialog.tsx b/src/Frontend/Components/ImportDialog/ImportDialog.tsx index 2dedbf5e9..d3413db05 100644 --- a/src/Frontend/Components/ImportDialog/ImportDialog.tsx +++ b/src/Frontend/Components/ImportDialog/ImportDialog.tsx @@ -50,24 +50,12 @@ export const ImportDialog: React.FC = ({ fileFormat }) => { [isLoading], ); - const [logToDisplay, setLogToDisplay] = useState(null); - - useStateEffect( - getLogMessage, - (log) => { - if (isLoading) { - setLogToDisplay(log); - } - }, - [isLoading], - ); - async function selectInputFilePath(): Promise { const filePath = await window.electronAPI.selectFile(fileFormat); if (filePath) { setInputFilePath(filePath); - dispatch(clearLogMessage()); + setLogToDisplay(null); } } @@ -89,7 +77,7 @@ export const ImportDialog: React.FC = ({ fileFormat }) => { if (filePath) { setOpossumFilePath(filePath); - dispatch(clearLogMessage()); + setLogToDisplay(null); } } diff --git a/src/Frontend/Components/MergeDialog/MergeDialog.tsx b/src/Frontend/Components/MergeDialog/MergeDialog.tsx index 9f9f5dc30..85bc71578 100644 --- a/src/Frontend/Components/MergeDialog/MergeDialog.tsx +++ b/src/Frontend/Components/MergeDialog/MergeDialog.tsx @@ -5,14 +5,12 @@ import MuiTypography from '@mui/material/Typography'; import { useState } from 'react'; +import { AllowedFrontendChannels } from '../../../shared/ipc-channels'; import { FileFormatInfo, Log } from '../../../shared/shared-types'; import { text } from '../../../shared/text'; -import { - clearLogMessage, - closePopup, -} from '../../state/actions/view-actions/view-actions'; -import { useAppDispatch, useStateEffect } from '../../state/hooks'; -import { getLogMessage } from '../../state/selectors/view-selector'; +import { closePopup } from '../../state/actions/view-actions/view-actions'; +import { useAppDispatch } from '../../state/hooks'; +import { LoggingListener, useIpcRenderer } from '../../util/use-ipc-renderer'; import { FilePathInput } from '../FilePathInput/FilePathInput'; import { LogDisplay } from '../LogDisplay/LogDisplay'; import { NotificationPopup } from '../NotificationPopup/NotificationPopup'; @@ -30,9 +28,9 @@ export const MergeDialog: React.FC = ({ fileFormat }) => { const [logToDisplay, setLogToDisplay] = useState(null); - useStateEffect( - getLogMessage, - (log) => { + useIpcRenderer( + AllowedFrontendChannels.Logging, + (_, log) => { if (isLoading) { setLogToDisplay(log); } @@ -45,7 +43,7 @@ export const MergeDialog: React.FC = ({ fileFormat }) => { if (filePath) { setInputFilePath(filePath); - dispatch(clearLogMessage()); + setLogToDisplay(null); } } From 25b80d16be38ac68834191323a7981ea51ab2eda Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Mon, 24 Feb 2025 18:55:36 +0100 Subject: [PATCH 22/27] fix: log formatting in ProcessPopup --- .../Components/ProcessPopup/ProcessPopup.style.ts | 14 -------------- .../Components/ProcessPopup/ProcessPopup.tsx | 13 ++++++++++--- 2 files changed, 10 insertions(+), 17 deletions(-) delete mode 100644 src/Frontend/Components/ProcessPopup/ProcessPopup.style.ts diff --git a/src/Frontend/Components/ProcessPopup/ProcessPopup.style.ts b/src/Frontend/Components/ProcessPopup/ProcessPopup.style.ts deleted file mode 100644 index 5076e969a..000000000 --- a/src/Frontend/Components/ProcessPopup/ProcessPopup.style.ts +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates -// SPDX-FileCopyrightText: TNG Technology Consulting GmbH -// -// SPDX-License-Identifier: Apache-2.0 -import MuiDialogContent from '@mui/material/DialogContent'; -import { styled } from '@mui/system'; - -export const DialogContent = styled(MuiDialogContent)({ - display: 'grid', - gridTemplateColumns: '24px 80px 1fr', - gridTemplateRows: 'repeat(auto-fill, 1fr)', - columnGap: '8px', - rowGap: '4px', -}); diff --git a/src/Frontend/Components/ProcessPopup/ProcessPopup.tsx b/src/Frontend/Components/ProcessPopup/ProcessPopup.tsx index 4aa06158d..0ab1827eb 100644 --- a/src/Frontend/Components/ProcessPopup/ProcessPopup.tsx +++ b/src/Frontend/Components/ProcessPopup/ProcessPopup.tsx @@ -3,6 +3,7 @@ // // SPDX-License-Identifier: Apache-2.0 import MuiDialog from '@mui/material/Dialog'; +import MuiDialogContent from '@mui/material/DialogContent'; import MuiDialogTitle from '@mui/material/DialogTitle'; import { useState } from 'react'; @@ -17,7 +18,6 @@ import { useIpcRenderer, } from '../../util/use-ipc-renderer'; import { LogDisplay } from '../LogDisplay/LogDisplay'; -import { DialogContent } from './ProcessPopup.style'; export function ProcessPopup() { const [logs, setLogs] = useState>([]); @@ -55,16 +55,23 @@ export function ProcessPopup() { function renderDialogContent() { return ( - + {logs.map((log, index) => ( ))} - + ); } } From f28baf449ee401c8c10156d430a1009eb859321f Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Mon, 24 Feb 2025 19:05:26 +0100 Subject: [PATCH 23/27] fix: linter issues --- src/ElectronBackend/main/__tests__/listeners.test.ts | 2 +- src/ElectronBackend/main/menu/fileMenu.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ElectronBackend/main/__tests__/listeners.test.ts b/src/ElectronBackend/main/__tests__/listeners.test.ts index a406b845c..2d2185e2d 100644 --- a/src/ElectronBackend/main/__tests__/listeners.test.ts +++ b/src/ElectronBackend/main/__tests__/listeners.test.ts @@ -33,7 +33,7 @@ import { linkHasHttpSchema, openLinkListener, selectBaseURLListener, - selectFileListener + selectFileListener, } from '../listeners'; import { importFileFormats } from '../menu/fileMenu'; diff --git a/src/ElectronBackend/main/menu/fileMenu.ts b/src/ElectronBackend/main/menu/fileMenu.ts index 3a8c8f38d..d984e81ab 100644 --- a/src/ElectronBackend/main/menu/fileMenu.ts +++ b/src/ElectronBackend/main/menu/fileMenu.ts @@ -16,8 +16,8 @@ import { isFileLoaded } from '../../utils/getLoadedFile'; import { getGlobalBackendState } from '../globalBackendState'; import { getIconBasedOnTheme } from '../iconHelpers'; import { - importFileListener, getMergeListener, + importFileListener, selectBaseURLListener, setLoadingState, } from '../listeners'; From 733970ff2d853fc065de77a759555e215f7c1f64 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Mon, 24 Feb 2025 19:15:08 +0100 Subject: [PATCH 24/27] fix: update loading state handling in MergeDialog --- .../Components/MergeDialog/MergeDialog.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Frontend/Components/MergeDialog/MergeDialog.tsx b/src/Frontend/Components/MergeDialog/MergeDialog.tsx index 85bc71578..8ab716eca 100644 --- a/src/Frontend/Components/MergeDialog/MergeDialog.tsx +++ b/src/Frontend/Components/MergeDialog/MergeDialog.tsx @@ -10,7 +10,11 @@ import { FileFormatInfo, Log } from '../../../shared/shared-types'; import { text } from '../../../shared/text'; import { closePopup } from '../../state/actions/view-actions/view-actions'; import { useAppDispatch } from '../../state/hooks'; -import { LoggingListener, useIpcRenderer } from '../../util/use-ipc-renderer'; +import { + IsLoadingListener, + LoggingListener, + useIpcRenderer, +} from '../../util/use-ipc-renderer'; import { FilePathInput } from '../FilePathInput/FilePathInput'; import { LogDisplay } from '../LogDisplay/LogDisplay'; import { NotificationPopup } from '../NotificationPopup/NotificationPopup'; @@ -24,7 +28,13 @@ export const MergeDialog: React.FC = ({ fileFormat }) => { const [inputFilePath, setInputFilePath] = useState(''); - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + useIpcRenderer( + AllowedFrontendChannels.FileLoading, + (_, { isLoading }) => setIsLoading(isLoading), + [], + ); const [logToDisplay, setLogToDisplay] = useState(null); @@ -52,8 +62,6 @@ export const MergeDialog: React.FC = ({ fileFormat }) => { } async function onConfirm(): Promise { - setIsLoading(true); - const success = await window.electronAPI.mergeFileAndLoad( inputFilePath, fileFormat.fileType, @@ -62,12 +70,6 @@ export const MergeDialog: React.FC = ({ fileFormat }) => { if (success) { dispatch(closePopup()); } - - // NOTE: without a tiny delay, isLoading is sometimes set to false before the - // final log message is processed by LogDisplayForDialog, causing it to be - // missed. This seems to only happen when running the app with yarn start - await new Promise((resolve) => setTimeout(resolve, 1)); - setIsLoading(false); } return ( From 3ebbd99025a10e2de7786399907e74576e86e62e Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Tue, 25 Feb 2025 10:45:45 +0100 Subject: [PATCH 25/27] refactor: rename merge listener --- src/ElectronBackend/main/listeners.ts | 2 +- src/ElectronBackend/main/main.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ElectronBackend/main/listeners.ts b/src/ElectronBackend/main/listeners.ts index e43603fff..7ba50ad22 100644 --- a/src/ElectronBackend/main/listeners.ts +++ b/src/ElectronBackend/main/listeners.ts @@ -239,7 +239,7 @@ export const importFileConvertAndLoadListener = } }; -export const getMergeFileAndLoadListener = +export const mergeFileAndLoadListener = (mainWindow: BrowserWindow) => async ( _: Electron.IpcMainInvokeEvent, diff --git a/src/ElectronBackend/main/main.ts b/src/ElectronBackend/main/main.ts index 9a64e2888..11ba82a90 100644 --- a/src/ElectronBackend/main/main.ts +++ b/src/ElectronBackend/main/main.ts @@ -10,9 +10,9 @@ import { getMessageBoxContentForErrorsWrapper } from '../errorHandling/errorHand import { createWindow } from './createWindow'; import { exportFileListener, - getMergeFileAndLoadListener, importFileConvertAndLoadListener, importFileSelectSaveLocationListener, + mergeFileAndLoadListener, openFileListener, openLinkListener, saveFileListener, @@ -82,7 +82,7 @@ export async function main(): Promise { ); ipcMain.handle( IpcChannel.MergeFileAndLoad, - getMergeFileAndLoadListener(mainWindow), + mergeFileAndLoadListener(mainWindow), ); ipcMain.handle(IpcChannel.SaveFile, saveFileListener(mainWindow)); ipcMain.handle(IpcChannel.ExportFile, exportFileListener(mainWindow)); From 6f312b3426c9c14cf3697a4d607f955ac0289711 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Tue, 25 Feb 2025 15:38:56 +0100 Subject: [PATCH 26/27] refactor: move location of style definitions in ProcessPopup --- .../Components/ProcessPopup/ProcessPopup.style.ts | 12 ++++++++++++ .../Components/ProcessPopup/ProcessPopup.tsx | 8 +++----- 2 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 src/Frontend/Components/ProcessPopup/ProcessPopup.style.ts diff --git a/src/Frontend/Components/ProcessPopup/ProcessPopup.style.ts b/src/Frontend/Components/ProcessPopup/ProcessPopup.style.ts new file mode 100644 index 000000000..574edc6c6 --- /dev/null +++ b/src/Frontend/Components/ProcessPopup/ProcessPopup.style.ts @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 +import MuiDialogContent from '@mui/material/DialogContent'; +import { styled } from '@mui/system'; + +export const DialogContent = styled(MuiDialogContent)({ + display: 'grid', + gridTemplateRows: 'repeat(auto-fill, 1fr)', + rowGap: '4px', +}); diff --git a/src/Frontend/Components/ProcessPopup/ProcessPopup.tsx b/src/Frontend/Components/ProcessPopup/ProcessPopup.tsx index 0ab1827eb..08edf8be5 100644 --- a/src/Frontend/Components/ProcessPopup/ProcessPopup.tsx +++ b/src/Frontend/Components/ProcessPopup/ProcessPopup.tsx @@ -3,7 +3,6 @@ // // SPDX-License-Identifier: Apache-2.0 import MuiDialog from '@mui/material/Dialog'; -import MuiDialogContent from '@mui/material/DialogContent'; import MuiDialogTitle from '@mui/material/DialogTitle'; import { useState } from 'react'; @@ -18,6 +17,7 @@ import { useIpcRenderer, } from '../../util/use-ipc-renderer'; import { LogDisplay } from '../LogDisplay/LogDisplay'; +import { DialogContent } from './ProcessPopup.style'; export function ProcessPopup() { const [logs, setLogs] = useState>([]); @@ -55,7 +55,7 @@ export function ProcessPopup() { function renderDialogContent() { return ( - + {logs.map((log, index) => ( ))} - + ); } } From d5647b8804f77a9e5bbf588e091e9e66a99c62e5 Mon Sep 17 00:00:00 2001 From: Philipp Martens Date: Tue, 25 Feb 2025 16:59:14 +0100 Subject: [PATCH 27/27] refactor: move log related styling from sx into styled components --- .../DialogLogDisplay/DialogLogDisplay.style.ts | 16 ++++++++++++++++ .../Components/ImportDialog/ImportDialog.tsx | 8 ++------ .../Components/LogDisplay/LogDisplay.tsx | 5 +++-- .../Components/MergeDialog/MergeDialog.tsx | 8 ++------ .../ProcessPopup/ProcessPopup.style.ts | 8 ++++++++ .../Components/ProcessPopup/ProcessPopup.tsx | 10 ++-------- 6 files changed, 33 insertions(+), 22 deletions(-) create mode 100644 src/Frontend/Components/DialogLogDisplay/DialogLogDisplay.style.ts diff --git a/src/Frontend/Components/DialogLogDisplay/DialogLogDisplay.style.ts b/src/Frontend/Components/DialogLogDisplay/DialogLogDisplay.style.ts new file mode 100644 index 000000000..582aff2de --- /dev/null +++ b/src/Frontend/Components/DialogLogDisplay/DialogLogDisplay.style.ts @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Meta Platforms, Inc. and its affiliates +// SPDX-FileCopyrightText: TNG Technology Consulting GmbH +// +// SPDX-License-Identifier: Apache-2.0 +import { styled } from '@mui/system'; + +import { LogDisplay } from '../LogDisplay/LogDisplay'; + +export const DialogLogDisplay = styled(LogDisplay)({ + display: 'flex', + alignItems: 'center', + columnGap: '4px', + flexGrow: 1, + flexBasis: 0, + minWidth: 0, +}); diff --git a/src/Frontend/Components/ImportDialog/ImportDialog.tsx b/src/Frontend/Components/ImportDialog/ImportDialog.tsx index d3413db05..7b07e57d0 100644 --- a/src/Frontend/Components/ImportDialog/ImportDialog.tsx +++ b/src/Frontend/Components/ImportDialog/ImportDialog.tsx @@ -16,8 +16,8 @@ import { LoggingListener, useIpcRenderer, } from '../../util/use-ipc-renderer'; +import { DialogLogDisplay } from '../DialogLogDisplay/DialogLogDisplay.style'; import { FilePathInput } from '../FilePathInput/FilePathInput'; -import { LogDisplay } from '../LogDisplay/LogDisplay'; import { NotificationPopup } from '../NotificationPopup/NotificationPopup'; export interface ImportDialogProps { @@ -130,17 +130,13 @@ export const ImportDialog: React.FC = ({ fileFormat }) => { isOpen={true} customAction={ logToDisplay ? ( - ) : undefined diff --git a/src/Frontend/Components/LogDisplay/LogDisplay.tsx b/src/Frontend/Components/LogDisplay/LogDisplay.tsx index e58e919ce..24d62ce3d 100644 --- a/src/Frontend/Components/LogDisplay/LogDisplay.tsx +++ b/src/Frontend/Components/LogDisplay/LogDisplay.tsx @@ -32,10 +32,11 @@ interface LogDisplayProps { showDate: boolean; useEllipsis?: boolean; sx?: SxProps; + className?: string; } export function LogDisplay(props: LogDisplayProps) { - const { log, isInProgress, showDate, useEllipsis, sx } = props; + const { log, isInProgress, showDate, useEllipsis, sx, className } = props; const icon = useMemo(() => { const { color, Component } = icons[log.level]; @@ -47,7 +48,7 @@ export function LogDisplay(props: LogDisplayProps) { }, [log, isInProgress]); return ( - + {icon} {showDate ? ( diff --git a/src/Frontend/Components/MergeDialog/MergeDialog.tsx b/src/Frontend/Components/MergeDialog/MergeDialog.tsx index 8ab716eca..e3721b22f 100644 --- a/src/Frontend/Components/MergeDialog/MergeDialog.tsx +++ b/src/Frontend/Components/MergeDialog/MergeDialog.tsx @@ -15,8 +15,8 @@ import { LoggingListener, useIpcRenderer, } from '../../util/use-ipc-renderer'; +import { DialogLogDisplay } from '../DialogLogDisplay/DialogLogDisplay.style'; import { FilePathInput } from '../FilePathInput/FilePathInput'; -import { LogDisplay } from '../LogDisplay/LogDisplay'; import { NotificationPopup } from '../NotificationPopup/NotificationPopup'; export interface MergeDialogProps { @@ -95,17 +95,13 @@ export const MergeDialog: React.FC = ({ fileFormat }) => { isOpen={true} customAction={ logToDisplay ? ( - ) : undefined diff --git a/src/Frontend/Components/ProcessPopup/ProcessPopup.style.ts b/src/Frontend/Components/ProcessPopup/ProcessPopup.style.ts index 574edc6c6..89736d5c0 100644 --- a/src/Frontend/Components/ProcessPopup/ProcessPopup.style.ts +++ b/src/Frontend/Components/ProcessPopup/ProcessPopup.style.ts @@ -5,8 +5,16 @@ import MuiDialogContent from '@mui/material/DialogContent'; import { styled } from '@mui/system'; +import { LogDisplay } from '../LogDisplay/LogDisplay'; + export const DialogContent = styled(MuiDialogContent)({ display: 'grid', gridTemplateRows: 'repeat(auto-fill, 1fr)', rowGap: '4px', }); + +export const GridLogDisplay = styled(LogDisplay)({ + display: 'grid', + gridTemplateColumns: '24px 80px 1fr', + columnGap: '8px', +}); diff --git a/src/Frontend/Components/ProcessPopup/ProcessPopup.tsx b/src/Frontend/Components/ProcessPopup/ProcessPopup.tsx index 08edf8be5..5ed61db54 100644 --- a/src/Frontend/Components/ProcessPopup/ProcessPopup.tsx +++ b/src/Frontend/Components/ProcessPopup/ProcessPopup.tsx @@ -16,8 +16,7 @@ import { LoggingListener, useIpcRenderer, } from '../../util/use-ipc-renderer'; -import { LogDisplay } from '../LogDisplay/LogDisplay'; -import { DialogContent } from './ProcessPopup.style'; +import { DialogContent, GridLogDisplay } from './ProcessPopup.style'; export function ProcessPopup() { const [logs, setLogs] = useState>([]); @@ -57,16 +56,11 @@ export function ProcessPopup() { return ( {logs.map((log, index) => ( - ))}