Show native save dialogs in desktop app #50, #264

This commit introduces native operating system file dialogs in the
desktop application replacing the existing web-based dialogs.

It lays the foundation for future enhancements such as:

- Providing error messages when saving or executing files, addressing
  #264.
- Creating system restore points, addressing #50.

Documentation updates:

- Update `desktop-vs-web-features.md` with added functionality.
- Update `README.md` with security feature highlights.
- Update home page documentation to emphasize security features.

Other supporting changes include:

- Integrate IPC communication channels for secure Electron dialog API
  interactions.
- Refactor `IpcRegistration` for more type-safety and simplicity.
- Introduce a Vue hook to encapsulate dialog functionality.
- Improve errors during IPC registration for easier troubleshooting.
- Move `ClientLoggerFactory` for consistency in hooks organization and
  remove `LoggerFactory` interface for simplicity.
- Add tests for the save file dialog in the browser context.
- Add `Blob` polyfill in tests to compensate for the missing
  `blob.text()` function in `jsdom` (see jsdom/jsdom#2555).

Improve environment detection logic:

- Treat test environment as browser environments to correctly activate
  features based on the environment. This resolves issues where the
  environment is misidentified as desktop, but Electron preloader APIs
  are missing.
- Rename `isDesktop` environment identification variable to
  `isRunningAsDesktopApplication` for better clarity and to avoid
  confusion with desktop environments in web/browser/test environments.
- Simplify `BrowserRuntimeEnvironment` to consistently detect
  non-desktop application environments.
- Improve environment detection for Electron main process
  (electron/electron#2288).
This commit is contained in:
undergroundwires
2024-01-13 18:04:23 +01:00
parent da4be500da
commit c546a33eff
65 changed files with 1384 additions and 345 deletions

View File

@@ -1,53 +0,0 @@
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
import { ConsoleLogger } from '@/infrastructure/Log/ConsoleLogger';
import { Logger } from '@/application/Common/Log/Logger';
import { LoggerFactory } from '@/application/Common/Log/LoggerFactory';
import { NoopLogger } from '@/infrastructure/Log/NoopLogger';
import { WindowInjectedLogger } from '@/infrastructure/Log/WindowInjectedLogger';
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
export class ClientLoggerFactory implements LoggerFactory {
public static readonly Current: LoggerFactory = new ClientLoggerFactory();
public readonly logger: Logger;
protected constructor(
environment: RuntimeEnvironment = CurrentEnvironment,
windowAccessor: WindowAccessor = () => globalThis.window,
noopLoggerFactory: LoggerCreationFunction = () => new NoopLogger(),
windowInjectedLoggerFactory: LoggerCreationFunction = () => new WindowInjectedLogger(),
consoleLoggerFactory: LoggerCreationFunction = () => new ConsoleLogger(),
) {
if (isUnitOrIntegrationTests(environment, windowAccessor)) {
this.logger = noopLoggerFactory(); // keep the test outputs clean
return;
}
if (environment.isDesktop) {
this.logger = windowInjectedLoggerFactory();
return;
}
if (environment.isNonProduction) {
this.logger = consoleLoggerFactory();
return;
}
this.logger = noopLoggerFactory();
}
}
export type WindowAccessor = () => OptionalWindow;
export type LoggerCreationFunction = () => Logger;
type OptionalWindow = Window | undefined | null;
function isUnitOrIntegrationTests(
environment: RuntimeEnvironment,
windowAccessor: WindowAccessor,
): boolean {
/*
In a desktop application context, Electron preloader process inject a logger into
the global window object. If we're in a desktop (Node) environment and the logger isn't
injected, it indicates a testing environment.
*/
return environment.isDesktop && !windowAccessor()?.log;
}

View File

@@ -11,9 +11,10 @@ import {
} from '@/presentation/injectionSymbols';
import { PropertyKeys } from '@/TypeHelpers';
import { useUserSelectionState } from '@/presentation/components/Shared/Hooks/UseUserSelectionState';
import { useLogger } from '@/presentation/components/Shared/Hooks/UseLogger';
import { useLogger } from '@/presentation/components/Shared/Hooks/Log/UseLogger';
import { useCodeRunner } from '@/presentation/components/Shared/Hooks/UseCodeRunner';
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
import { useDialog } from '@/presentation/components/Shared/Hooks/Dialog/UseDialog';
export function provideDependencies(
context: IApplicationContext,
@@ -67,6 +68,10 @@ export function provideDependencies(
InjectionKeys.useCodeRunner,
useCodeRunner,
),
useDialog: (di) => di.provide(
InjectionKeys.useDialog,
useDialog,
),
};
registerAll(Object.values(resolvers), api);
}

View File

@@ -1,6 +1,6 @@
import { Logger } from '@/application/Common/Log/Logger';
import { ClientLoggerFactory } from '@/presentation/components/Shared/Hooks/Log/ClientLoggerFactory';
import { Bootstrapper } from '../Bootstrapper';
import { ClientLoggerFactory } from '../ClientLoggerFactory';
export class AppInitializationLogger implements Bootstrapper {
constructor(