Skip to content

Commit 7756b75

Browse files
authored
Merge pull request #8579 from michaelchadwick/frontend-4298-make-admin-users-columns-sortable
Allow Admin User columns to be sortable
2 parents 2ae30aa + 05bf39e commit 7756b75

File tree

8 files changed

+225
-93
lines changed

8 files changed

+225
-93
lines changed

packages/frontend/app/components/ilios-users.gjs

Lines changed: 42 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,20 @@ import { TrackedAsyncData } from 'ember-async-data';
66
import { tracked, cached } from '@glimmer/tracking';
77
import { ensureSafeComponent } from '@embroider/util';
88
import { action } from '@ember/object';
9-
import NewDirectoryUser from './new-directory-user';
10-
import NewUser from './new-user';
11-
import t from 'ember-intl/helpers/t';
129
import { on } from '@ember/modifier';
10+
import { fn } from '@ember/helper';
11+
import t from 'ember-intl/helpers/t';
1312
import pick from 'ilios-common/helpers/pick';
1413
import or from 'ember-truth-helpers/helpers/or';
15-
import { fn } from '@ember/helper';
1614
import FaIcon from 'ilios-common/components/fa-icon';
1715
import notEq from 'ember-truth-helpers/helpers/not-eq';
1816
import load from 'ember-async-data/helpers/load';
1917
import BulkNewUsers from 'frontend/components/bulk-new-users';
2018
import LoadingSpinner from 'ilios-common/components/loading-spinner';
2119
import PagedlistControls from 'ilios-common/components/pagedlist-controls';
22-
import gt from 'ember-truth-helpers/helpers/gt';
2320
import UserList from 'frontend/components/user-list';
21+
import NewDirectoryUser from './new-directory-user';
22+
import NewUser from './new-user';
2423

2524
const DEBOUNCE_TIMEOUT = 250;
2625

@@ -35,7 +34,6 @@ export default class IliosUsersComponent extends Component {
3534
constructor() {
3635
super(...arguments);
3736
this.query = this.args.query;
38-
this.searchForUsers.perform();
3937
}
4038

4139
@cached
@@ -53,17 +51,21 @@ export default class IliosUsersComponent extends Component {
5351
return ensureSafeComponent(component, this);
5452
}
5553

56-
searchForUsers = restartableTask(async () => {
54+
searchForUsers = restartableTask(async (sort = 'lastName', sortDir = 'ASC') => {
5755
const q = cleanQuery(this.args.query);
58-
await timeout(DEBOUNCE_TIMEOUT);
59-
return this.store.query('user', {
56+
const orderPrimary = `order_by[${sort === 'fullName' ? 'lastName' : sort}]`;
57+
const orderSecondary = 'order_by[firstName]';
58+
const query = {
6059
// overfetch for nextPage functionality
6160
limit: this.args.limit + 1,
6261
q,
6362
offset: this.args.offset,
64-
'order_by[lastName]': 'ASC',
65-
'order_by[firstName]': 'ASC',
66-
});
63+
};
64+
query[orderPrimary] = sortDir;
65+
query[orderSecondary] = sortDir;
66+
67+
await timeout(DEBOUNCE_TIMEOUT);
68+
return this.store.query('user', query);
6769
});
6870

6971
@action
@@ -164,43 +166,34 @@ export default class IliosUsersComponent extends Component {
164166
{{/let}}
165167
{{/if}}
166168
</section>
167-
{{#if this.searchForUsers.lastSuccessful}}
168-
<div data-test-top-paged-list-controls>
169-
<PagedlistControls
170-
@total={{this.searchForUsers.lastSuccessful.value.length}}
171-
@offset={{@offset}}
172-
@limit={{@limit}}
173-
@limitless={{true}}
174-
@setOffset={{this.setOffset}}
175-
@setLimit={{this.setLimit}}
176-
/>
177-
</div>
178-
<div class="list">
179-
{{#if this.searchForUsers.isRunning}}
180-
<LoadingSpinner />
181-
{{else}}
182-
{{#if (gt this.searchForUsers.lastSuccessful.value.length 0)}}
183-
<UserList @users={{this.searchForUsers.lastSuccessful.value}} />
184-
{{else}}
185-
<span class="no-results">
186-
{{t "general.noResultsFound"}}
187-
</span>
188-
{{/if}}
189-
{{/if}}
190-
</div>
191-
<div data-test-bottom-paged-list-controls>
192-
<PagedlistControls
193-
@total={{this.searchForUsers.lastSuccessful.value.length}}
194-
@offset={{@offset}}
195-
@limit={{@limit}}
196-
@limitless={{true}}
197-
@setOffset={{this.setOffset}}
198-
@setLimit={{this.setLimit}}
199-
/>
200-
</div>
201-
{{else}}
202-
<LoadingSpinner />
203-
{{/if}}
169+
<div data-test-top-paged-list-controls>
170+
<PagedlistControls
171+
@total={{this.searchForUsers.lastSuccessful.value.length}}
172+
@offset={{@offset}}
173+
@limit={{@limit}}
174+
@limitless={{true}}
175+
@setOffset={{this.setOffset}}
176+
@setLimit={{this.setLimit}}
177+
/>
178+
</div>
179+
<div class="list">
180+
<UserList
181+
@headerIsLocked={{true}}
182+
@searchForUsers={{this.searchForUsers}}
183+
@sortBy={{@sortBy}}
184+
@setSortBy={{@setSortBy}}
185+
/>
186+
</div>
187+
<div data-test-bottom-paged-list-controls>
188+
<PagedlistControls
189+
@total={{this.searchForUsers.lastSuccessful.value.length}}
190+
@offset={{@offset}}
191+
@limit={{@limit}}
192+
@limitless={{true}}
193+
@setOffset={{this.setOffset}}
194+
@setLimit={{this.setLimit}}
195+
/>
196+
</div>
204197
</section>
205198
</div>
206199
</template>
Lines changed: 125 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,138 @@
1+
import Component from '@glimmer/component';
2+
import { cached } from '@glimmer/tracking';
3+
import { action } from '@ember/object';
4+
import { fn } from '@ember/helper';
5+
import or from 'ember-truth-helpers/helpers/or';
6+
import eq from 'ember-truth-helpers/helpers/eq';
17
import t from 'ember-intl/helpers/t';
28
import FaIcon from 'ilios-common/components/fa-icon';
39
import { LinkTo } from '@ember/routing';
10+
import { TrackedAsyncData } from 'ember-async-data';
411
import UserNameInfo from 'ilios-common/components/user-name-info';
5-
<template>
6-
<div class="user-list" data-test-user-list ...attributes>
7-
<table>
8-
<thead>
12+
import SortableTh from 'ilios-common/components/sortable-th';
13+
import LoadingSpinner from 'ilios-common/components/loading-spinner';
14+
15+
export default class UserList extends Component {
16+
get sortedAscending() {
17+
return this.args.sortBy.search(/desc/) === -1;
18+
}
19+
20+
@cached
21+
get sortedUsersData() {
22+
return new TrackedAsyncData(
23+
this.args.searchForUsers.perform(
24+
this.sortInfo.column,
25+
this.sortInfo.descending ? 'DESC' : 'ASC',
26+
),
27+
);
28+
}
29+
30+
get sortedUsers() {
31+
if (this.args.users) {
32+
return this.args.users;
33+
}
34+
return this.sortedUsersData.isResolved ? this.sortedUsersData.value : [];
35+
}
36+
37+
get sortInfo() {
38+
const parts = this.args.sortBy.split(':');
39+
const column = parts[0];
40+
const descending = parts.length > 1 && parts[1] === 'desc';
41+
42+
return { column, descending, sortBy: this.args.sortBy };
43+
}
44+
45+
@action
46+
setSortBy(what) {
47+
if (this.args.sortBy === what) {
48+
what += ':desc';
49+
}
50+
this.args.setSortBy(what);
51+
}
52+
<template>
53+
<table class="user-list" data-test-user-list ...attributes>
54+
<thead class={{if @headerIsLocked "locked"}}>
955
<tr>
10-
<th class="text-left" colspan="1"></th>
11-
<th class="text-left" colspan="3">
56+
<th colspan="1" class="user-list-disabled" data-test-user-list-disabled></th>
57+
<SortableTh
58+
@align="left"
59+
@colspan={{3}}
60+
@sortedAscending={{this.sortedAscending}}
61+
@onClick={{fn this.setSortBy "fullName"}}
62+
@sortedBy={{or (eq @sortBy "fullName") (eq @sortBy "fullName:desc")}}
63+
>
1264
{{t "general.fullName"}}
13-
</th>
14-
<th class="text-left hide-from-small-screen" colspan="2">
65+
</SortableTh>
66+
<SortableTh
67+
class="hide-from-small-screen"
68+
@align="left"
69+
@colspan={{2}}
70+
@sortedAscending={{this.sortedAscending}}
71+
@onClick={{fn this.setSortBy "campusId"}}
72+
@sortedBy={{or (eq @sortBy "campusId") (eq @sortBy "campusId:desc")}}
73+
>
1574
{{t "general.campusId"}}
16-
</th>
17-
<th class="text-left hide-from-small-screen" colspan="5">
75+
</SortableTh>
76+
<SortableTh
77+
class="hide-from-small-screen"
78+
@align="left"
79+
@colspan={{5}}
80+
@sortedAscending={{this.sortedAscending}}
81+
@onClick={{fn this.setSortBy "email"}}
82+
@sortedBy={{or (eq @sortBy "email") (eq @sortBy "email:desc")}}
83+
>
1884
{{t "general.email"}}
19-
</th>
20-
<th class="text-left hide-from-small-screen" colspan="2">
21-
{{t "general.primarySchool"}}
22-
</th>
85+
</SortableTh>
86+
<th colspan="2" class="hide-from-small-screen" data-test-user-list-school>{{t
87+
"general.primarySchool"
88+
}}</th>
2389
</tr>
2490
</thead>
2591
<tbody>
26-
{{#each @users as |user|}}
27-
<tr class={{unless user.enabled "disabled-user-account" ""}} data-test-user>
28-
<td class="text-left" colspan="1">
29-
{{#unless user.enabled}}
30-
<FaIcon
31-
@icon="user-xmark"
32-
@title={{t "general.disabled"}}
33-
class="error"
34-
data-test-disabled-user-icon
35-
/>
36-
{{/unless}}
37-
</td>
38-
<td class="text-left" colspan="3">
39-
<LinkTo @route="user" @model={{user}} data-test-user-link>
40-
<UserNameInfo @user={{user}} />
41-
</LinkTo>
42-
</td>
43-
<td class="text-left hide-from-small-screen" colspan="2" data-test-campus-id>
44-
{{user.campusId}}
45-
</td>
46-
<td class="text-left hide-from-small-screen" colspan="5" data-test-email>
47-
{{user.email}}
48-
</td>
49-
<td class="text-left hide-from-small-screen" colspan="2" data-test-school>
50-
{{user.school.title}}
51-
</td>
52-
</tr>
53-
{{/each}}
92+
{{#if (or @users this.sortedUsersData.isResolved)}}
93+
{{#if this.sortedUsers.length}}
94+
{{#each this.sortedUsers as |user|}}
95+
<tr
96+
class="user-list-row{{unless user.enabled ' disabled-user-account'}}"
97+
data-test-user
98+
>
99+
<td colspan="1" class="text-left" data-test-user-disabled>
100+
{{#unless user.enabled}}
101+
<FaIcon
102+
@icon="user-xmark"
103+
@title={{t "general.disabled"}}
104+
class="error"
105+
data-test-disabled-user-icon
106+
/>
107+
{{/unless}}
108+
</td>
109+
<td colspan="3" class="text-left" data-test-full-name>
110+
<LinkTo @route="user" @model={{user}} data-test-user-link>
111+
<UserNameInfo @user={{user}} />
112+
</LinkTo>
113+
</td>
114+
<td colspan="2" class="text-left hide-from-small-screen" data-test-campus-id>
115+
{{user.campusId}}
116+
</td>
117+
<td colspan="5" class="text-left hide-from-small-screen" data-test-email>
118+
{{user.email}}
119+
</td>
120+
<td colspan="2" class="text-left hide-from-small-screen" data-test-school>
121+
{{user.school.title}}
122+
</td>
123+
</tr>
124+
{{/each}}
125+
{{else}}
126+
<tr>
127+
<td colspan="13" class="no-results">
128+
{{t "general.noResultsFound"}}
129+
</td>
130+
</tr>
131+
{{/if}}
132+
{{else}}
133+
<LoadingSpinner />
134+
{{/if}}
54135
</tbody>
55136
</table>
56-
</div>
57-
</template>
137+
</template>
138+
}

packages/frontend/app/controllers/users.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export default class UsersController extends Controller {
77

88
queryParams = [
99
{
10+
sortBy: 'sortBy',
1011
offset: 'offset',
1112
limit: 'limit',
1213
query: 'filter',
@@ -15,6 +16,7 @@ export default class UsersController extends Controller {
1516
searchTerms: 'search',
1617
},
1718
];
19+
sortBy = 'fullName';
1820
offset = 0;
1921
limit = 25;
2022
query = null;

packages/frontend/app/styles/components/ilios-users.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@
3434
.list {
3535
@include m.main-list-box-table;
3636

37+
.user-list {
38+
thead {
39+
&.locked {
40+
position: sticky;
41+
top: 0;
42+
}
43+
}
44+
}
45+
3746
i {
3847
margin-left: 0.5rem;
3948
}

packages/frontend/app/templates/users.hbs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{{page-title (t "general.admin")}}
22
<BackToAdminDashboard />
33
<IliosUsers
4+
@sortBy={{this.sortBy}}
5+
@setSortBy={{set this "sortBy"}}
46
@offset={{this.offset}}
57
@setOffset={{set this "offset"}}
68
@limit={{this.limit}}

0 commit comments

Comments
 (0)