Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check for unsaved changes before import/open/export #2782

Merged
merged 26 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2a35fe0
feat: show NotSavedPopup before file import if necessary
PhilippMa Feb 12, 2025
5efb370
feat: also show NotSavedPopup when starting open file action from app…
PhilippMa Feb 12, 2025
0711d9e
feat: show NotSavedPopup before export
PhilippMa Feb 13, 2025
2e2647d
refactor: move test to match previous refactoring
PhilippMa Feb 13, 2025
db7c031
fix: properly reset state in proceedFromUnsavedPopup
PhilippMa Feb 13, 2025
65139ea
test: update popup-actions test for modified existing popup-actions
PhilippMa Feb 13, 2025
d9ce26b
refactor: extract separate function for conditionally opening NotSave…
PhilippMa Feb 13, 2025
75f5970
fix: fix failing e2e test and small styling problem
PhilippMa Feb 13, 2025
ede6199
fix: fix failing e2e test
PhilippMa Feb 13, 2025
1538833
refactor: change back accidentally changed function name
PhilippMa Feb 13, 2025
f1f4c66
test: update test for popup-actions to reflect recent changes
PhilippMa Feb 13, 2025
f9c1b25
refactor: factor out more common logic regarding unsaved checks
PhilippMa Feb 13, 2025
204af1f
test: e2e test that import dialog works with NotSavedPopup
PhilippMa Feb 13, 2025
bf908db
refactor: make usage of withUnsavedCheck more consistent
PhilippMa Feb 14, 2025
21702bb
refactor: clean up some unit tests
PhilippMa Feb 14, 2025
3a3450c
test: make unit test for NotSavedPopup on import request more precise
PhilippMa Feb 14, 2025
d13a6da
refactor: turn withUnsavedCheck parameters into an object to have nam…
PhilippMa Feb 14, 2025
cd9e09b
refactor: redirect isLoading and log message events through redux store
PhilippMa Feb 14, 2025
6004cce
refactor: introduce a new hook to simplify using redux state for even…
PhilippMa Feb 14, 2025
ee22348
fix: cache effect callback in useStateEffect to avoid unnecessary rer…
PhilippMa Feb 14, 2025
a3faa4d
fix: fix broken unit test for ProcessPopup
PhilippMa Feb 14, 2025
68871d3
feat: reintroduce log message at the start of export action
PhilippMa Feb 14, 2025
3422c70
refactor: rename functions in export-actions
PhilippMa Feb 14, 2025
8d95f1d
test: add missing test and move all NotSavedPopup tests to updating-a…
PhilippMa Feb 14, 2025
a312615
feat: add minimum width and height for electron window
PhilippMa Feb 17, 2025
bb47c74
refactor: change ProcessPopup test to use act instead of rerender
PhilippMa Feb 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/ElectronBackend/main/createWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export async function createWindow(): Promise<BrowserWindow> {
const mainWindow = new BrowserWindow({
width: 1920,
height: 1080,
minWidth: 500,
minHeight: 300,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
Expand Down
2 changes: 1 addition & 1 deletion src/ElectronBackend/main/listeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ export function getImportFileConvertAndLoadListener(
opossumFilePath: string,
) => {
if (!resourceFilePath.trim() || !fs.existsSync(resourceFilePath)) {
throw new Error('Input file does not exists');
throw new Error('Input file does not exist');
}

if (!opossumFilePath.trim()) {
Expand Down
5 changes: 4 additions & 1 deletion src/ElectronBackend/main/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ class Logger {
message: string,
{ level }: Pick<Log, 'level'>,
): void {
BrowserWindow.getFocusedWindow()?.webContents.send(
// NOTE: there are situations where BrowserWindow.getAllWindows() returns a
// non-empty array but BrowserWindow.getFocusedWindow() returns null.
// Thus, using getAllWindows here is more robust than getFocusedWindow
BrowserWindow.getAllWindows()[0]?.webContents.send(
AllowedFrontendChannels.Logging,
{
date: new Date(),
Expand Down
25 changes: 5 additions & 20 deletions src/ElectronBackend/main/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,7 @@ import {
getIconBasedOnTheme,
makeFirstIconVisibleAndSecondHidden,
} from './iconHelpers';
import {
getImportFileListener,
getOpenFileListener,
getSelectBaseURLListener,
setLoadingState,
} from './listeners';
import logger from './logger';
import { getImportFileListener, getSelectBaseURLListener } from './listeners';
import {
getPathOfChromiumNoticeDocument,
getPathOfNoticeDocument,
Expand Down Expand Up @@ -113,7 +107,10 @@ export async function createMenu(mainWindow: BrowserWindow): Promise<Menu> {
),
label: 'Open File',
accelerator: 'CmdOrCtrl+O',
click: getOpenFileListener(mainWindow, activateMenuItems),
click: () =>
mainWindow.webContents.send(
AllowedFrontendChannels.OpenFileWithUnsavedCheck,
),
},
{
icon: getIconBasedOnTheme(
Expand Down Expand Up @@ -155,8 +152,6 @@ export async function createMenu(mainWindow: BrowserWindow): Promise<Menu> {
'icons/follow-up-black.png',
),
click: () => {
setLoadingState(mainWindow.webContents, true);
logger.info('Preparing data for follow-up export');
webContents.send(
AllowedFrontendChannels.ExportFileRequest,
ExportType.FollowUp,
Expand All @@ -172,8 +167,6 @@ export async function createMenu(mainWindow: BrowserWindow): Promise<Menu> {
),
label: INITIALLY_DISABLED_ITEMS_INFO.compactComponentList.label,
click: () => {
setLoadingState(mainWindow.webContents, true);
logger.info('Preparing data for compact component list export');
webContents.send(
AllowedFrontendChannels.ExportFileRequest,
ExportType.CompactBom,
Expand All @@ -189,10 +182,6 @@ export async function createMenu(mainWindow: BrowserWindow): Promise<Menu> {
),
label: INITIALLY_DISABLED_ITEMS_INFO.detailedComponentList.label,
click: () => {
setLoadingState(mainWindow.webContents, true);
logger.info(
'Preparing data for detailed component list export',
);
webContents.send(
AllowedFrontendChannels.ExportFileRequest,
ExportType.DetailedBom,
Expand All @@ -208,8 +197,6 @@ export async function createMenu(mainWindow: BrowserWindow): Promise<Menu> {
),
label: INITIALLY_DISABLED_ITEMS_INFO.spdxYAML.label,
click: () => {
setLoadingState(mainWindow.webContents, true);
logger.info('Preparing data for SPDX (yaml) export');
webContents.send(
AllowedFrontendChannels.ExportFileRequest,
ExportType.SpdxDocumentYaml,
Expand All @@ -225,8 +212,6 @@ export async function createMenu(mainWindow: BrowserWindow): Promise<Menu> {
),
label: INITIALLY_DISABLED_ITEMS_INFO.spdxJSON.label,
click: () => {
setLoadingState(mainWindow.webContents, true);
logger.info('Preparing data for SPDX (json) export');
webContents.send(
AllowedFrontendChannels.ExportFileRequest,
ExportType.SpdxDocumentJson,
Expand Down
2 changes: 0 additions & 2 deletions src/Frontend/Components/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { useSignalsWorker } from '../../web-workers/use-signals-worker';
import { AuditView } from '../AuditView/AuditView';
import { ErrorFallback } from '../ErrorFallback/ErrorFallback';
import { GlobalPopup } from '../GlobalPopup/GlobalPopup';
import { ImportDialogProvider } from '../ImportDialog/ImportDialogProvider';
import { ProcessPopup } from '../ProcessPopup/ProcessPopup';
import { ReportView } from '../ReportView/ReportView';
import { TopBar } from '../TopBar/TopBar';
Expand All @@ -40,7 +39,6 @@ export function App() {
<ErrorBoundary FallbackComponent={ErrorFallback}>
<GlobalPopup />
<ProcessPopup />
<ImportDialogProvider />
<TopBar />
{renderView()}
</ErrorBoundary>
Expand Down
206 changes: 42 additions & 164 deletions src/Frontend/Components/BackendCommunication/BackendCommunication.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,40 @@
// SPDX-License-Identifier: Apache-2.0
import dayjs from 'dayjs';
import { IpcRendererEvent } from 'electron';
import pick from 'lodash/pick';

import { AllowedFrontendChannels } from '../../../shared/ipc-channels';
import {
Attributions,
BaseURLForRootArgs,
ExportSpdxDocumentJsonArgs,
ExportSpdxDocumentYamlArgs,
ExportType,
ParsedFileContent,
} from '../../../shared/shared-types';
import { PopupType } from '../../enums/enums';
import { ROOT_PATH } from '../../shared-constants';
import {
exportFileOrOpenUnsavedPopup,
openFileOrOpenUnsavedPopup,
showImportDialogOrOpenUnsavedPopup,
} from '../../state/actions/popup-actions/popup-actions';
import {
resetResourceState,
setBaseUrlsForSources,
} from '../../state/actions/resource-actions/all-views-simple-actions';
import { loadFromFile } from '../../state/actions/resource-actions/load-actions';
import { openPopup } from '../../state/actions/view-actions/view-actions';
import { useAppDispatch, useAppSelector } from '../../state/hooks';
import {
getAttributionBreakpoints,
getBaseUrlsForSources,
getFilesWithChildren,
getFrequentLicensesTexts,
getManualData,
getResources,
} from '../../state/selectors/resource-selectors';
openPopup,
setLoading,
writeLogMessage,
} from '../../state/actions/view-actions/view-actions';
import { useAppDispatch, useAppSelector } from '../../state/hooks';
import { getBaseUrlsForSources } from '../../state/selectors/resource-selectors';
import {
getAttributionsWithAllChildResourcesWithoutFolders,
getAttributionsWithResources,
removeSlashesFromFilesWithChildren,
} from '../../util/get-attributions-with-resources';
import { LoggingListener, useIpcRenderer } from '../../util/use-ipc-renderer';
ExportFileRequestListener,
IsLoadingListener,
LoggingListener,
ShowImportDialogListener,
useIpcRenderer,
} from '../../util/use-ipc-renderer';

export const BackendCommunication: React.FC = () => {
const resources = useAppSelector(getResources);
const manualData = useAppSelector(getManualData);
const attributionBreakpoints = useAppSelector(getAttributionBreakpoints);
const filesWithChildren = useAppSelector(getFilesWithChildren);
const frequentLicenseTexts = useAppSelector(getFrequentLicensesTexts);
const baseUrlsForSources = useAppSelector(getBaseUrlsForSources);
const dispatch = useAppDispatch();

Expand All @@ -58,118 +51,6 @@ export const BackendCommunication: React.FC = () => {
dispatch(openPopup(PopupType.ProjectStatisticsPopup));
}

function getExportFileRequestListener(
_: IpcRendererEvent,
exportType: ExportType,
): void {
switch (exportType) {
case ExportType.SpdxDocumentJson:
case ExportType.SpdxDocumentYaml:
return getSpdxDocumentExportListener(exportType);
case ExportType.FollowUp:
return getFollowUpExportListener();
case ExportType.CompactBom:
return getCompactBomExportListener();
case ExportType.DetailedBom:
return getDetailedBomExportListener();
}
}

function getFollowUpExportListener(): void {
const followUpAttributions = pick(
manualData.attributions,
Object.keys(manualData.attributions).filter(
(attributionId) => manualData.attributions[attributionId].followUp,
),
);

const followUpAttributionsWithResources =
getAttributionsWithAllChildResourcesWithoutFolders(
followUpAttributions,
manualData.attributionsToResources,
manualData.resourcesToAttributions,
resources || {},
attributionBreakpoints,
filesWithChildren,
);
const followUpAttributionsWithFormattedResources =
removeSlashesFromFilesWithChildren(
followUpAttributionsWithResources,
filesWithChildren,
);

window.electronAPI.exportFile({
type: ExportType.FollowUp,
followUpAttributionsWithResources:
followUpAttributionsWithFormattedResources,
});
}

function getSpdxDocumentExportListener(
exportType: ExportType.SpdxDocumentYaml | ExportType.SpdxDocumentJson,
): void {
const attributions = Object.fromEntries(
Object.entries(manualData.attributions).map((entry) => {
const packageInfo = entry[1];

const licenseName = packageInfo.licenseName || '';
const isFrequentLicense =
licenseName && licenseName in frequentLicenseTexts;
const licenseText =
packageInfo.licenseText || isFrequentLicense
? frequentLicenseTexts[licenseName]
: '';
return [
entry[0],
{
...entry[1],
licenseText,
},
];
}),
);

const args: ExportSpdxDocumentYamlArgs | ExportSpdxDocumentJsonArgs = {
type: exportType,
spdxAttributions: attributions,
};

window.electronAPI.exportFile(args);
}

function getDetailedBomExportListener(): void {
const bomAttributions = getBomAttributions(
manualData.attributions,
ExportType.DetailedBom,
);

const bomAttributionsWithResources = getAttributionsWithResources(
bomAttributions,
manualData.attributionsToResources,
);

const bomAttributionsWithFormattedResources =
removeSlashesFromFilesWithChildren(
bomAttributionsWithResources,
filesWithChildren,
);

window.electronAPI.exportFile({
type: ExportType.DetailedBom,
bomAttributionsWithResources: bomAttributionsWithFormattedResources,
});
}

function getCompactBomExportListener(): void {
window.electronAPI.exportFile({
type: ExportType.CompactBom,
bomAttributions: getBomAttributions(
manualData.attributions,
ExportType.CompactBom,
),
});
}

function resetLoadedFileListener(
_: IpcRendererEvent,
resetState: boolean,
Expand Down Expand Up @@ -220,6 +101,13 @@ export const BackendCommunication: React.FC = () => {
}
}

useIpcRenderer<IsLoadingListener>(
AllowedFrontendChannels.FileLoading,
(_, { isLoading }) => {
dispatch(setLoading(isLoading));
},
[dispatch],
);
useIpcRenderer(AllowedFrontendChannels.FileLoaded, fileLoadedListener, [
dispatch,
]);
Expand All @@ -230,8 +118,11 @@ export const BackendCommunication: React.FC = () => {
);
useIpcRenderer<LoggingListener>(
AllowedFrontendChannels.Logging,
(_, { date, level, message }) =>
console[level](`${dayjs(date).format('HH:mm:ss.SSS')} ${message}`),
(_, log) => {
const { date, level, message } = log;
console[level](`${dayjs(date).format('HH:mm:ss.SSS')} ${message}`);
dispatch(writeLogMessage(log));
},
[dispatch],
);
useIpcRenderer(
Expand All @@ -249,39 +140,26 @@ export const BackendCommunication: React.FC = () => {
setBaseURLForRootListener,
[dispatch, baseUrlsForSources],
);
useIpcRenderer(
useIpcRenderer<ExportFileRequestListener>(
AllowedFrontendChannels.ExportFileRequest,
getExportFileRequestListener,
[
manualData,
attributionBreakpoints,
frequentLicenseTexts,
filesWithChildren,
],
(_, exportType) => dispatch(exportFileOrOpenUnsavedPopup(exportType)),
[dispatch],
);
useIpcRenderer(
AllowedFrontendChannels.ShowUpdateAppPopup,
showUpdateAppPopupListener,
[dispatch],
);
useIpcRenderer(
AllowedFrontendChannels.OpenFileWithUnsavedCheck,
() => dispatch(openFileOrOpenUnsavedPopup()),
[dispatch],
);
useIpcRenderer<ShowImportDialogListener>(
AllowedFrontendChannels.ImportFileShowDialog,
(_, fileFormat) => dispatch(showImportDialogOrOpenUnsavedPopup(fileFormat)),
[dispatch],
);

return null;
};

export function getBomAttributions(
attributions: Attributions,
exportType: ExportType,
): Attributions {
return pick(
attributions,
Object.keys(attributions).filter(
(attributionId) =>
!attributions[attributionId].followUp &&
!attributions[attributionId].firstParty &&
!(
exportType === ExportType.CompactBom &&
attributions[attributionId].excludeFromNotice
),
),
);
}
Loading
Loading