Skip to content

Commit

Permalink
feat: adds edit and delete functionality to userlist (#399)
Browse files Browse the repository at this point in the history
* feat: only proceed confirmed deletions in EditForm

* feat: add edit and delete actions to userlist

* test: add coverage for confirm-task decorator

* chore: add missing translations

* test: fix tests where confirm-task decorator in passive use
  • Loading branch information
derrabauke authored Feb 3, 2022
1 parent b513c73 commit 69d3c17
Show file tree
Hide file tree
Showing 12 changed files with 224 additions and 3 deletions.
3 changes: 3 additions & 0 deletions addon/components/edit-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { inject as service } from "@ember/service";
import Component from "@glimmer/component";
import { task } from "ember-concurrency";

import { confirmTask } from "../decorators/confirm-task";

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

export default class EditFormComponent extends Component {
Expand Down Expand Up @@ -76,6 +78,7 @@ export default class EditFormComponent extends Component {
}

@task
@confirmTask({ message: "emeis.form.confirmEntryDelete" })
@handleTaskErrors({ errorMessage: "emeis.form.delete-error" })
*delete() {
yield this.args.model.destroyRecord();
Expand Down
14 changes: 14 additions & 0 deletions addon/controllers/users/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { inject as service } from "@ember/service";
import { task } from "ember-concurrency";

import { confirmTask } from "../../decorators/confirm-task";

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

export default class UsersIndexController extends PaginationController {
@service emeisOptions;
@service notification;
@service intl;

get emailAsUsername() {
return this.emeisOptions.emailAsUsername;
Expand All @@ -16,4 +22,12 @@ export default class UsersIndexController extends PaginationController {
get linkToScope() {
return this.emeisOptions.navigationEntries?.includes("scopes");
}

@task
@confirmTask({ message: "emeis.form.confirmUserDelete" })
@handleTaskErrors({ errorMessage: "emeis.form.delete-error" })
*delete(model) {
yield model.destroyRecord();
this.notification.success(this.intl.t("emeis.form.delete-success"));
}
}
112 changes: 112 additions & 0 deletions addon/decorators/confirm-task.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { assert } from "@ember/debug";
import UIkit from "uikit";

import ENV from "ember-emeis/config/environment";

/**
* This decorator makes heavy use of the UIKit modal. If anything breaks in future releases, it could be
* caused by the use of non-public API in this decorator.
*
* For further details have a look at: https://github.com/uikit/uikit/blob/develop/src/js/core/modal.js
*/

const LABELS = ["message", "ok", "cancel"];

async function confirm(text, options) {
try {
await UIkit.modal.confirm(text, options);
return true;
} catch (error) {
return false;
}
}

function initOptions(args) {
if (args.length === 1) {
assert(
"You should pass the confirm-task options as an object looking like this: { message: 'emeis.form.deleteMessage', cancel: 'emeis.form.back' } ",
typeof args[0] === "object"
);
return args[0];
}
return {};
}

function validateIntl(context) {
assert(
"Inject the `intl` service into your component to properly translate the modal dialog.",
context.intl
);
}

function translateOptions(context, options) {
const translated = {};
for (const key in options) {
if (LABELS.includes(key) && context.intl.exists(options[key])) {
translated[key] = context.intl.t(options[key]);
}
}
return {
labels: {
// set some defaults
message: context.intl.t("emeis.form.confirmText"),
ok: context.intl.t("emeis.form.ok"),
cancel: context.intl.t("emeis.form.cancel"),
// add the override with the transmitted options
...translated,
},
};
}

function filterOptions(options) {
const filteredOptions = Object.fromEntries(
Object.entries(options).filter(([key]) => !LABELS.includes(key))
);

if (ENV.environment === "test") {
filteredOptions.container = "#ember-testing";
}
return filteredOptions;
}

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

return decorateFn;
};

export function confirmTask(...decoratorArgs) {
const options = initOptions(decoratorArgs);

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

desc.value = function* (...args) {
const event = args.find((arg) => arg instanceof Event);

if (event) {
event.preventDefault();
}

validateIntl(this);
const translatedOptions = translateOptions(this, options);
// append other options which are not confirm-labels to the modal object
// that way you can modify the modal with further options like "container"
const filteredOptions = filterOptions(options);
if (
!(yield confirm(translatedOptions.labels.message, {
...translatedOptions,
...filteredOptions,
}))
) {
return;
}
return yield* gen.apply(this, args);
};
}
return makeFlexibleDecorator(decorate, decoratorArgs);
}
10 changes: 10 additions & 0 deletions addon/templates/users/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
<Column>
{{t "emeis.scopes.title"}}
</Column>
<Column>
</Column>
</table.head>
<table.body as |body|>
<body.row>
Expand Down Expand Up @@ -89,6 +91,14 @@
{{/each}}
</ul>
</td>
<td class="uk-text-right small-padding-right">
<LinkTo @route="users.edit" @model={{user}} class="uk-link-reset uk-margin-small-right">
<UkIcon @icon="pencil"/>
</LinkTo>
<UkButton @color="link" {{on "click" (perform this.delete user)}}>
<UkIcon @icon="trash" />
</UkButton>
</td>
{{/let}}
</body.row>
</table.body>
Expand Down
4 changes: 4 additions & 0 deletions app/styles/ember-emeis.scss
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ div.uk-grid-divider-fix.uk-grid-stack > .uk-grid-margin::before {
}
}

.uk-table td.small-padding-right {
padding-right: 1.75em;
}

.sortable-th {
cursor: pointer;

Expand Down
4 changes: 4 additions & 0 deletions tests/acceptance/permissions-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
currentURL,
fillIn,
click,
waitFor,
waitUntil,
settled,
} from "@ember/test-helpers";
Expand Down Expand Up @@ -151,6 +152,9 @@ module("Acceptance | permissions", function (hooks) {
});
await click("[data-test-delete]");

await waitFor(".uk-modal.uk-open");
await click(".uk-modal .uk-button-primary");

// For some reason the await click is not actually waiting for the delete task to finish.
// Probably some runloop issue.
await waitUntil(() => currentURL() !== `/permissions/${permission.id}`);
Expand Down
3 changes: 3 additions & 0 deletions tests/acceptance/roles-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
click,
fillIn,
findAll,
waitFor,
waitUntil,
settled,
} from "@ember/test-helpers";
Expand Down Expand Up @@ -131,6 +132,8 @@ module("Acceptance | roles", function (hooks) {
assert.strictEqual(role.id, request.params.id);
});
await click("[data-test-delete]");
await waitFor(".uk-modal.uk-open");
await click(".uk-modal .uk-button-primary");

// For some reason the await click is not actually waiting for the delete task to finish.
// Probably some runloop issue.
Expand Down
3 changes: 3 additions & 0 deletions tests/acceptance/scopes-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
currentURL,
fillIn,
click,
waitFor,
waitUntil,
settled,
} from "@ember/test-helpers";
Expand Down Expand Up @@ -140,6 +141,8 @@ module("Acceptance | scopes", function (hooks) {
assert.strictEqual(scope.id, request.params.id);
});
await click("[data-test-delete]");
await waitFor(".uk-modal.uk-open");
await click(".uk-modal .uk-button-primary");

// For some reason the await click is not actually waiting for the delete task to finish.
// Probably some runloop issue.
Expand Down
5 changes: 4 additions & 1 deletion tests/integration/components/edit-form-test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Service from "@ember/service";
import { render, click } from "@ember/test-helpers";
import { render, click, waitFor } from "@ember/test-helpers";
import { hbs } from "ember-cli-htmlbars";
import { setupIntl } from "ember-intl/test-support";
import { setupRenderingTest } from "ember-qunit";
Expand Down Expand Up @@ -89,6 +89,9 @@ module("Integration | Component | edit-form", function (hooks) {
await render(hbs`<EditForm @model={{this.model}}/>`);

await click("[data-test-delete]");
await waitFor(".uk-modal.uk-open");
await click(".uk-modal .uk-button-primary");

assert.verifySteps(["destroyRecord"]);
});

Expand Down
53 changes: 53 additions & 0 deletions tests/unit/decorators/confirm-task-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { click, waitFor } from "@ember/test-helpers";
import { task } from "ember-concurrency";
import { setupTest } from "ember-qunit";
import { module, test } from "qunit";

import { confirmTask } from "ember-emeis/decorators/confirm-task";

const DUMMY_ARGUMENT = { test: "ok" };

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

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

@task
@confirmTask({ container: "#ember-testing" })
*deleteSomething(someArgument) {
assert.step("delete");
return yield someArgument;
}
};
});

test("it triggers the action on confirm", async function (assert) {
assert.expect(3);
const instance = new this.TestStub();

const result = instance.deleteSomething.perform(DUMMY_ARGUMENT);

await waitFor(".uk-modal.uk-open");
await click(".uk-button-primary");

assert.deepEqual(result.args[0], DUMMY_ARGUMENT);
assert.verifySteps(["delete"]);
});

test("it does not trigger the action on cancel", async function (assert) {
assert.expect(1);
const instance = new this.TestStub();
instance.deleteSomething.perform();

await waitFor(".uk-modal.uk-open");
await click(".uk-modal-close");

assert.verifySteps([]);
});
});
10 changes: 8 additions & 2 deletions translations/de.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ emeis:
empty: "Keine Einträge gefunden..."
loading: "Laden..."
general-error: "Ein Problem ist aufgetretten. Bitte versuchen Sie es erneut."
actions: "Aktionen"

scopes:
title: "Organisationen"
Expand Down Expand Up @@ -67,10 +68,15 @@ emeis:
form:
save: "Speichern"
back: "Zurück"
save-success: "Erfolgreich gespeichert."
save-error: "Während dem Speichern ist ein Fehler aufgetretten. Bitte versuchen Sie es erneut."
delete: "Löschen"
ok: "OK"
cancel: "Abbrechen"
confirmText: "Sind Sie sich sicher, dass Sie diese Aktion fortfahren wollen?"
confirmEntryDelete: "Sind Sie sich sicher, dass Sie diesen Eintrag löschen wollen?"
confirmUserDelete: "Sind Sie sich sicher, dass Sie diesen Benutzer löschen wollen?"
save-success: "Erfolgreich gespeichert."
delete-success: "Erfolgreich gelöscht."
save-error: "Während dem Speichern ist ein Fehler aufgetretten. Bitte versuchen Sie es erneut."
delete-error: "Beim löschen ist ein Problem aufgetretten. Falls der Eintrag noch existiert, versuchen Sie es erneut."
phone-hint: "Buchstaben sind in Telefonnummern nicht erlaubt."
slug-hint: "Es sind nur Buchstaben, Nummern und Bindestriche erlaubt."
Expand Down
6 changes: 6 additions & 0 deletions translations/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ emeis:
empty: "No entries found..."
loading: "Loading..."
general-error: "A problem occured. Please try again."
actions: "Actions"

scopes:
title: "Scopes"
Expand Down Expand Up @@ -69,6 +70,11 @@ emeis:
save: "Save"
back: "Back"
delete: "Delete"
ok: "OK"
cancel: "Cancel"
confirmText: "Are you sure you want to proceed with this action?"
confirmEntryDelete: "Are you sure you want to delete this entry?"
confirmUserDelete: "Are you sure you want to delete this user?"
save-success: "Saved successfully"
delete-success: "Deleted successfully"
save-error: "A problem occurred while saving. Please try again."
Expand Down

0 comments on commit 69d3c17

Please sign in to comment.