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:
@@ -0,0 +1,25 @@
|
||||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||
import { Dialog } from '@/presentation/common/Dialog';
|
||||
import { BrowserDialog } from '@/infrastructure/Dialog/Browser/BrowserDialog';
|
||||
|
||||
export function determineDialogBasedOnEnvironment(
|
||||
environment: RuntimeEnvironment,
|
||||
windowInjectedDialogFactory: WindowDialogCreationFunction = () => globalThis.window.dialog,
|
||||
browserDialogFactory: BrowserDialogCreationFunction = () => new BrowserDialog(),
|
||||
): Dialog {
|
||||
if (!environment.isRunningAsDesktopApplication) {
|
||||
return browserDialogFactory();
|
||||
}
|
||||
const dialog = windowInjectedDialogFactory();
|
||||
if (!dialog) {
|
||||
throw new Error([
|
||||
'The Dialog API could not be retrieved from the window object.',
|
||||
'This may indicate that the Dialog API is either not implemented or not correctly exposed in the current desktop environment.',
|
||||
].join('\n'));
|
||||
}
|
||||
return dialog;
|
||||
}
|
||||
|
||||
export type WindowDialogCreationFunction = () => Dialog | undefined;
|
||||
|
||||
export type BrowserDialogCreationFunction = () => Dialog;
|
||||
14
src/presentation/components/Shared/Hooks/Dialog/UseDialog.ts
Normal file
14
src/presentation/components/Shared/Hooks/Dialog/UseDialog.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Dialog } from '@/presentation/common/Dialog';
|
||||
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||
import { determineDialogBasedOnEnvironment } from './ClientDialogFactory';
|
||||
|
||||
export function useDialog(
|
||||
factory: DialogFactory = () => determineDialogBasedOnEnvironment(CurrentEnvironment),
|
||||
) {
|
||||
const dialog = factory();
|
||||
return {
|
||||
dialog,
|
||||
};
|
||||
}
|
||||
|
||||
export type DialogFactory = () => Dialog;
|
||||
@@ -0,0 +1,53 @@
|
||||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||
import { ConsoleLogger } from '@/infrastructure/Log/ConsoleLogger';
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
import { NoopLogger } from '@/infrastructure/Log/NoopLogger';
|
||||
import { WindowInjectedLogger } from '@/infrastructure/Log/WindowInjectedLogger';
|
||||
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||
import { LoggerFactory } from './LoggerFactory';
|
||||
|
||||
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.isRunningAsDesktopApplication) {
|
||||
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.isRunningAsDesktopApplication && !windowAccessor()?.log;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
|
||||
export interface LoggerFactory {
|
||||
readonly logger: Logger;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ClientLoggerFactory } from './ClientLoggerFactory';
|
||||
import { LoggerFactory } from './LoggerFactory';
|
||||
|
||||
export function useLogger(factory: LoggerFactory = ClientLoggerFactory.Current) {
|
||||
return {
|
||||
log: factory.logger,
|
||||
};
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { LoggerFactory } from '@/application/Common/Log/LoggerFactory';
|
||||
import { ClientLoggerFactory } from '@/presentation/bootstrapping/ClientLoggerFactory';
|
||||
|
||||
export function useLogger(factory: LoggerFactory = ClientLoggerFactory.Current) {
|
||||
return {
|
||||
log: factory.logger,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user