Skip to content

Commit

Permalink
fix: error handling decorator for ember-concurrency tasks (#386)
Browse files Browse the repository at this point in the history
  • Loading branch information
czosel authored Jan 27, 2022
1 parent f54d27a commit 63b8704
Show file tree
Hide file tree
Showing 10 changed files with 172 additions and 13 deletions.
87 changes: 87 additions & 0 deletions addon/-private/decorators.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { assert } from "@ember/debug";

const catchErrors = (context, args, exception) => {
const [
{
routeFor404,
errorMessage = "emeis.general-error",
notFoundErrorMessage = "emeis.not-found",
} = {},
] = args;
// Transition to route if 404 recieved and routeFor404 is set
if (
routeFor404 &&
exception.isAdapterError &&
exception.errors[0].status === "404"
) {
context.notification.danger(context.intl.t(notFoundErrorMessage));
context.replaceWith(routeFor404);
} else {
console.error(exception);
if (
!exception.errors ||
!exception.errors.map((e) => e.detail).filter(Boolean).length
) {
context.notification.danger(context.intl.t(errorMessage));
return;
}
exception.errors?.forEach(({ detail }) => {
context.notification.danger(detail);
});
}
};

const validate = (context) => {
assert(
"Inject the `notification` as well as the `intl` service into your route to properly display errors.",
context.notification && context.intl
);
};

// make sure that decorator can be called with or without arguments
const makeFlexibleDecorator = (decorateFn, args) => {
if (args.length === 3 && !args[0].routeFor404 && !args[0].errorMessage) {
// We can assume that the decorator was called without options
return decorateFn(...args);
}

return decorateFn;
};

export function handleModelErrors(...decoratorArgs) {
function decorate(target, name, descriptor) {
const originalDescriptor = descriptor.value;

descriptor.value = function (...args) {
validate(this);
try {
const result = originalDescriptor.apply(this, args);
return result?.then
? result.catch((exception) =>
catchErrors(this, decoratorArgs, exception)
)
: result;
} catch (exception) {
catchErrors(this, decoratorArgs, exception);
}
};
}

return makeFlexibleDecorator(decorate, decoratorArgs);
}

export function handleTaskErrors(...decoratorArgs) {
function decorate(target, property, desc) {
const gen = desc.value;

desc.value = function* (...args) {
validate(this);
try {
return yield* gen.apply(this, args);
} catch (exception) {
catchErrors(this, decoratorArgs, exception);
}
};
}
return makeFlexibleDecorator(decorate, decoratorArgs);
}
6 changes: 3 additions & 3 deletions addon/components/edit-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { inject as service } from "@ember/service";
import Component from "@glimmer/component";
import { task } from "ember-concurrency";

import handleModelErrors from "ember-emeis/decorators/handle-model-errors";
import { handleTaskErrors } from "ember-emeis/-private/decorators";

export default class EditFormComponent extends Component {
@service intl;
Expand Down Expand Up @@ -59,7 +59,7 @@ export default class EditFormComponent extends Component {
}

@task
@handleModelErrors({ errorMessage: "emeis.form.save-error" })
@handleTaskErrors({ errorMessage: "emeis.form.save-error" })
*save(event) {
event.preventDefault();

Expand All @@ -76,7 +76,7 @@ export default class EditFormComponent extends Component {
}

@task
@handleModelErrors({ errorMessage: "emeis.form.delete-error" })
@handleTaskErrors({ errorMessage: "emeis.form.delete-error" })
*delete() {
yield this.args.model.destroyRecord();
this.notification.success(this.intl.t("emeis.form.delete-success"));
Expand Down
4 changes: 2 additions & 2 deletions addon/components/relationship-select.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { inject as service } from "@ember/service";
import Component from "@glimmer/component";
import { restartableTask, lastValue, timeout } from "ember-concurrency";

import handleModelErrors from "ember-emeis/decorators/handle-model-errors";
import { handleTaskErrors } from "ember-emeis/-private/decorators";

export default class RelationshipSelectComponent extends Component {
@service notification;
Expand All @@ -16,7 +16,7 @@ export default class RelationshipSelectComponent extends Component {
}

@restartableTask
@handleModelErrors
@handleTaskErrors
*fetchModels(search) {
if (this.args.model) {
return this.args.model;
Expand Down
6 changes: 3 additions & 3 deletions addon/controllers/users/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { dropTask } from "ember-concurrency";
const ALL_ADDITIONAL_FIELDS = ["phone", "language", "address", "city", "zip"];

import PaginationController from "ember-emeis/-private/controllers/pagination";
import handleModelErrors from "ember-emeis/decorators/handle-model-errors";
import { handleTaskErrors } from "ember-emeis/-private/decorators";

export default class UsersEditController extends PaginationController {
@service intl;
Expand Down Expand Up @@ -67,7 +67,7 @@ export default class UsersEditController extends PaginationController {
}

@dropTask
@handleModelErrors({ errorMessage: "emeis.form.save-error" })
@handleTaskErrors({ errorMessage: "emeis.form.save-error" })
*createAclEntry(aclProperties) {
const aclEntry = this.store.createRecord("acl", { ...aclProperties });

Expand All @@ -89,7 +89,7 @@ export default class UsersEditController extends PaginationController {
}

@dropTask
@handleModelErrors({ errorMessage: "emeis.form.delete-error" })
@handleTaskErrors({ errorMessage: "emeis.form.delete-error" })
*deleteAclEntry(aclEntry, refreshDataTable) {
yield aclEntry.destroyRecord();
this.notification.success(this.intl.t("emeis.form.delete-success"));
Expand Down
2 changes: 1 addition & 1 deletion addon/routes/permissions/edit.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Route from "@ember/routing/route";
import { inject as service } from "@ember/service";

import handleModelErrors from "ember-emeis/decorators/handle-model-errors";
import { handleModelErrors } from "ember-emeis/-private/decorators";

export default class PermissionsEditRoute extends Route {
@service notification;
Expand Down
2 changes: 1 addition & 1 deletion addon/routes/roles/edit.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Route from "@ember/routing/route";
import { inject as service } from "@ember/service";

import handleModelErrors from "ember-emeis/decorators/handle-model-errors";
import { handleModelErrors } from "ember-emeis/-private/decorators";

export default class RolesEditRoute extends Route {
@service notification;
Expand Down
2 changes: 1 addition & 1 deletion addon/routes/scopes/edit.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Route from "@ember/routing/route";
import { inject as service } from "@ember/service";

import handleModelErrors from "ember-emeis/decorators/handle-model-errors";
import { handleModelErrors } from "ember-emeis/-private/decorators";

export default class ScopesEditRoute extends Route {
@service notification;
Expand Down
2 changes: 1 addition & 1 deletion addon/routes/users/edit.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Route from "@ember/routing/route";
import { inject as service } from "@ember/service";

import handleModelErrors from "ember-emeis/decorators/handle-model-errors";
import { handleModelErrors } from "ember-emeis/-private/decorators";

export default class UsersEditRoute extends Route {
@service notification;
Expand Down
1 change: 0 additions & 1 deletion app/decorators/handle-model-errors.js

This file was deleted.

73 changes: 73 additions & 0 deletions tests/unit/decorators-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { restartableTask } from "ember-concurrency";
import { setupTest } from "ember-qunit";
import { module, test } from "qunit";

import {
handleModelErrors,
handleTaskErrors,
} from "ember-emeis/-private/decorators";

module("Unit | decorators", function (hooks) {
setupTest(hooks);

hooks.beforeEach(function (assert) {
this.TestStub = class TestStub {
intl = {
t: () => {
return "Fehler.";
},
};

notification = {
danger: () => {
assert.step("notify-danger");
},
};

@handleModelErrors
async fetchModel(shouldThrow) {
if (shouldThrow) {
throw "nope";
}
return "yeah";
}

@restartableTask
@handleTaskErrors
*fetchModelTask(shouldThrow) {
if (shouldThrow) {
throw "nope";
}
return yield "yeah";
}
};
});

module("handle-model-errors", function () {
test("doesnt catch successes", async function (assert) {
const instance = new this.TestStub();
await instance.fetchModel(false);
assert.verifySteps([]);
});

test("catches 404 error", async function (assert) {
const instance = new this.TestStub();
await instance.fetchModel(true);
assert.verifySteps(["notify-danger"]);
});
});

module("handle-task-errors", function () {
test("doesnt catch successes", async function (assert) {
const instance = new this.TestStub();
await instance.fetchModelTask.perform(false);
assert.verifySteps([]);
});

test("catches 404 error", async function (assert) {
const instance = new this.TestStub();
await instance.fetchModelTask.perform(true);
assert.verifySteps(["notify-danger"]);
});
});
});

0 comments on commit 63b8704

Please sign in to comment.