Skip to content

Commit

Permalink
feat(data-table): sortable columns for userlist (projectcaluma#341)
Browse files Browse the repository at this point in the history
* feat(data-table): sortable columns for userlist

* fix: data-table loading animation, minor refactoring

* fix: scope view with mirage backend

* fix: data table testing

Co-authored-by: Christian Zosel <[email protected]>
  • Loading branch information
derrabauke and czosel committed Dec 21, 2021
1 parent 2de32e6 commit bcc68c8
Show file tree
Hide file tree
Showing 14 changed files with 233 additions and 51 deletions.
4 changes: 2 additions & 2 deletions addon/-private/controllers/pagination.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { inject as service } from "@ember/service";
import { tracked } from "@glimmer/tracking";

export default class PaginationController extends Controller {
queryParams = ["page", "search"];
queryParams = ["page", "search", "sort"];

@service router;

Expand All @@ -13,7 +13,7 @@ export default class PaginationController extends Controller {

@action
updateQueryParam(field, value) {
// We dont want to set an empty stirng as this is still serialized
// We dont want to set an empty string as this is still serialized
if (typeof value === "string" && !value.length) {
value = null;
}
Expand Down
25 changes: 12 additions & 13 deletions addon/components/data-table.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -29,28 +29,27 @@
</div>

<table
class="uk-table uk-table-divider"
class="uk-table uk-table-striped"
...attributes
>

{{#if this.isLoading}}
<tbody>
<td colspan="99" class="uk-padding-large uk-animation-fade">
<UkFlex
@direction="column"
@horizontal="center"
@vertical="middle"
class="uk-height-large"
>
<UkSpinner @ratio="3" />
</UkFlex>
</td>
{{yield (hash
head=(component "data-table/head" sortedBy=this.sort update=this.updateSort)
)
}}
<tbody data-test-loading>
<tr>
<td colspan="99" class="uk-padding-large uk-animation-fade uk-text-center">
<UkSpinner @ratio="1" />
</td>
</tr>
</tbody>
{{else}}
{{yield
(hash
body=(component "data-table/body" models=this.data.value)
head=(component "data-table/head")
head=(component "data-table/head" sortedBy=this.sort update=this.updateSort)
refresh=(perform this.fetchData)
)
}}
Expand Down
56 changes: 46 additions & 10 deletions addon/components/data-table.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,21 @@ export default class DataTableComponent extends Component {
@tracked numPages;
@tracked internalSearch;
@tracked internalPage = 1;
@tracked internalSort;

// While using 'useTask' we ended up in an infinite loop.
// data = useTask(this, this.fetchData, () => [this.args.filter]);
data = useFunction(this, this.fetchData.perform, () => [
this.args.filter,
this.page,
this.search,
this.sort,
this.page,
]);

get sort() {
return this.internalSort || this.args.defaultSort;
}

get page() {
return this.args.page || this.internalPage;
}
Expand All @@ -31,6 +37,13 @@ export default class DataTableComponent extends Component {
return this.args.search || this.internalSearch;
}

set sort(sort) {
if (this.args.updateSort) {
this.args.updateSort(sort);
}
this.internalSort = sort;
}

set search(search) {
if (this.args.updateSearch) {
this.args.updateSearch(search);
Expand All @@ -39,6 +52,14 @@ export default class DataTableComponent extends Component {
}
}

set page(page) {
if (this.args.updatePage) {
this.args.updatePage(page);
} else {
this.internalPage = page;
}
}

get isLoading() {
return this.fetchData.isRunning;
}
Expand Down Expand Up @@ -66,7 +87,7 @@ export default class DataTableComponent extends Component {

let options = {
filter: { search: this.search, ...(this.args.filter || {}) },
sort: this.args.sort,
sort: this.sort,
include: this.args.include || "",
};

Expand All @@ -87,28 +108,43 @@ export default class DataTableComponent extends Component {
return data;
}

@action
updateSort(sortLabel) {
if (this.args.updateSort) {
this.args.updateSort(sortLabel);
}

const invers = this.sort[0] === "-";

if (
this.sort === sortLabel ||
(invers && this.sort.slice(1) === sortLabel)
) {
if (invers) {
this.internalSort = undefined;
} else {
this.internalSort = `-${sortLabel}`;
}
} else {
this.internalSort = sortLabel;
}
}

@action
updateSearch(submitEvent) {
// Prevent reload because of form submit
submitEvent.preventDefault();
this.search = submitEvent.target.elements.search.value;
this.fetchData.perform();
}

@action
resetSearch(event) {
event.preventDefault();
this.search = "";
this.fetchData.perform();
}

@action
updatePage(page) {
if (this.args.updatePage) {
this.args.updatePage(page);
} else {
this.internalPage = page;
}
this.fetchData.perform();
this.page = page;
}
}
4 changes: 2 additions & 2 deletions addon/components/data-table/head.hbs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<thead>
<tr>
{{yield}}
{{yield (hash sorthead=(component "data-table/head/sortable-th" update=@update sortedBy=@sortedBy))}}
</tr>
</thead>
</thead>
15 changes: 15 additions & 0 deletions addon/components/data-table/head/sortable-th.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<th>
<span
data-test-sortable-th={{@sort}}
class="sortable-th"
role="button"
{{on "click" (fn @update @sort)}}
>
{{yield}}
{{#if (eq @sortedBy @sort)}}
<UkIcon @icon="chevron-down"/>
{{else if (eq @sortedBy (concat "-" @sort))}}
<UkIcon @icon="chevron-up"/>
{{/if}}
</span>
</th>
5 changes: 5 additions & 0 deletions addon/controllers/scopes/edit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,9 @@ export default class ScopesEditIndexController extends Controller {

return model;
}

@action
setParent(scope) {
this.model.parent = scope;
}
}
3 changes: 2 additions & 1 deletion addon/decorators/handle-model-errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,14 @@ export default function (...args) {
this.notification.danger(this.intl.t(notFoundErrorMessage));
this.replaceWith(routeFor404);
} else {
console.error(exception);
this.notification.danger(this.intl.t(errorMessage));
}
};

try {
const result = originalDescriptor.apply(this, args);
return result.then ? result.catch(catchErrors) : result;
return result?.then ? result.catch(catchErrors) : result;
} catch (exception) {
catchErrors(exception);
}
Expand Down
2 changes: 1 addition & 1 deletion addon/templates/scopes/edit/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
@model={{this.allScopes}}
@selected={{@model.parent}}
@placeholder="{{t "emeis.scopes.headings.parent"}}..."
@onChange={{set @model.parent}} as |scope|
@onChange={{this.setParent}} as |scope|
>
{{scope.name}}
</RelationshipSelect>
Expand Down
27 changes: 15 additions & 12 deletions addon/templates/users/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,28 @@
@modelName="user"
@page={{this.page}}
@search={{this.search}}
@sort="last_name"
@defaultSort="last_name"
@updatePage={{fn this.updateQueryParam "page"}}
@updateSearch={{fn this.updateQueryParam "search"}} as |table|
@updateSearch={{fn this.updateQueryParam "search"}}
@updateSort={{fn this.updateQueryParam "sort"}}
@include={{"acls.role"}}
as |table|
>
<table.head>
<table.head as |head|>
{{#unless this.emailAsUsername}}
<th>
<head.sorthead @sort="username">
{{t "emeis.users.headings.username"}}
</th>
{{/unless}}
<th>
</head.sorthead>
{{/unless}}
<head.sorthead @sort="last_name">
{{t "emeis.users.headings.lastName"}}
</th>
<th>
</head.sorthead>
<head.sorthead @sort="first_name">
{{t "emeis.users.headings.firstName"}}
</th>
<th>
</head.sorthead>
<head.sorthead @sort="email">
{{t "emeis.users.headings.email"}}
</th>
</head.sorthead>
</table.head>
<table.body as |body|>
<body.row>
Expand Down
1 change: 1 addition & 0 deletions app/components/data-table/head/sortable-th.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "ember-emeis/components/data-table/head/sortable-th";
8 changes: 8 additions & 0 deletions app/styles/ember-emeis.scss
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ $ember-power-select-border-color: $global-border;
border-left-color: transparent;
}
}

.sortable-th {
cursor: pointer;

:hover {
text-decoration: underline;
}
}
/* END GENERAL INPUTS*/

/* TREE VIEW */
Expand Down
8 changes: 6 additions & 2 deletions tests/acceptance/users-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,9 @@ module("Acceptance | users", function (hooks) {
await click("[data-test-acl-delete] button");
// eslint-disable-next-line ember/no-settled-after-test-helper
await settled();
await waitUntil(() => this.element.querySelector("table thead"));
await waitUntil(() =>
this.element.querySelector("table tbody:not([data-test-loading])")
);
assert.dom("[data-test-acl-role]").exists({ count: 2 });
});

Expand Down Expand Up @@ -313,7 +315,9 @@ module("Acceptance | users", function (hooks) {
await click("[data-test-create-acl]");
// For some reason the await click is not actually waiting for the fetch task to finish.
// Probably some runloop issue.
await waitUntil(() => this.element.querySelector("table tr"));
await waitUntil(() =>
this.element.querySelector("table tbody:not([data-test-loading])")
);
assert.dom("[data-test-back]").doesNotExist();
assert.dom("[data-test-acl-scope]").hasText(scope.name.en);
assert.dom("[data-test-acl-role]").hasText(role.name.en);
Expand Down
39 changes: 31 additions & 8 deletions tests/integration/components/data-table-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ module("Integration | Component | data-table", function (hooks) {
});

test("pagination", async function (assert) {
assert.expect(19);
assert.expect(15);

this.set("modelName", "role");
this.set("page", 1);
Expand All @@ -79,8 +79,12 @@ module("Integration | Component | data-table", function (hooks) {
@page={{this.page}}
as |table|>
<table.head as |role|>
<td>Heading 1</td>
<td>Heading 2</td>
<role.sorthead @sort="one">
Heading One
</role.sorthead>
<role.sorthead @sort="two">
Heading One
</role.sorthead>
</table.head>
<table.body as |body|>
<body.row>
Expand Down Expand Up @@ -115,19 +119,29 @@ module("Integration | Component | data-table", function (hooks) {
});

test("search", async function (assert) {
assert.expect(4);
assert.expect(5);
const search = "test";

const store = this.owner.lookup("service:store");
const expectedSearch = [undefined, search];
store.query = (_, options) => {
assert.strictEqual(options.filter.search, search);
assert.strictEqual(options.filter.search, expectedSearch.shift());
return { meta: { pagination: { pages: 3 } } };
};

await render(hbs`
<DataTable
@modelName="role"
/>
as |table|>
<table.body as |body|>
<body.row>
{{#let body.model as |role|}}
<td>{{role.name}}</td>
<td>{{role.slug}}</td>
{{/let}}
</body.row>
</table.body>
</DataTable>
`);

assert.dom('form input[name="search"]').exists();
Expand All @@ -139,7 +153,7 @@ module("Integration | Component | data-table", function (hooks) {
});

test("external search", async function (assert) {
assert.expect(6);
assert.expect(8);
this.setProperties({
search: undefined,
});
Expand All @@ -155,7 +169,16 @@ module("Integration | Component | data-table", function (hooks) {
@modelName="role"
@search={{this.search}}
@updateSearch={{set this.search}}
/>
as |table|>
<table.body as |body|>
<body.row>
{{#let body.model as |role|}}
<td>{{role.name}}</td>
<td>{{role.slug}}</td>
{{/let}}
</body.row>
</table.body>
</DataTable>
`);

assert.dom('form input[name="search"]').exists();
Expand Down
Loading

0 comments on commit bcc68c8

Please sign in to comment.