Show save/execution error dialogs on desktop #264
This commit introduces system-native error dialogs on desktop application for code save or execution failures, addressing user confusion described in issue #264. This commit adds informative feedback when script execution or saving fails. Changes: - Implement support for system-native error dialogs. - Refactor `CodeRunner` and `Dialog` interfaces and their implementations to improve error handling and provide better type safety. - Introduce structured error handling, allowing UI to display detailed error messages. - Replace error throwing with an error object interface for controlled handling. This ensures that errors are propagated to the renderer process without being limited by Electron's error object serialization limitations as detailed in electron/electron#24427. - Add logging for dialog actions to aid in troubleshooting. - Rename `fileName` to `defaultFilename` in `saveFile` functions to clarify its purpose. - Centralize message assertion in `LoggerStub` for consistency. - Introduce `expectTrue` in tests for clearer boolean assertions. - Standardize `filename` usage across the codebase. - Enhance existing test names and organization for clarity. - Update related documentation.
This commit is contained in:
@@ -2,5 +2,35 @@ export interface CodeRunner {
|
||||
runCode(
|
||||
code: string,
|
||||
fileExtension: string,
|
||||
): Promise<void>;
|
||||
): Promise<CodeRunOutcome>;
|
||||
}
|
||||
|
||||
export type CodeRunErrorType =
|
||||
| 'FileWriteError'
|
||||
| 'FilePathGenerationError'
|
||||
| 'UnsupportedOperatingSystem'
|
||||
| 'FileExecutionError'
|
||||
| 'DirectoryCreationError'
|
||||
| 'UnexpectedError';
|
||||
|
||||
export type CodeRunOutcome = SuccessfulCodeRun | FailedCodeRun;
|
||||
|
||||
interface CodeRunStatus {
|
||||
readonly success: boolean;
|
||||
readonly error?: CodeRunError;
|
||||
}
|
||||
|
||||
interface SuccessfulCodeRun extends CodeRunStatus {
|
||||
readonly success: true;
|
||||
readonly error?: undefined;
|
||||
}
|
||||
|
||||
export interface FailedCodeRun extends CodeRunStatus {
|
||||
readonly success: false;
|
||||
readonly error: CodeRunError;
|
||||
}
|
||||
|
||||
export interface CodeRunError {
|
||||
readonly type: CodeRunErrorType;
|
||||
readonly message: string;
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const ScriptFileName = 'privacy-script' as const;
|
||||
1
src/application/CodeRunner/ScriptFilename.ts
Normal file
1
src/application/CodeRunner/ScriptFilename.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const ScriptFilename = 'privacy-script' as const;
|
||||
@@ -1,40 +1,116 @@
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||
import { CodeRunError, CodeRunErrorType } from '@/application/CodeRunner/CodeRunner';
|
||||
import { SystemOperations } from '../../System/SystemOperations';
|
||||
import { NodeElectronSystemOperations } from '../../System/NodeElectronSystemOperations';
|
||||
import { ScriptDirectoryProvider } from './ScriptDirectoryProvider';
|
||||
import { ScriptDirectoryOutcome, ScriptDirectoryProvider } from './ScriptDirectoryProvider';
|
||||
|
||||
export const ExecutionSubdirectory = 'runs';
|
||||
|
||||
/**
|
||||
* Provides a dedicated directory for script execution.
|
||||
* Benefits of using a persistent directory:
|
||||
* - Antivirus Exclusions: Easier antivirus configuration.
|
||||
* - Auditability: Stores script execution history for troubleshooting.
|
||||
* - Reliability: Avoids issues with directory clean-ups during execution,
|
||||
* seen in Windows Pro Azure VMs when stored on Windows temporary directory.
|
||||
*/
|
||||
export class PersistentDirectoryProvider implements ScriptDirectoryProvider {
|
||||
constructor(
|
||||
private readonly system: SystemOperations = new NodeElectronSystemOperations(),
|
||||
private readonly logger: Logger = ElectronLogger,
|
||||
) { }
|
||||
|
||||
async provideScriptDirectory(): Promise<string> {
|
||||
const scriptsDirectory = this.system.location.combinePaths(
|
||||
/*
|
||||
Switched from temporary to persistent directory for script storage for improved reliability.
|
||||
|
||||
Temporary directories in some environments, such certain Windows Pro Azure VMs, showed
|
||||
issues where scripts were interrupted due to directory cleanup during script execution.
|
||||
This was observed with system temp directories (e.g., `%LOCALAPPDATA%\Temp`).
|
||||
|
||||
Persistent directories offer better stability during long executions and aid in auditability
|
||||
and troubleshooting.
|
||||
*/
|
||||
this.system.operatingSystem.getUserDataDirectory(),
|
||||
ExecutionSubdirectory,
|
||||
);
|
||||
this.logger.info(`Attempting to create script directory at path: ${scriptsDirectory}`);
|
||||
try {
|
||||
await this.system.fileSystem.createDirectory(scriptsDirectory, true);
|
||||
this.logger.info(`Script directory successfully created at: ${scriptsDirectory}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error creating script directory at ${scriptsDirectory}: ${error.message}`, error);
|
||||
throw error;
|
||||
public async provideScriptDirectory(): Promise<ScriptDirectoryOutcome> {
|
||||
const {
|
||||
success: isPathConstructed,
|
||||
error: pathConstructionError,
|
||||
directoryPath,
|
||||
} = this.constructScriptDirectoryPath();
|
||||
if (!isPathConstructed) {
|
||||
return {
|
||||
success: false,
|
||||
error: pathConstructionError,
|
||||
};
|
||||
}
|
||||
return scriptsDirectory;
|
||||
const {
|
||||
success: isDirectoryCreated,
|
||||
error: directoryCreationError,
|
||||
} = await this.createDirectory(directoryPath);
|
||||
if (!isDirectoryCreated) {
|
||||
return {
|
||||
success: false,
|
||||
error: directoryCreationError,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
directoryAbsolutePath: directoryPath,
|
||||
};
|
||||
}
|
||||
|
||||
private async createDirectory(directoryPath: string): Promise<DirectoryPathCreationOutcome> {
|
||||
try {
|
||||
this.logger.info(`Attempting to create script directory at path: ${directoryPath}`);
|
||||
await this.system.fileSystem.createDirectory(directoryPath, true);
|
||||
this.logger.info(`Script directory successfully created at: ${directoryPath}`);
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: this.handleException(error, 'DirectoryCreationError'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private constructScriptDirectoryPath(): DirectoryPathConstructionOutcome {
|
||||
try {
|
||||
const parentDirectory = this.system.operatingSystem.getUserDataDirectory();
|
||||
const scriptDirectory = this.system.location.combinePaths(
|
||||
parentDirectory,
|
||||
ExecutionSubdirectory,
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
directoryPath: scriptDirectory,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: this.handleException(error, 'DirectoryCreationError'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private handleException(
|
||||
exception: Error,
|
||||
errorType: CodeRunErrorType,
|
||||
): CodeRunError {
|
||||
const errorMessage = 'Error during script directory creation';
|
||||
this.logger.error(errorType, errorMessage, exception);
|
||||
return {
|
||||
type: errorType,
|
||||
message: `${errorMessage}: ${exception.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type DirectoryPathConstructionOutcome = {
|
||||
readonly success: false;
|
||||
readonly error: CodeRunError;
|
||||
readonly directoryPath?: undefined;
|
||||
} | {
|
||||
readonly success: true;
|
||||
readonly directoryPath: string;
|
||||
readonly error?: undefined;
|
||||
};
|
||||
|
||||
type DirectoryPathCreationOutcome = {
|
||||
readonly success: false;
|
||||
readonly error: CodeRunError;
|
||||
} | {
|
||||
readonly success: true;
|
||||
readonly error?: undefined;
|
||||
};
|
||||
|
||||
@@ -1,3 +1,23 @@
|
||||
import { CodeRunError } from '@/application/CodeRunner/CodeRunner';
|
||||
|
||||
export interface ScriptDirectoryProvider {
|
||||
provideScriptDirectory(): Promise<string>;
|
||||
provideScriptDirectory(): Promise<ScriptDirectoryOutcome>;
|
||||
}
|
||||
|
||||
export type ScriptDirectoryOutcome = SuccessfulDirectoryCreation | FailedDirectoryCreation;
|
||||
|
||||
interface ScriptDirectoryCreationStatus {
|
||||
readonly success: boolean;
|
||||
readonly directoryAbsolutePath?: string;
|
||||
readonly error?: CodeRunError;
|
||||
}
|
||||
|
||||
interface SuccessfulDirectoryCreation extends ScriptDirectoryCreationStatus {
|
||||
readonly success: true;
|
||||
readonly directoryAbsolutePath: string;
|
||||
}
|
||||
|
||||
interface FailedDirectoryCreation extends ScriptDirectoryCreationStatus {
|
||||
readonly success: false;
|
||||
readonly error: CodeRunError;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ScriptFileNameParts } from '../ScriptFileCreator';
|
||||
import { ScriptFilenameParts } from '../ScriptFileCreator';
|
||||
|
||||
export interface FilenameGenerator {
|
||||
generateFilename(scriptFileNameParts: ScriptFileNameParts): string;
|
||||
generateFilename(scriptFilenameParts: ScriptFilenameParts): string;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { ScriptFileNameParts } from '../ScriptFileCreator';
|
||||
import { ScriptFilenameParts } from '../ScriptFileCreator';
|
||||
import { FilenameGenerator } from './FilenameGenerator';
|
||||
|
||||
export class TimestampedFilenameGenerator implements FilenameGenerator {
|
||||
public generateFilename(
|
||||
scriptFileNameParts: ScriptFileNameParts,
|
||||
scriptFilenameParts: ScriptFilenameParts,
|
||||
date = new Date(),
|
||||
): string {
|
||||
validateScriptFileNameParts(scriptFileNameParts);
|
||||
const baseFileName = `${createTimeStampForFile(date)}-${scriptFileNameParts.scriptName}`;
|
||||
return scriptFileNameParts.scriptFileExtension ? `${baseFileName}.${scriptFileNameParts.scriptFileExtension}` : baseFileName;
|
||||
validateScriptFilenameParts(scriptFilenameParts);
|
||||
const baseFilename = `${createTimeStampForFile(date)}-${scriptFilenameParts.scriptName}`;
|
||||
return scriptFilenameParts.scriptFileExtension ? `${baseFilename}.${scriptFilenameParts.scriptFileExtension}` : baseFilename;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@ function createTimeStampForFile(date: Date): string {
|
||||
.replace(/\..+/, '');
|
||||
}
|
||||
|
||||
function validateScriptFileNameParts(scriptFileNameParts: ScriptFileNameParts) {
|
||||
if (!scriptFileNameParts.scriptName) {
|
||||
function validateScriptFilenameParts(scriptFilenameParts: ScriptFilenameParts) {
|
||||
if (!scriptFilenameParts.scriptName) {
|
||||
throw new Error('Script name is required but not provided.');
|
||||
}
|
||||
if (scriptFileNameParts.scriptFileExtension?.startsWith('.')) {
|
||||
if (scriptFilenameParts.scriptFileExtension?.startsWith('.')) {
|
||||
throw new Error('File extension should not start with a dot.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
import { CodeRunError, CodeRunErrorType } from '@/application/CodeRunner/CodeRunner';
|
||||
import { SystemOperations } from '../System/SystemOperations';
|
||||
import { NodeElectronSystemOperations } from '../System/NodeElectronSystemOperations';
|
||||
import { FilenameGenerator } from './Filename/FilenameGenerator';
|
||||
import { ScriptFileNameParts, ScriptFileCreator } from './ScriptFileCreator';
|
||||
import { ScriptFilenameParts, ScriptFileCreator, ScriptFileCreationOutcome } from './ScriptFileCreator';
|
||||
import { TimestampedFilenameGenerator } from './Filename/TimestampedFilenameGenerator';
|
||||
import { ScriptDirectoryProvider } from './Directory/ScriptDirectoryProvider';
|
||||
import { PersistentDirectoryProvider } from './Directory/PersistentDirectoryProvider';
|
||||
@@ -18,23 +19,99 @@ export class ScriptFileCreationOrchestrator implements ScriptFileCreator {
|
||||
|
||||
public async createScriptFile(
|
||||
contents: string,
|
||||
scriptFileNameParts: ScriptFileNameParts,
|
||||
): Promise<string> {
|
||||
const filePath = await this.provideFilePath(scriptFileNameParts);
|
||||
await this.createFile(filePath, contents);
|
||||
return filePath;
|
||||
scriptFilenameParts: ScriptFilenameParts,
|
||||
): Promise<ScriptFileCreationOutcome> {
|
||||
const {
|
||||
success: isDirectoryCreated, error: directoryCreationError, directoryAbsolutePath,
|
||||
} = await this.directoryProvider.provideScriptDirectory();
|
||||
if (!isDirectoryCreated) {
|
||||
return createFailure(directoryCreationError);
|
||||
}
|
||||
const {
|
||||
success: isFilePathConstructed, error: filePathGenerationError, filePath,
|
||||
} = this.constructFilePath(scriptFilenameParts, directoryAbsolutePath);
|
||||
if (!isFilePathConstructed) {
|
||||
return createFailure(filePathGenerationError);
|
||||
}
|
||||
const {
|
||||
success: isFileCreated, error: fileCreationError,
|
||||
} = await this.writeFile(filePath, contents);
|
||||
if (!isFileCreated) {
|
||||
return createFailure(fileCreationError);
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
scriptFileAbsolutePath: filePath,
|
||||
};
|
||||
}
|
||||
|
||||
private async provideFilePath(scriptFileNameParts: ScriptFileNameParts): Promise<string> {
|
||||
const filename = this.filenameGenerator.generateFilename(scriptFileNameParts);
|
||||
const directoryPath = await this.directoryProvider.provideScriptDirectory();
|
||||
const filePath = this.system.location.combinePaths(directoryPath, filename);
|
||||
return filePath;
|
||||
private constructFilePath(
|
||||
scriptFilenameParts: ScriptFilenameParts,
|
||||
directoryPath: string,
|
||||
): FilePathConstructionOutcome {
|
||||
try {
|
||||
const filename = this.filenameGenerator.generateFilename(scriptFilenameParts);
|
||||
const filePath = this.system.location.combinePaths(directoryPath, filename);
|
||||
return { success: true, filePath };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: this.handleException(error, 'FilePathGenerationError'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async createFile(filePath: string, contents: string): Promise<void> {
|
||||
this.logger.info(`Creating file at ${filePath}, size: ${contents.length} characters`);
|
||||
await this.system.fileSystem.writeToFile(filePath, contents);
|
||||
this.logger.info(`File created successfully at ${filePath}`);
|
||||
private async writeFile(
|
||||
filePath: string,
|
||||
contents: string,
|
||||
): Promise<FileWriteOutcome> {
|
||||
try {
|
||||
this.logger.info(`Creating file at ${filePath}, size: ${contents.length} characters`);
|
||||
await this.system.fileSystem.writeToFile(filePath, contents);
|
||||
this.logger.info(`File created successfully at ${filePath}`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: this.handleException(error, 'FileWriteError'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private handleException(
|
||||
exception: Error,
|
||||
errorType: CodeRunErrorType,
|
||||
): CodeRunError {
|
||||
const errorMessage = 'Error during script file operation';
|
||||
this.logger.error(errorType, errorMessage, exception);
|
||||
return {
|
||||
type: errorType,
|
||||
message: `${errorMessage}: ${exception.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function createFailure(error: CodeRunError): ScriptFileCreationOutcome {
|
||||
return {
|
||||
success: false,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
type FileWriteOutcome = {
|
||||
readonly success: true;
|
||||
readonly error?: undefined;
|
||||
} | {
|
||||
readonly success: false;
|
||||
readonly error: CodeRunError;
|
||||
};
|
||||
|
||||
type FilePathConstructionOutcome = {
|
||||
readonly success: true;
|
||||
readonly filePath: string;
|
||||
readonly error?: undefined;
|
||||
} | {
|
||||
readonly success: false;
|
||||
readonly filePath?: undefined;
|
||||
readonly error: CodeRunError;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,31 @@
|
||||
import { CodeRunError } from '@/application/CodeRunner/CodeRunner';
|
||||
|
||||
export interface ScriptFileCreator {
|
||||
createScriptFile(
|
||||
contents: string,
|
||||
scriptFileNameParts: ScriptFileNameParts,
|
||||
): Promise<string>;
|
||||
scriptFilenameParts: ScriptFilenameParts,
|
||||
): Promise<ScriptFileCreationOutcome>;
|
||||
}
|
||||
|
||||
export interface ScriptFileNameParts {
|
||||
export interface ScriptFilenameParts {
|
||||
readonly scriptName: string;
|
||||
readonly scriptFileExtension: string | undefined;
|
||||
}
|
||||
|
||||
export type ScriptFileCreationOutcome = SuccessfulScriptCreation | FailedScriptCreation;
|
||||
|
||||
interface ScriptFileCreationStatus {
|
||||
readonly success: boolean;
|
||||
readonly error?: CodeRunError;
|
||||
readonly scriptFileAbsolutePath?: string;
|
||||
}
|
||||
|
||||
interface SuccessfulScriptCreation extends ScriptFileCreationStatus {
|
||||
readonly success: true;
|
||||
readonly scriptFileAbsolutePath: string;
|
||||
}
|
||||
|
||||
interface FailedScriptCreation extends ScriptFileCreationStatus {
|
||||
readonly success: false;
|
||||
readonly error: CodeRunError;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
import { CodeRunError } from '@/application/CodeRunner/CodeRunner';
|
||||
|
||||
export interface ScriptFileExecutor {
|
||||
executeScriptFile(filePath: string): Promise<void>;
|
||||
executeScriptFile(filePath: string): Promise<ScriptFileExecutionOutcome>;
|
||||
}
|
||||
|
||||
export type ScriptFileExecutionOutcome = SuccessfulScriptFileExecution | FailedScriptFileExecution;
|
||||
|
||||
interface ScriptFileExecutionStatus {
|
||||
readonly success: boolean;
|
||||
readonly error?: CodeRunError;
|
||||
}
|
||||
|
||||
interface SuccessfulScriptFileExecution extends ScriptFileExecutionStatus {
|
||||
readonly success: true;
|
||||
readonly error?: undefined;
|
||||
}
|
||||
|
||||
export interface FailedScriptFileExecution extends ScriptFileExecutionStatus {
|
||||
readonly success: false;
|
||||
readonly error: CodeRunError;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||
import { NodeElectronSystemOperations } from '@/infrastructure/CodeRunner/System/NodeElectronSystemOperations';
|
||||
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||
import { ScriptFileExecutor } from './ScriptFileExecutor';
|
||||
import { CodeRunErrorType } from '@/application/CodeRunner/CodeRunner';
|
||||
import { isString } from '@/TypeHelpers';
|
||||
import { FailedScriptFileExecution, ScriptFileExecutionOutcome, ScriptFileExecutor } from './ScriptFileExecutor';
|
||||
|
||||
export class VisibleTerminalScriptExecutor implements ScriptFileExecutor {
|
||||
constructor(
|
||||
@@ -14,38 +16,77 @@ export class VisibleTerminalScriptExecutor implements ScriptFileExecutor {
|
||||
private readonly environment: RuntimeEnvironment = CurrentEnvironment,
|
||||
) { }
|
||||
|
||||
public async executeScriptFile(filePath: string): Promise<void> {
|
||||
public async executeScriptFile(filePath: string): Promise<ScriptFileExecutionOutcome> {
|
||||
const { os } = this.environment;
|
||||
if (os === undefined) {
|
||||
throw new Error('Unknown operating system');
|
||||
return this.handleError('UnsupportedOperatingSystem', 'Operating system could not be identified from environment.');
|
||||
}
|
||||
await this.setFileExecutablePermissions(filePath);
|
||||
await this.runFileWithRunner(filePath, os);
|
||||
const filePermissionsResult = await this.setFileExecutablePermissions(filePath);
|
||||
if (!filePermissionsResult.success) {
|
||||
return filePermissionsResult;
|
||||
}
|
||||
const scriptExecutionResult = await this.runFileWithRunner(filePath, os);
|
||||
if (!scriptExecutionResult.success) {
|
||||
return scriptExecutionResult;
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
private async setFileExecutablePermissions(filePath: string): Promise<void> {
|
||||
private async setFileExecutablePermissions(
|
||||
filePath: string,
|
||||
): Promise<ScriptFileExecutionOutcome> {
|
||||
/*
|
||||
This is required on macOS and Linux otherwise the terminal emulators will refuse to
|
||||
execute the script. It's not needed on Windows.
|
||||
*/
|
||||
this.logger.info(`Setting execution permissions for file at ${filePath}`);
|
||||
await this.system.fileSystem.setFilePermissions(filePath, '755');
|
||||
this.logger.info(`Execution permissions set successfully for ${filePath}`);
|
||||
try {
|
||||
this.logger.info(`Setting execution permissions for file at ${filePath}`);
|
||||
await this.system.fileSystem.setFilePermissions(filePath, '755');
|
||||
this.logger.info(`Execution permissions set successfully for ${filePath}`);
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return this.handleError('FileExecutionError', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async runFileWithRunner(filePath: string, os: OperatingSystem): Promise<void> {
|
||||
private async runFileWithRunner(
|
||||
filePath: string,
|
||||
os: OperatingSystem,
|
||||
): Promise<ScriptFileExecutionOutcome> {
|
||||
this.logger.info(`Executing script file: ${filePath} on ${OperatingSystem[os]}.`);
|
||||
const runner = TerminalRunners[os];
|
||||
if (!runner) {
|
||||
throw new Error(`Unsupported operating system: ${OperatingSystem[os]}`);
|
||||
return this.handleError('UnsupportedOperatingSystem', `Unsupported operating system: ${OperatingSystem[os]}`);
|
||||
}
|
||||
const context: TerminalExecutionContext = {
|
||||
scriptFilePath: filePath,
|
||||
commandOps: this.system.command,
|
||||
logger: this.logger,
|
||||
};
|
||||
await runner(context);
|
||||
this.logger.info('Command script file successfully.');
|
||||
try {
|
||||
await runner(context);
|
||||
this.logger.info('Command script file successfully.');
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return this.handleError('FileExecutionError', error);
|
||||
}
|
||||
}
|
||||
|
||||
private handleError(
|
||||
type: CodeRunErrorType,
|
||||
error: Error | string,
|
||||
): FailedScriptFileExecution {
|
||||
const errorMessage = 'Error during script file execution';
|
||||
this.logger.error([type, errorMessage, ...(error ? [error] : [])]);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
type,
|
||||
message: `${errorMessage}: ${isString(error) ? error : errorMessage}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
import { ScriptFileName } from '@/application/CodeRunner/ScriptFileName';
|
||||
import { CodeRunner } from '@/application/CodeRunner/CodeRunner';
|
||||
import { ScriptFilename } from '@/application/CodeRunner/ScriptFilename';
|
||||
import {
|
||||
CodeRunError, CodeRunOutcome, CodeRunner, FailedCodeRun,
|
||||
} from '@/application/CodeRunner/CodeRunner';
|
||||
import { ElectronLogger } from '../Log/ElectronLogger';
|
||||
import { ScriptFileExecutor } from './Execution/ScriptFileExecutor';
|
||||
import { ScriptFileCreator } from './Creation/ScriptFileCreator';
|
||||
@@ -18,18 +20,38 @@ export class ScriptFileCodeRunner implements CodeRunner {
|
||||
public async runCode(
|
||||
code: string,
|
||||
fileExtension: string,
|
||||
): Promise<void> {
|
||||
): Promise<CodeRunOutcome> {
|
||||
this.logger.info('Initiating script running process.');
|
||||
try {
|
||||
const scriptFilePath = await this.scriptFileCreator.createScriptFile(code, {
|
||||
scriptName: ScriptFileName,
|
||||
scriptFileExtension: fileExtension,
|
||||
});
|
||||
await this.scriptFileExecutor.executeScriptFile(scriptFilePath);
|
||||
this.logger.info(`Successfully ran script at ${scriptFilePath}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error running script: ${error.message}`, error);
|
||||
throw error;
|
||||
const {
|
||||
success: isFileCreated, scriptFileAbsolutePath, error: fileCreationError,
|
||||
} = await this.scriptFileCreator.createScriptFile(code, {
|
||||
scriptName: ScriptFilename,
|
||||
scriptFileExtension: fileExtension,
|
||||
});
|
||||
if (!isFileCreated) {
|
||||
return createFailure(fileCreationError);
|
||||
}
|
||||
const {
|
||||
success: isFileSuccessfullyExecuted,
|
||||
error: fileExecutionError,
|
||||
} = await this.scriptFileExecutor.executeScriptFile(
|
||||
scriptFileAbsolutePath,
|
||||
);
|
||||
if (!isFileSuccessfullyExecuted) {
|
||||
return createFailure(fileExecutionError);
|
||||
}
|
||||
this.logger.info(`Successfully ran script at ${scriptFileAbsolutePath}`);
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function createFailure(
|
||||
error: CodeRunError,
|
||||
): FailedCodeRun {
|
||||
return {
|
||||
success: false,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
import { Dialog, FileType } from '@/presentation/common/Dialog';
|
||||
import { Dialog, FileType, SaveFileOutcome } from '@/presentation/common/Dialog';
|
||||
import { FileSaverDialog } from './FileSaverDialog';
|
||||
import { BrowserSaveFileDialog } from './BrowserSaveFileDialog';
|
||||
|
||||
export class BrowserDialog implements Dialog {
|
||||
constructor(private readonly saveFileDialog: BrowserSaveFileDialog = new FileSaverDialog()) {
|
||||
constructor(
|
||||
private readonly window: WindowDialogAccessor = globalThis.window,
|
||||
private readonly saveFileDialog: BrowserSaveFileDialog = new FileSaverDialog(),
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
public showError(title: string, message: string): void {
|
||||
this.window.alert(`❌ ${title}\n\n${message}`);
|
||||
}
|
||||
|
||||
public saveFile(
|
||||
fileContents: string,
|
||||
fileName: string,
|
||||
defaultFilename: string,
|
||||
type: FileType,
|
||||
): Promise<void> {
|
||||
): Promise<SaveFileOutcome> {
|
||||
return Promise.resolve(
|
||||
this.saveFileDialog.saveFile(fileContents, fileName, type),
|
||||
this.saveFileDialog.saveFile(fileContents, defaultFilename, type),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface WindowDialogAccessor {
|
||||
readonly alert: typeof window.alert;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { FileType } from '@/presentation/common/Dialog';
|
||||
import { FileType, SaveFileOutcome } from '@/presentation/common/Dialog';
|
||||
|
||||
export interface BrowserSaveFileDialog {
|
||||
saveFile(
|
||||
fileContents: string,
|
||||
fileName: string,
|
||||
defaultFilename: string,
|
||||
fileType: FileType,
|
||||
): void;
|
||||
): SaveFileOutcome;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import fileSaver from 'file-saver';
|
||||
import { FileType } from '@/presentation/common/Dialog';
|
||||
import { FileType, SaveFileOutcome } from '@/presentation/common/Dialog';
|
||||
import { BrowserSaveFileDialog } from './BrowserSaveFileDialog';
|
||||
|
||||
export type SaveAsFunction = (data: Blob, filename?: string) => void;
|
||||
@@ -14,17 +14,20 @@ export class FileSaverDialog implements BrowserSaveFileDialog {
|
||||
|
||||
public saveFile(
|
||||
fileContents: string,
|
||||
fileName: string,
|
||||
defaultFilename: string,
|
||||
fileType: FileType,
|
||||
): void {
|
||||
): SaveFileOutcome {
|
||||
const mimeType = MimeTypes[fileType];
|
||||
this.saveBlob(fileContents, mimeType, fileName);
|
||||
this.saveBlob(fileContents, mimeType, defaultFilename);
|
||||
return {
|
||||
success: true, // Exceptions are handled internally
|
||||
};
|
||||
}
|
||||
|
||||
private saveBlob(file: BlobPart, mimeType: string, fileName: string): void {
|
||||
private saveBlob(file: BlobPart, mimeType: string, defaultFilename: string): void {
|
||||
try {
|
||||
const blob = new Blob([file], { type: mimeType });
|
||||
this.fileSaverSaveAs(blob, fileName);
|
||||
this.fileSaverSaveAs(blob, defaultFilename);
|
||||
} catch (e) {
|
||||
this.windowOpen(`data:${mimeType},${encodeURIComponent(file.toString())}`, '_blank', '');
|
||||
}
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
import { Dialog, FileType } from '@/presentation/common/Dialog';
|
||||
import { dialog } from 'electron/main';
|
||||
import { Dialog, FileType, SaveFileOutcome } from '@/presentation/common/Dialog';
|
||||
import { NodeElectronSaveFileDialog } from './NodeElectronSaveFileDialog';
|
||||
import { ElectronSaveFileDialog } from './ElectronSaveFileDialog';
|
||||
|
||||
export class ElectronDialog implements Dialog {
|
||||
constructor(
|
||||
private readonly fileSaveDialog: ElectronSaveFileDialog = new NodeElectronSaveFileDialog(),
|
||||
private readonly saveFileDialog: ElectronSaveFileDialog = new NodeElectronSaveFileDialog(),
|
||||
private readonly electron: ElectronDialogAccessor = {
|
||||
showErrorBox: dialog.showErrorBox.bind(dialog),
|
||||
},
|
||||
) { }
|
||||
|
||||
public async saveFile(
|
||||
public saveFile(
|
||||
fileContents: string,
|
||||
fileName: string,
|
||||
defaultFilename: string,
|
||||
type: FileType,
|
||||
): Promise<void> {
|
||||
await this.fileSaveDialog.saveFile(fileContents, fileName, type);
|
||||
): Promise<SaveFileOutcome> {
|
||||
return this.saveFileDialog.saveFile(fileContents, defaultFilename, type);
|
||||
}
|
||||
|
||||
public showError(title: string, message: string): void {
|
||||
this.electron.showErrorBox(title, message);
|
||||
}
|
||||
}
|
||||
|
||||
export interface ElectronDialogAccessor {
|
||||
readonly showErrorBox: typeof dialog.showErrorBox;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { FileType } from '@/presentation/common/Dialog';
|
||||
import { FileType, SaveFileOutcome } from '@/presentation/common/Dialog';
|
||||
|
||||
export interface ElectronSaveFileDialog {
|
||||
saveFile(
|
||||
fileContents: string,
|
||||
fileName: string,
|
||||
defaultFilename: string,
|
||||
type: FileType,
|
||||
): Promise<void>;
|
||||
): Promise<SaveFileOutcome>;
|
||||
}
|
||||
|
||||
@@ -3,19 +3,11 @@ import { writeFile } from 'node:fs/promises';
|
||||
import { app, dialog } from 'electron/main';
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||
import { FileType } from '@/presentation/common/Dialog';
|
||||
import {
|
||||
FileType, SaveFileError, SaveFileErrorType, SaveFileOutcome,
|
||||
} from '@/presentation/common/Dialog';
|
||||
import { ElectronSaveFileDialog } from './ElectronSaveFileDialog';
|
||||
|
||||
export interface ElectronFileDialogOperations {
|
||||
getUserDownloadsPath(): string;
|
||||
showSaveDialog(options: Electron.SaveDialogOptions): Promise<Electron.SaveDialogReturnValue>;
|
||||
}
|
||||
|
||||
export interface NodeFileOperations {
|
||||
readonly join: typeof join;
|
||||
writeFile(file: string, data: string): Promise<void>;
|
||||
}
|
||||
|
||||
export class NodeElectronSaveFileDialog implements ElectronSaveFileDialog {
|
||||
constructor(
|
||||
private readonly logger: Logger = ElectronLogger,
|
||||
@@ -31,44 +23,123 @@ export class NodeElectronSaveFileDialog implements ElectronSaveFileDialog {
|
||||
|
||||
public async saveFile(
|
||||
fileContents: string,
|
||||
fileName: string,
|
||||
defaultFilename: string,
|
||||
type: FileType,
|
||||
): Promise<void> {
|
||||
const userSelectedFilePath = await this.showSaveFileDialog(fileName, type);
|
||||
if (!userSelectedFilePath) {
|
||||
this.logger.info(`File save cancelled by user: ${fileName}`);
|
||||
return;
|
||||
): Promise<SaveFileOutcome> {
|
||||
const {
|
||||
success: isPathConstructed,
|
||||
filePath: defaultFilePath,
|
||||
error: pathConstructionError,
|
||||
} = this.constructDefaultFilePath(defaultFilename);
|
||||
if (!isPathConstructed) {
|
||||
return { success: false, error: pathConstructionError };
|
||||
}
|
||||
await this.writeFile(userSelectedFilePath, fileContents);
|
||||
const fileDialog = await this.showSaveFileDialog(defaultFilename, defaultFilePath, type);
|
||||
if (!fileDialog.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: fileDialog.error,
|
||||
};
|
||||
}
|
||||
if (fileDialog.canceled) {
|
||||
this.logger.info(`File save cancelled by user: ${defaultFilename}`);
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
const result = await this.writeFile(fileDialog.filePath, fileContents);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async writeFile(filePath: string, fileContents: string): Promise<void> {
|
||||
private async writeFile(
|
||||
filePath: string,
|
||||
fileContents: string,
|
||||
): Promise<SaveFileOutcome> {
|
||||
try {
|
||||
this.logger.info(`Saving file: ${filePath}`);
|
||||
await this.node.writeFile(filePath, fileContents);
|
||||
this.logger.info(`File saved: ${filePath}`);
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Error saving file: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: this.handleException(error, 'FileCreationError'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async showSaveFileDialog(fileName: string, type: FileType): Promise<string | undefined> {
|
||||
const downloadsFolder = this.electron.getUserDownloadsPath();
|
||||
const defaultFilePath = this.node.join(downloadsFolder, fileName);
|
||||
const dialogResult = await this.electron.showSaveDialog({
|
||||
title: fileName,
|
||||
defaultPath: defaultFilePath,
|
||||
filters: getDialogFileFilters(type),
|
||||
properties: [
|
||||
'createDirectory', // Enables directory creation on macOS.
|
||||
'showOverwriteConfirmation', // Shows overwrite confirmation on Linux.
|
||||
],
|
||||
});
|
||||
if (dialogResult.canceled) {
|
||||
return undefined;
|
||||
private async showSaveFileDialog(
|
||||
defaultFilename: string,
|
||||
defaultFilePath: string,
|
||||
type: FileType,
|
||||
): Promise<SaveDialogOutcome> {
|
||||
try {
|
||||
const dialogResult = await this.electron.showSaveDialog({
|
||||
title: defaultFilename,
|
||||
defaultPath: defaultFilePath,
|
||||
filters: getDialogFileFilters(type),
|
||||
properties: [
|
||||
'createDirectory', // Enables directory creation on macOS.
|
||||
'showOverwriteConfirmation', // Shows overwrite confirmation on Linux.
|
||||
],
|
||||
});
|
||||
if (dialogResult.canceled) {
|
||||
return { success: true, canceled: true };
|
||||
}
|
||||
if (!dialogResult.filePath) {
|
||||
return {
|
||||
success: false,
|
||||
error: { type: 'DialogDisplayError', message: 'Unexpected Error: File path is undefined after save dialog completion.' },
|
||||
};
|
||||
}
|
||||
return { success: true, filePath: dialogResult.filePath };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: this.handleException(error, 'DialogDisplayError'),
|
||||
};
|
||||
}
|
||||
return dialogResult.filePath;
|
||||
}
|
||||
|
||||
private constructDefaultFilePath(defaultFilename: string): DefaultFilePathConstructionOutcome {
|
||||
try {
|
||||
const downloadsFolder = this.electron.getUserDownloadsPath();
|
||||
const defaultFilePath = this.node.join(downloadsFolder, defaultFilename);
|
||||
return {
|
||||
success: true,
|
||||
filePath: defaultFilePath,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false,
|
||||
error: this.handleException(err, 'DialogDisplayError'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private handleException(
|
||||
exception: Error,
|
||||
errorType: SaveFileErrorType,
|
||||
): SaveFileError {
|
||||
const errorMessage = 'Error during saving script file.';
|
||||
this.logger.error(errorType, errorMessage, exception);
|
||||
return {
|
||||
type: errorType,
|
||||
message: `${errorMessage}: ${exception.message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface ElectronFileDialogOperations {
|
||||
getUserDownloadsPath(): string;
|
||||
showSaveDialog(options: Electron.SaveDialogOptions): Promise<Electron.SaveDialogReturnValue>;
|
||||
}
|
||||
|
||||
export interface NodeFileOperations {
|
||||
readonly join: typeof join;
|
||||
writeFile(file: string, data: string): Promise<void>;
|
||||
}
|
||||
|
||||
function getDialogFileFilters(fileType: FileType): Electron.FileFilter[] {
|
||||
@@ -96,3 +167,12 @@ const FileTypeSpecificFilters: Record<FileType, Electron.FileFilter[]> = {
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
type SaveDialogOutcome =
|
||||
| { readonly success: true; readonly filePath: string; readonly canceled?: false }
|
||||
| { readonly success: true; readonly canceled: true }
|
||||
| { readonly success: false; readonly error: SaveFileError; readonly canceled?: false };
|
||||
|
||||
type DefaultFilePathConstructionOutcome =
|
||||
| { readonly success: true; readonly filePath: string; readonly error?: undefined; }
|
||||
| { readonly success: false; readonly filePath?: undefined; readonly error: SaveFileError; };
|
||||
|
||||
36
src/infrastructure/Dialog/LoggingDialogDecorator.ts
Normal file
36
src/infrastructure/Dialog/LoggingDialogDecorator.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { Logger } from '@/application/Common/Log/Logger';
|
||||
import { Dialog, FileType } from '@/presentation/common/Dialog';
|
||||
|
||||
export function decorateWithLogging(
|
||||
dialog: Dialog,
|
||||
logger: Logger,
|
||||
): Dialog {
|
||||
return new LoggingDialogDecorator(dialog, logger);
|
||||
}
|
||||
|
||||
class LoggingDialogDecorator implements Dialog {
|
||||
constructor(
|
||||
private readonly dialog: Dialog,
|
||||
private readonly logger: Logger,
|
||||
) { }
|
||||
|
||||
public async saveFile(
|
||||
fileContents: string,
|
||||
defaultFilename: string,
|
||||
fileType: FileType,
|
||||
) {
|
||||
this.logger.info(`Opening save file dialog with default filename: ${defaultFilename}.`);
|
||||
const dialogResult = await this.dialog.saveFile(fileContents, defaultFilename, fileType);
|
||||
if (dialogResult.success) {
|
||||
this.logger.info('File saving process completed successfully.');
|
||||
} else {
|
||||
this.logger.error('Error encountered while saving the file.', dialogResult.error);
|
||||
}
|
||||
return dialogResult;
|
||||
}
|
||||
|
||||
public showError(title: string, message: string) {
|
||||
this.logger.error(`Showing error dialog: ${title} - ${message}`);
|
||||
this.dialog.showError(title, message);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,35 @@
|
||||
export interface Dialog {
|
||||
saveFile(fileContents: string, fileName: string, type: FileType): Promise<void>;
|
||||
showError(title: string, message: string): void;
|
||||
saveFile(fileContents: string, defaultFilename: string, type: FileType): Promise<SaveFileOutcome>;
|
||||
}
|
||||
|
||||
export enum FileType {
|
||||
BatchFile,
|
||||
ShellScript,
|
||||
}
|
||||
|
||||
export type SaveFileOutcome = SuccessfulSaveFile | FailedSaveFile;
|
||||
|
||||
interface SaveFileStatus {
|
||||
readonly success: boolean;
|
||||
readonly error?: SaveFileError;
|
||||
}
|
||||
|
||||
interface SuccessfulSaveFile extends SaveFileStatus {
|
||||
readonly success: true;
|
||||
readonly error?: SaveFileError;
|
||||
}
|
||||
|
||||
interface FailedSaveFile extends SaveFileStatus {
|
||||
readonly success: false;
|
||||
readonly error: SaveFileError;
|
||||
}
|
||||
|
||||
export interface SaveFileError {
|
||||
readonly type: SaveFileErrorType;
|
||||
readonly message: string;
|
||||
}
|
||||
|
||||
export type SaveFileErrorType =
|
||||
| 'FileCreationError'
|
||||
| 'DialogDisplayError';
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
v-if="canRun"
|
||||
text="Run"
|
||||
icon-name="play"
|
||||
@click="executeCode"
|
||||
@click="runCode"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import { injectKey } from '@/presentation/injectionSymbols';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { Dialog } from '@/presentation/common/Dialog';
|
||||
import IconButton from './IconButton.vue';
|
||||
|
||||
export default defineComponent({
|
||||
@@ -21,6 +22,7 @@ export default defineComponent({
|
||||
const { currentState, currentContext } = injectKey((keys) => keys.useCollectionState);
|
||||
const { os, isRunningAsDesktopApplication } = injectKey((keys) => keys.useRuntimeEnvironment);
|
||||
const { codeRunner } = injectKey((keys) => keys.useCodeRunner);
|
||||
const { dialog } = injectKey((keys) => keys.useDialog);
|
||||
|
||||
const canRun = computed<boolean>(() => getCanRunState(
|
||||
currentState.value.os,
|
||||
@@ -28,17 +30,20 @@ export default defineComponent({
|
||||
os,
|
||||
));
|
||||
|
||||
async function executeCode() {
|
||||
async function runCode() {
|
||||
if (!codeRunner) { throw new Error('missing code runner'); }
|
||||
await codeRunner.runCode(
|
||||
const { success, error } = await codeRunner.runCode(
|
||||
currentContext.state.code.current,
|
||||
currentContext.state.collection.scripting.fileExtension,
|
||||
);
|
||||
if (!success) {
|
||||
showScriptRunError(dialog, `${error.type}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
canRun,
|
||||
executeCode,
|
||||
runCode,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -51,4 +56,24 @@ function getCanRunState(
|
||||
const isRunningOnSelectedOs = selectedOs === hostOs;
|
||||
return isRunningAsDesktopApplication && isRunningOnSelectedOs;
|
||||
}
|
||||
|
||||
function showScriptRunError(dialog: Dialog, technicalDetails: string) {
|
||||
dialog.showError(
|
||||
'Error Running Script',
|
||||
[
|
||||
'We encountered an issue while running the script.',
|
||||
'This could be due to a variety of factors such as system permissions, resource constraints, or security software interventions.',
|
||||
'\n',
|
||||
'Here are some steps you can take:',
|
||||
'- Confirm that you have the necessary permissions to execute scripts on your system.',
|
||||
'- Check if there is sufficient disk space and system resources available.',
|
||||
'- Antivirus or security software can sometimes mistakenly block script execution. If you suspect this, verify your security settings, or temporarily disable the security software to see if that resolves the issue.',
|
||||
'- If possible, try running a different script to determine if the issue is specific to a particular script.',
|
||||
'- Should the problem persist, reach out to the community for further assistance.',
|
||||
'\n',
|
||||
'For your reference, here are the technical details of the error:',
|
||||
technicalDetails,
|
||||
].join('\n'),
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -19,8 +19,8 @@ import { injectKey } from '@/presentation/injectionSymbols';
|
||||
import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { ScriptFileName } from '@/application/CodeRunner/ScriptFileName';
|
||||
import { FileType } from '@/presentation/common/Dialog';
|
||||
import { ScriptFilename } from '@/application/CodeRunner/ScriptFilename';
|
||||
import { Dialog, FileType } from '@/presentation/common/Dialog';
|
||||
import IconButton from '../IconButton.vue';
|
||||
import InstructionList from './Instructions/InstructionList.vue';
|
||||
import { IInstructionListData } from './Instructions/InstructionListData';
|
||||
@@ -38,25 +38,28 @@ export default defineComponent({
|
||||
const { dialog } = injectKey((keys) => keys.useDialog);
|
||||
|
||||
const areInstructionsVisible = ref(false);
|
||||
const fileName = computed<string>(() => buildFileName(currentState.value.collection.scripting));
|
||||
const filename = computed<string>(() => buildFilename(currentState.value.collection.scripting));
|
||||
const instructions = computed<IInstructionListData | undefined>(() => getInstructions(
|
||||
currentState.value.collection.os,
|
||||
fileName.value,
|
||||
filename.value,
|
||||
));
|
||||
|
||||
async function saveCode() {
|
||||
await dialog.saveFile(
|
||||
const { success, error } = await dialog.saveFile(
|
||||
currentState.value.code.current,
|
||||
fileName.value,
|
||||
filename.value,
|
||||
getType(currentState.value.collection.scripting.language),
|
||||
);
|
||||
if (!success) {
|
||||
showScriptSaveError(dialog, `${error.type}: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
areInstructionsVisible.value = true;
|
||||
}
|
||||
|
||||
return {
|
||||
isRunningAsDesktopApplication,
|
||||
instructions,
|
||||
fileName,
|
||||
areInstructionsVisible,
|
||||
saveCode,
|
||||
};
|
||||
@@ -74,10 +77,30 @@ function getType(language: ScriptingLanguage) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildFileName(scripting: IScriptingDefinition) {
|
||||
function buildFilename(scripting: IScriptingDefinition) {
|
||||
if (scripting.fileExtension) {
|
||||
return `${ScriptFileName}.${scripting.fileExtension}`;
|
||||
return `${ScriptFilename}.${scripting.fileExtension}`;
|
||||
}
|
||||
return ScriptFileName;
|
||||
return ScriptFilename;
|
||||
}
|
||||
|
||||
function showScriptSaveError(dialog: Dialog, technicalDetails: string) {
|
||||
dialog.showError(
|
||||
'Error Saving Script',
|
||||
[
|
||||
'An error occurred while saving the script.',
|
||||
'This issue may arise from insufficient permissions, limited disk space, or interference from security software.',
|
||||
'\n',
|
||||
'To address this:',
|
||||
'- Verify your permissions for the selected save directory.',
|
||||
'- Check available disk space.',
|
||||
'- Review your antivirus or security settings; adding an exclusion for privacy.sexy might be necessary.',
|
||||
'- Try saving the script to a different location or modifying your selection.',
|
||||
'- If the problem persists, reach out to the community for further assistance.',
|
||||
'\n',
|
||||
'Technical Details:',
|
||||
technicalDetails,
|
||||
].join('\n'),
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,25 @@
|
||||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||
import { Dialog } from '@/presentation/common/Dialog';
|
||||
import { BrowserDialog } from '@/infrastructure/Dialog/Browser/BrowserDialog';
|
||||
import { decorateWithLogging } from '@/infrastructure/Dialog/LoggingDialogDecorator';
|
||||
import { ClientLoggerFactory } from '../Log/ClientLoggerFactory';
|
||||
|
||||
export function determineDialogBasedOnEnvironment(
|
||||
export function createEnvironmentSpecificLoggedDialog(
|
||||
environment: RuntimeEnvironment,
|
||||
dialogLoggingDecorator: DialogLoggingDecorator = ClientLoggingDecorator,
|
||||
windowInjectedDialogFactory: WindowDialogCreationFunction = () => globalThis.window.dialog,
|
||||
browserDialogFactory: BrowserDialogCreationFunction = () => new BrowserDialog(),
|
||||
): Dialog {
|
||||
const dialog = determineDialogBasedOnEnvironment(
|
||||
environment,
|
||||
windowInjectedDialogFactory,
|
||||
browserDialogFactory,
|
||||
);
|
||||
const loggingDialog = dialogLoggingDecorator(dialog);
|
||||
return loggingDialog;
|
||||
}
|
||||
|
||||
function determineDialogBasedOnEnvironment(
|
||||
environment: RuntimeEnvironment,
|
||||
windowInjectedDialogFactory: WindowDialogCreationFunction = () => globalThis.window.dialog,
|
||||
browserDialogFactory: BrowserDialogCreationFunction = () => new BrowserDialog(),
|
||||
@@ -10,16 +27,23 @@ export function determineDialogBasedOnEnvironment(
|
||||
if (!environment.isRunningAsDesktopApplication) {
|
||||
return browserDialogFactory();
|
||||
}
|
||||
const dialog = windowInjectedDialogFactory();
|
||||
if (!dialog) {
|
||||
const windowDialog = windowInjectedDialogFactory();
|
||||
if (!windowDialog) {
|
||||
throw new Error([
|
||||
'The Dialog API could not be retrieved from the window object.',
|
||||
'Failed to retrieve Dialog API from window object in desktop environment.',
|
||||
'This may indicate that the Dialog API is either not implemented or not correctly exposed in the current desktop environment.',
|
||||
].join('\n'));
|
||||
}
|
||||
return dialog;
|
||||
return windowDialog;
|
||||
}
|
||||
|
||||
export type WindowDialogCreationFunction = () => Dialog | undefined;
|
||||
|
||||
export type BrowserDialogCreationFunction = () => Dialog;
|
||||
|
||||
export type DialogLoggingDecorator = (dialog: Dialog) => Dialog;
|
||||
|
||||
const ClientLoggingDecorator: DialogLoggingDecorator = (dialog) => decorateWithLogging(
|
||||
dialog,
|
||||
ClientLoggerFactory.Current.logger,
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Dialog } from '@/presentation/common/Dialog';
|
||||
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||
import { determineDialogBasedOnEnvironment } from './ClientDialogFactory';
|
||||
import { createEnvironmentSpecificLoggedDialog } from './ClientDialogFactory';
|
||||
|
||||
export function useDialog(
|
||||
factory: DialogFactory = () => determineDialogBasedOnEnvironment(CurrentEnvironment),
|
||||
factory: DialogFactory = () => createEnvironmentSpecificLoggedDialog(CurrentEnvironment),
|
||||
) {
|
||||
const dialog = factory();
|
||||
return {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { IpcChannel } from './IpcChannel';
|
||||
|
||||
export const IpcChannelDefinitions = {
|
||||
CodeRunner: defineElectronIpcChannel<CodeRunner>('code-run', ['runCode']),
|
||||
Dialog: defineElectronIpcChannel<Dialog>('dialogs', ['saveFile']),
|
||||
Dialog: defineElectronIpcChannel<Dialog>('dialogs', ['showError', 'saveFile']),
|
||||
} as const;
|
||||
|
||||
export type ChannelDefinitionKey = keyof typeof IpcChannelDefinitions;
|
||||
|
||||
Reference in New Issue
Block a user