* Added error codes to known errors

* Restructured errors for localization

* Added localized error messages for errors and messages

* Created error handler component to display application errors

* Added unit tests for error handler component

* Added unit tests for error handler component

* Updated setting AppError properties to resolve weird unit test failure

* Addressed PR feedback
This commit is contained in:
Wallace Breza 2019-01-25 14:08:58 -08:00 коммит произвёл GitHub
Родитель ed98ef2ce3
Коммит 6d97cef60e
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
18 изменённых файлов: 480 добавлений и 232 удалений

Просмотреть файл

@ -5,13 +5,12 @@ import { ToastContainer } from "react-toastify";
import Navbar from "./react/components/shell/navbar";
import Sidebar from "./react/components/shell/sidebar";
import MainContentRouter from "./react/components/shell/mainContentRouter";
import { IAppError, IApplicationState, IProject } from "./models/applicationState";
import { IAppError, IApplicationState, IProject, AppError, ErrorCode } from "./models/applicationState";
import "./App.scss";
import "react-toastify/dist/ReactToastify.css";
import ErrorBoundary from "./react/components/common/errorBoundary";
import IAppErrorActions, * as appErrorActions from "./redux/actions/appErrorActions";
import { bindActionCreators } from "redux";
import Alert from "./react/components/common/alert/alert";
import { ErrorHandler } from "./react/components/common/errorHandler/errorHandler";
interface IAppProps {
currentProject?: IProject;
@ -37,7 +36,7 @@ function mapDispatchToProps(dispatch) {
* @description - Root level component for VoTT Application
*/
@connect(mapStateToProps, mapDispatchToProps)
class App extends React.Component<IAppProps> {
export default class App extends React.Component<IAppProps> {
constructor(props, context) {
super(props, context);
@ -46,20 +45,23 @@ class App extends React.Component<IAppProps> {
};
}
public render() {
const showError = (this.props.appError !== null);
const errorTitle = showError ? this.props.appError.title : "";
const errorMessage = showError ? this.props.appError.message : "";
public componentDidCatch(error: Error) {
this.props.actions.showError({
errorCode: ErrorCode.GenericRenderError,
title: error.name,
message: error.message,
});
}
public render() {
return (
<Fragment>
<Alert title={errorTitle}
message={errorMessage}
closeButtonColor="info"
show={showError}
onClose={this.props.actions.clearError}
/>
<ErrorBoundary>
<ErrorHandler
error={this.props.appError}
onError={this.props.actions.showError}
onClearError={this.props.actions.clearError} />
{/* Don't render app contents during a render error */}
{(!this.props.appError || this.props.appError.errorCode !== ErrorCode.GenericRenderError) &&
<Router>
<div className="app-shell">
<Navbar />
@ -70,10 +72,8 @@ class App extends React.Component<IAppProps> {
<ToastContainer />
</div>
</Router >
</ErrorBoundary >
}
</Fragment>
);
}
}
export default App;

Просмотреть файл

@ -28,10 +28,6 @@ export const english: IAppStrings = {
title: "Delete Project",
confirmation: "Are you sure you want to delete project",
},
loadProjectError: {
title: "Project Loading has an error",
message: "File is not valid json",
},
},
appSettings: {
title: "Application Settings",
@ -58,6 +54,9 @@ export const english: IAppStrings = {
description: "Reload the app discarding all current changes",
button: "Refresh Application",
},
messages: {
saveSuccess: "Successfully saved application settings",
},
},
projectSettings: {
title: "Project Settings",
@ -80,6 +79,9 @@ export const english: IAppStrings = {
frameExtractionRate: "Frame Extraction Rate (frames per a video second)",
},
addConnection: "Add Connection",
messages: {
saveSuccess: "Successfully saved ${project.name} project settings",
},
},
tags: {
title: "Tags",
@ -160,6 +162,9 @@ export const english: IAppStrings = {
tfRecords: "Tensorflow Records",
tfPascalVoc: "Tensorflow Pascal VOC",
},
messages: {
saveSuccess: "Successfully saved export settings",
},
},
activeLearning: {
title: "Active Learning",
@ -167,4 +172,33 @@ export const english: IAppStrings = {
profile: {
settings: "Profile Settings",
},
errors: {
unknown: {
title: "Unknown Error",
message: "The app encounted an unknown error. Please try again.",
},
projectUploadError: {
title: "Error Uploading File",
message: `There was an error uploading the file.
Please verify the file is of the correct format and try again.`,
},
genericRenderError: {
title: "Error Loading Application",
message: "An error occured while rendering the application. Please try again",
},
projectInvalidSecurityToken: {
title: "Error loading project file",
message: `The security token referenced by the project is invalid.
Verify that the security token for the project has been set correctly within your application settings`,
},
projectInvalidJson: {
title: "Error parsing project file",
message: "The selected project files does not contain valid JSON. Please check the file any try again.",
},
securityTokenNotFound: {
title: "Error loading project file",
message: `The security token referenced by the project cannot be found in your current application settings.
Verify the security token exists and try to reload the project.`,
},
},
};

Просмотреть файл

@ -28,10 +28,6 @@ export const spanish: IAppStrings = {
title: "Borrar Proyecto",
confirmation: "¿Está seguro que quiere borrar el proyecto",
},
loadProjectError: {
title: "Proyecto de carga tiene un error",
message: "El archivo no es válido json",
},
},
appSettings: {
title: "Configuración de Aplicación",
@ -59,6 +55,9 @@ export const spanish: IAppStrings = {
description: "Recargar la aplicación descartando todos los cambios actuales",
button: "Recargar la aplicación",
},
messages: {
saveSuccess: "Configuración de la aplicación guardada correctamente",
},
},
projectSettings: {
title: "Configuración de Proyecto",
@ -81,6 +80,9 @@ export const spanish: IAppStrings = {
frameExtractionRate: "Tasa de extracción de cuadros (cuadros por segundo de video)",
},
addConnection: "Agregar Conexión",
messages: {
saveSuccess: "Guardado correctamente ${project.name} configuración del proyecto",
},
},
tags: {
title: "Etiquetas",
@ -161,6 +163,9 @@ export const spanish: IAppStrings = {
tfRecords: "Registros de Tensorflow",
tfPascalVoc: "Tensorflow Pascal VOC",
},
messages: {
saveSuccess: "Configuración de exportación guardada correctamente",
},
},
activeLearning: {
title: "Aprendizaje Activo",
@ -168,4 +173,33 @@ export const spanish: IAppStrings = {
profile: {
settings: "Configuración de Perfíl",
},
errors: {
unknown: {
title: "Error desconocido",
message: "La aplicación contó un error desconocido. Por favor inténtalo de nuevo.",
},
projectUploadError: {
title: "Error al cargar el archivo",
message: `Se ha cargado un error al cargar el archivo.
Compruebe que el archivo es del tipo correcto e inténtelo de nuevo.`,
},
genericRenderError: {
title: "Error desconocido",
message: "La aplicación contó un error desconocido. Por favor inténtalo de nuevo.",
},
projectInvalidSecurityToken: {
title: "Error al cargar el archivo de proyecto",
message: "Asegúrese de que el token de seguridad del proyecto existe",
},
projectInvalidJson: {
title: "Error al analizar el archivo de proyecto",
message: "El archivo de proyecto no es válido JSON",
},
securityTokenNotFound: {
title: "Error loading project file",
message: `El token de seguridad al que hace referencia el proyecto no se encuentra en la
configuración de la aplicación actual. Compruebe que existe el token de seguridad e intente
volver a cargar el proyecto.`,
},
},
};

Просмотреть файл

@ -2,7 +2,7 @@ import shortid from "shortid";
import {
AssetState, AssetType, IApplicationState, IAppSettings, IAsset, IAssetMetadata,
IConnection, IExportFormat, IProject, ITag, StorageType, ISecurityToken,
IAppError, IProjectVideoSettings, AppErrorType, EditorMode,
EditorMode, IAppError, IProjectVideoSettings, AppError, ErrorCode,
} from "../models/applicationState";
import { ExportAssetState } from "../providers/export/exportProvider";
import { IAssetProvider, IAssetProviderRegistrationOptions } from "../providers/storage/assetProviderFactory";
@ -27,20 +27,19 @@ export default class MockFactory {
/**
* Creates sample IAppError
* @param {string} title to be display in Alert
* @param {string} message to be display in body of Alert
* @param {string} errorType to specify whether this is a render error or generic exception thrown
* @param errorCode The error code to map to the error
* @param title The title of the error
* @param message The detailed error message
* @returns {IAppError}
*/
public static createAppError(
errorCode: ErrorCode = ErrorCode.Unknown,
title: string = "",
message: string = "",
errorType: string = AppErrorType.Generic,
): IAppError {
message: string = ""): IAppError {
return {
errorCode,
title,
message,
errorType,
};
}

Просмотреть файл

@ -21,20 +21,16 @@ export interface IAppStrings {
newProject: string;
openLocalProject: {
title: string;
}
},
openCloudProject: {
title: string;
selectConnection: string;
}
},
deleteProject: {
title: string;
confirmation: string;
}
recentProjects: string;
loadProjectError: {
title: string;
message: string;
}
},
recentProjects: string,
};
appSettings: {
title: string;
@ -60,7 +56,10 @@ export interface IAppStrings {
reload: {
description: string;
button: string;
}
},
messages: {
saveSuccess: string;
},
};
projectSettings: {
title: string;
@ -82,7 +81,10 @@ export interface IAppStrings {
description: string;
frameExtractionRate: string;
},
addConnection: string;
addConnection: string,
messages: {
saveSuccess: string;
},
};
tags: {
title: string;
@ -163,6 +165,9 @@ export interface IAppStrings {
tfRecords: string;
tfPascalVoc: string;
},
messages: {
saveSuccess: string;
},
};
activeLearning: {
title: string;
@ -170,6 +175,19 @@ export interface IAppStrings {
profile: {
settings: string;
};
errors: {
unknown: IErrorMetadata,
projectInvalidJson: IErrorMetadata,
projectInvalidSecurityToken: IErrorMetadata,
projectUploadError: IErrorMetadata,
genericRenderError: IErrorMetadata,
securityTokenNotFound: IErrorMetadata,
};
}
interface IErrorMetadata {
title: string;
message: string;
}
interface IStrings extends LocalizedStringsMethods, IAppStrings { }

Просмотреть файл

@ -25,19 +25,39 @@ export interface IApplicationState {
* @member errorCode - error category
*/
export interface IAppError {
title?: string;
errorCode: ErrorCode;
message: string;
errorCode?: string;
errorType: string;
title?: string;
}
/**
* @enum Generic - describe an error happened in an eventhandler, async function etc.
* @enum Render - describe an error happened inside render
* Enum of supported error codes
*/
export enum AppErrorType {
Generic = "generic",
Render = "render",
export enum ErrorCode {
// Note that the value of the enum is in camelCase while
// the enum key is in Pascal casing
Unknown = "unknown",
GenericRenderError = "genericRenderError",
ProjectInvalidJson = "projectInvalidJson",
ProjectInvalidSecurityToken = "projectInvalidSecurityToken",
ProjectUploadError = "projectUploadError",
SecurityTokenNotFound = "securityTokenNotFound",
}
/**
* Base application error
*/
export class AppError extends Error implements IAppError {
public errorCode: ErrorCode;
public message: string;
public title?: string;
constructor(errorCode: ErrorCode, message: string, title: string = null) {
super(message);
this.errorCode = errorCode;
this.message = message;
this.title = title;
}
}
/**

Просмотреть файл

@ -1,70 +0,0 @@
import React from "react";
import ErrorBoundary, { IErrorBoundaryProps } from "./errorBoundary";
import { mount, ReactWrapper } from "enzyme";
import { Provider } from "react-redux";
import { IApplicationState } from "../../../models/applicationState";
import IAppErrorActions, * as appErrorActions from "../../../redux/actions/appErrorActions";
import { AnyAction, Store } from "redux";
import createReduxStore from "../../../redux/store/store";
const ERROR_MSG = "Uncaught Exception from Child Component";
class ChildComponent extends React.Component {
public render() {
return <div />;
}
}
describe("ErrorBoundary Component", () => {
function createStore(state?: IApplicationState): Store<any, AnyAction> {
return createReduxStore(state);
}
function createComponent(store, props: IErrorBoundaryProps): ReactWrapper {
return mount(
<Provider store={store}>
<ErrorBoundary{...props}>
<ChildComponent />
</ErrorBoundary>
</Provider>,
);
}
function createProps(): IErrorBoundaryProps {
return {
actions: (appErrorActions as any) as IAppErrorActions,
};
}
it("call showError action when child component throws error", () => {
const props = createProps();
const showError = jest.spyOn(props.actions, "showError");
const wrapper = createComponent(createStore(), props);
const error = new Error(ERROR_MSG);
wrapper.find(ChildComponent).simulateError(error);
expect(showError).toBeCalledWith({ title: "Error", message: ERROR_MSG, errorType: "render" });
});
it("does not render anything when there's an error", () => {
const props = createProps();
const wrapper = createComponent(createStore(), props);
const error = new Error(ERROR_MSG);
wrapper.find(ChildComponent).simulateError(error);
const childWrapper = wrapper.find(ChildComponent);
expect(childWrapper.exists()).toBeFalsy();
});
it("render child component when there's no error", () => {
const props = createProps();
const wrapper = createComponent(createStore(), props);
const childWrapper = wrapper.find(ChildComponent);
expect(childWrapper.exists()).toBeTruthy();
});
});

Просмотреть файл

@ -1,53 +0,0 @@
import React, { ErrorInfo } from "react";
import { connect } from "react-redux";
import IAppErrorActions from "../../../redux/actions/appErrorActions";
import { bindActionCreators } from "redux";
import * as appErrorActions from "../../../redux/actions/appErrorActions";
import {
IAppError,
IApplicationState,
AppErrorType,
} from "../../../models/applicationState";
export interface IErrorBoundaryProps {
appError?: IAppError;
actions?: IAppErrorActions;
}
function mapStateToProps(state: IApplicationState) {
return {
appError: state.appError,
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(appErrorActions, dispatch),
};
}
@connect(mapStateToProps, mapDispatchToProps)
export default class ErrorBoundary extends React.Component<IErrorBoundaryProps> {
constructor(props) {
super(props);
}
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
this.props.actions.showError({
title: error.name,
message: error.message,
errorType: AppErrorType.Render,
});
}
public render() {
if (
this.props.appError &&
this.props.appError.errorType === AppErrorType.Render
) {
return null;
}
return this.props.children;
}
}

Просмотреть файл

@ -0,0 +1,155 @@
import React from "react";
import { IErrorHandlerProps, ErrorHandler } from "./errorHandler";
import { mount, ReactWrapper } from "enzyme";
import Alert from "../alert/alert";
import { ErrorCode, IAppError, AppError } from "../../../../models/applicationState";
import { strings } from "../../../../common/strings";
describe("Error Handler Component", () => {
const onErrorHandler = jest.fn();
const onClearErrorHandler = jest.fn();
const defaultProps: IErrorHandlerProps = {
error: null,
onError: onErrorHandler,
onClearError: onClearErrorHandler,
};
function createComponent(props: IErrorHandlerProps = null): ReactWrapper<IErrorHandlerProps> {
props = props || defaultProps;
return mount(<ErrorHandler {...props} />);
}
it("does not render an alert when error property is not set", () => {
const wrapper = createComponent();
expect(wrapper.find(Alert).exists()).toBe(false);
});
it("renders an alert when error property is set", () => {
const props: IErrorHandlerProps = {
...defaultProps,
error: {
errorCode: ErrorCode.Unknown,
message: "error message",
title: "error title",
},
};
const wrapper = createComponent(props);
const alert = wrapper.find(Alert);
expect(alert.exists()).toBe(true);
expect(alert.prop("title")).toEqual(strings.errors.unknown.title);
expect(alert.prop("message")).toEqual(strings.errors.unknown.message);
});
it("renders an alert with localized error messages", () => {
const props: IErrorHandlerProps = {
...defaultProps,
error: {
errorCode: ErrorCode.ProjectInvalidJson,
message: "JSON is messed up",
title: "JSON error",
},
};
const wrapper = createComponent(props);
const alert = wrapper.find(Alert);
expect(alert.exists()).toBe(true);
expect(alert.prop("title")).toEqual(strings.errors.projectInvalidJson.title);
expect(alert.prop("message")).toEqual(strings.errors.projectInvalidJson.message);
});
it("calls onError when window Error is thrown", () => {
const thrownError = new Error("New generic error message");
const errorEvent = new ErrorEvent("error", {
message: thrownError.message,
lineno: 100,
colno: 1,
error: thrownError,
});
const expectedError: IAppError = {
errorCode: ErrorCode.Unknown,
title: thrownError.name,
message: thrownError.message,
};
createComponent();
document.dispatchEvent(errorEvent);
expect(onErrorHandler).toBeCalledWith(expectedError);
});
it("calls onError when window AppError is thrown", () => {
const thrownError = new AppError(ErrorCode.ProjectInvalidJson, "message", "title");
const errorEvent = new ErrorEvent("error", {
message: thrownError.message,
lineno: 100,
colno: 1,
error: thrownError,
});
const expectedError: IAppError = {
errorCode: ErrorCode.ProjectInvalidJson,
title: thrownError.title,
message: thrownError.message,
};
createComponent();
document.dispatchEvent(errorEvent);
expect(onErrorHandler).toBeCalledWith(expectedError);
});
it("calls onError when unhandled rejection throws Error", () => {
const thrownError = new Error("New generic error message");
const errorEvent = new CustomEvent("unhandledrejection", {
detail: thrownError,
});
const expectedError: IAppError = {
errorCode: ErrorCode.Unknown,
title: thrownError.name,
message: thrownError.message,
};
createComponent();
document.dispatchEvent(errorEvent);
expect(onErrorHandler).toBeCalledWith(expectedError);
});
it("calls onError when unhandled rejection throws AppError", () => {
const thrownError = new AppError(ErrorCode.ProjectInvalidJson, "message", "title");
const errorEvent = new CustomEvent("unhandledrejection", {
detail: thrownError,
});
const expectedError: IAppError = {
errorCode: ErrorCode.ProjectInvalidJson,
title: thrownError.title,
message: thrownError.message,
};
createComponent();
document.dispatchEvent(errorEvent);
expect(onErrorHandler).toBeCalledWith(expectedError);
});
it("calls onError when unhandled rejection resolves with string reason", () => {
const thrownError = "message as string";
const errorEvent = new CustomEvent("unhandledrejection", {
detail: thrownError,
});
const expectedError: IAppError = {
errorCode: ErrorCode.Unknown,
message: thrownError,
};
createComponent();
document.dispatchEvent(errorEvent);
expect(onErrorHandler).toBeCalledWith(expectedError);
});
});

Просмотреть файл

@ -0,0 +1,130 @@
import React from "react";
import { IAppError, ErrorCode, AppError } from "../../../../models/applicationState";
import { strings } from "../../../../common/strings";
import Alert from "../alert/alert";
import { instanceOf } from "prop-types";
/**
* Component properties for ErrorHandler component
*/
export interface IErrorHandlerProps extends React.Props<ErrorHandler> {
error: IAppError;
onError: (error: IAppError) => void;
onClearError: () => void;
}
/**
* Component for catching and handling global application errors
*/
export class ErrorHandler extends React.Component<IErrorHandlerProps> {
constructor(props, context) {
super(props, context);
this.onWindowError = this.onWindowError.bind(this);
this.onUnhandedRejection = this.onUnhandedRejection.bind(this);
}
public componentDidMount() {
window.addEventListener("error", this.onWindowError, true);
window.addEventListener("unhandledrejection", this.onUnhandedRejection, true);
}
public componentWillMount() {
window.removeEventListener("error", this.onWindowError);
window.removeEventListener("unhandledrejection", this.onUnhandedRejection);
}
public render() {
const showError = !!this.props.error;
let localizedError: IAppError = null;
if (showError) {
localizedError = this.getLocalizedError(this.props.error);
}
if (!showError) {
return null;
}
return (
<Alert title={localizedError ? localizedError.title : ""}
message={localizedError ? localizedError.message : ""}
closeButtonColor="secondary"
show={showError}
onClose={this.props.onClearError} />
);
}
/**
* Unhandled errors that bubbled up to top of stack
* @param evt Error Event
*/
private onWindowError(evt: ErrorEvent) {
this.handleError(evt.error);
evt.preventDefault();
}
/**
* Handles async / promise based errors
* @param evt Unhandled Rejection Event
*/
private onUnhandedRejection(evt: any) {
this.handleError(evt.reason || evt.detail);
evt.preventDefault();
}
/**
* Handles various error format scenarios
* @param error The error to handle
*/
private handleError(error: string | Error | AppError) {
let appError: IAppError = null;
// Promise rejection with reason
if (typeof (error) === "string") {
// Promise rejection with string base reason
appError = {
errorCode: ErrorCode.Unknown,
message: error,
};
} else if (error instanceof AppError) {
// Promise rejection with AppError
const reason = error as IAppError;
appError = {
errorCode: reason.errorCode,
message: reason.message,
title: reason.title,
};
} else if (error instanceof Error) {
// Promise rejection with other error like object
const reason = error as Error;
appError = {
errorCode: ErrorCode.Unknown,
message: reason.message,
title: reason.name,
};
}
if (!appError) {
appError = new AppError(ErrorCode.Unknown, "Unknown Error occurred");
}
this.props.onError(appError);
}
/**
* Gets a localized version of the error
* @param appError The error thrown by the application
*/
private getLocalizedError(appError: IAppError): IAppError {
const localizedError = strings.errors[appError.errorCode];
if (!localizedError) {
return appError;
}
return {
errorCode: appError.errorCode,
message: localizedError.message || strings.errors.unknown.message,
title: localizedError.title || strings.errors.unknown.title,
};
}
}

Просмотреть файл

@ -79,7 +79,7 @@ export default class AppSettingsPage extends React.Component<IAppSettingsProps>
private async onFormSubmit(appSettings: IAppSettings) {
await this.props.actions.saveAppSettings(appSettings);
toast.success(`Successfully saved application settings`);
toast.success(strings.appSettings.messages.saveSuccess);
this.props.history.goBack();
}

Просмотреть файл

@ -7,6 +7,7 @@ import ExportForm from "./exportForm";
import { IProject, IApplicationState, IExportFormat } from "../../../../models/applicationState";
import { strings } from "../../../../common/strings";
import { ExportAssetState } from "../../../../providers/export/exportProvider";
import { toast } from "react-toastify";
/**
* Properties for Export Page
@ -89,6 +90,7 @@ export default class ExportPage extends React.Component<IExportPageProps> {
};
await this.props.actions.saveProject(projectToUpdate);
toast.success(strings.export.messages.saveSuccess);
this.props.history.goBack();
}

Просмотреть файл

@ -4,14 +4,13 @@ import { Provider } from "react-redux";
import { BrowserRouter as Router, Link } from "react-router-dom";
import { AnyAction, Store } from "redux";
import MockFactory from "../../../../common/mockFactory";
import { IApplicationState, IProject, AppErrorType } from "../../../../models/applicationState";
import { IApplicationState, IProject } from "../../../../models/applicationState";
import IProjectActions, * as projectActions from "../../../../redux/actions/projectActions";
import createReduxStore from "../../../../redux/store/store";
import ProjectService from "../../../../services/projectService";
import CondensedList from "../../common/condensedList/condensedList";
import FilePicker from "../../common/filePicker/filePicker";
import HomePage, { IHomepageProps } from "./homePage";
import IAppErrorActions, * as appErrorActions from "../../../../redux/actions/appErrorActions";
jest.mock("../../../../services/projectService");
@ -112,32 +111,6 @@ describe("Connection Picker Component", () => {
expect(openProjectSpy).toBeCalledWith(testProject);
});
it("should call showError action when passed an invalid json project", async () => {
// refactoring warning: action spy have to be created before creating component
const showErrorSpy = jest.spyOn(props.appErrorActions, "showError");
const wrapper = createComponent(store, props);
const textBlob = new Blob(["foo"], { type: "text/plain" });
const fileUpload = wrapper.find("a.file-upload").first();
const fileInput = wrapper.find(`input[type="file"]`);
fileUpload.simulate("click");
await MockFactory.flushUi(() => {
fileInput.simulate("change", ({ target: { files: [textBlob] } }));
});
await MockFactory.flushUi();
const expectedAppError = {
title: "Project Loading has an error",
message: "File is not valid json",
errorType: AppErrorType.Generic,
};
expect(showErrorSpy).toBeCalledWith(expectedAppError);
});
function createProps(): IHomepageProps {
return {
recentProjects: [],
@ -162,7 +135,6 @@ describe("Connection Picker Component", () => {
state: null,
},
actions: (projectActions as any) as IProjectActions,
appErrorActions: (appErrorActions as any) as IAppErrorActions,
match: {
params: {},
isExact: true,

Просмотреть файл

@ -3,7 +3,6 @@ import { connect } from "react-redux";
import { Link, RouteComponentProps } from "react-router-dom";
import { bindActionCreators } from "redux";
import { strings } from "../../../../common/strings";
import { IApplicationState, IConnection, IProject, AppErrorType } from "../../../../models/applicationState";
import IProjectActions, * as projectActions from "../../../../redux/actions/projectActions";
import { CloudFilePicker } from "../../common/cloudFilePicker/cloudFilePicker";
import CondensedList from "../../common/condensedList/condensedList";
@ -12,13 +11,15 @@ import FilePicker from "../../common/filePicker/filePicker";
import "./homePage.scss";
import RecentProjectItem from "./recentProjectItem";
import { constants } from "../../../../common/constants";
import IAppErrorActions, * as appErrorActions from "../../../../redux/actions/appErrorActions";
import {
IApplicationState, IConnection, IProject,
ErrorCode, AppError, IAppError,
} from "../../../../models/applicationState";
export interface IHomepageProps extends RouteComponentProps, React.Props<HomePage> {
recentProjects: IProject[];
connections: IConnection[];
actions: IProjectActions;
appErrorActions: IAppErrorActions;
}
function mapStateToProps(state: IApplicationState) {
@ -31,7 +32,6 @@ function mapStateToProps(state: IApplicationState) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(projectActions, dispatch),
appErrorActions: bindActionCreators(appErrorActions, dispatch),
};
}
@ -120,20 +120,22 @@ export default class HomePage extends React.Component<IHomepageProps> {
private onProjectFileUpload = async (e, project) => {
let projectJson: IProject;
try {
projectJson = JSON.parse(project);
await this.loadSelectedProject(projectJson);
} catch (error) {
this.props.appErrorActions.showError({
title: strings.homePage.loadProjectError.title,
message: strings.homePage.loadProjectError.message,
errorType: AppErrorType.Generic,
});
throw new AppError(ErrorCode.ProjectInvalidJson, "Error parsing JSON");
}
await this.loadSelectedProject(projectJson);
}
private onProjectFileUploadError = (e, err) => {
console.error(err);
private onProjectFileUploadError = (e, error: any) => {
if (error instanceof AppError) {
throw error;
}
throw new AppError(ErrorCode.ProjectUploadError, "Error uploading project file");
}
private loadSelectedProject = async (project: IProject) => {
@ -144,5 +146,4 @@ export default class HomePage extends React.Component<IHomepageProps> {
private deleteProject = async (project: IProject) => {
await this.props.actions.deleteProject(project);
}
}

Просмотреть файл

@ -3,11 +3,12 @@ import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import { RouteComponentProps } from "react-router-dom";
import ProjectForm from "./projectForm";
import { strings } from "../../../../common/strings";
import { strings, interpolate } from "../../../../common/strings";
import IProjectActions, * as projectActions from "../../../../redux/actions/projectActions";
import { IApplicationState, IProject, IConnection, IAppSettings } from "../../../../models/applicationState";
import IApplicationActions, * as applicationActions from "../../../../redux/actions/applicationActions";
import { generateKey } from "../../../../common/crypto";
import { toast } from "react-toastify";
/**
* Properties for Project Settings Page
@ -87,6 +88,8 @@ export default class ProjectSettingsPage extends React.Component<IProjectSetting
await this.ensureSecurityToken(project);
await this.props.projectActions.saveProject(project);
toast.success(interpolate(strings.projectSettings.messages.saveSuccess, { project }));
if (isNew) {
this.props.history.push(`/projects/${this.props.project.id}/edit`);
} else {

Просмотреть файл

@ -1,16 +1,14 @@
import { Dispatch, Action } from "redux";
import ProjectService from "../../services/projectService";
import {
IProject, IAsset, IAssetMetadata, IApplicationState,
ISecurityToken, IAppSettings,
} from "../../models/applicationState";
import { ActionTypes } from "./actionTypes";
import { AssetService } from "../../services/assetService";
import { ExportProviderFactory } from "../../providers/export/exportProviderFactory";
import {
IProject, IAsset, IAssetMetadata, IApplicationState,
ErrorCode, AppError,
} from "../../models/applicationState";
import { createPayloadAction, IPayloadAction, createAction } from "./actionCreators";
import { IExportResults } from "../../providers/export/exportProvider";
import { generateKey } from "../../common/crypto";
import { saveAppSettingsAction } from "./applicationActions";
/**
* Actions to be performed in relation to projects
@ -41,7 +39,7 @@ export function loadProject(project: IProject):
.find((st) => st.name === project.securityToken);
if (!securityToken) {
throw new Error(`Cannot locate security token '${project.securityToken}' from project`);
throw new AppError(ErrorCode.SecurityTokenNotFound, "Security Token Not Found");
}
const loadedProject = await projectService.load(project, securityToken);
@ -69,7 +67,7 @@ export function saveProject(project: IProject):
.find((st) => st.name === project.securityToken);
if (!securityToken) {
throw new Error(`Cannot locate security token '${project.securityToken}' from project`);
throw new AppError(ErrorCode.SecurityTokenNotFound, "Security Token Not Found");
}
const savedProject = await projectService.save(project, securityToken);

Просмотреть файл

@ -1,5 +1,5 @@
import { reducer } from "./appErrorReducer";
import { IAppError } from "../../models/applicationState";
import { IAppError, ErrorCode } from "../../models/applicationState";
import { clearErrorAction, showErrorAction } from "../actions/appErrorActions";
import { anyOtherAction } from "../actions/actionCreators";
import MockFactory from "../../common/mockFactory";
@ -13,6 +13,7 @@ describe("AppError Reducer", () => {
it("ShowError discard previous state and return an appError", () => {
const appError = MockFactory.createAppError(
ErrorCode.Unknown,
"Sample Error Title",
"Sample Error Message",
);

Просмотреть файл

@ -1,6 +1,6 @@
import shortid from "shortid";
import { StorageProviderFactory } from "../providers/storage/storageProviderFactory";
import { IProject, ISecurityToken } from "../models/applicationState";
import { IProject, ISecurityToken, AppError, ErrorCode } from "../models/applicationState";
import Guard from "../common/guard";
import { constants } from "../common/constants";
import { ExportProviderFactory } from "../providers/export/exportProviderFactory";
@ -31,9 +31,13 @@ export default class ProjectService implements IProjectService {
public load(project: IProject, securityToken: ISecurityToken): Promise<IProject> {
Guard.null(project);
const loadedProject = decryptProject(project, securityToken);
return Promise.resolve(loadedProject);
try {
const loadedProject = decryptProject(project, securityToken);
return Promise.resolve(loadedProject);
} catch (e) {
const error = new AppError(ErrorCode.ProjectInvalidSecurityToken, "Error decrypting project settings");
return Promise.reject(error);
}
}
/**