Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: error handling decorator for ember-concurrency tasks #386

Merged
merged 1 commit into from
Jan 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"]);
});
});
});