Skip to content

Commit eaf397a

Browse files
authored
feat(userlist): add sortable columns to User List (#1615)
1 parent 25e376c commit eaf397a

5 files changed

Lines changed: 295 additions & 44 deletions

File tree

cypress/e2e/user/user-list.cy.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ describe('User List', () => {
3636
cy.get('#email').type(testUser.emailAddress);
3737
cy.get('#password').type(testUser.password);
3838

39-
cy.intercept('/api/v1/user?take=10&skip=0&sort=displayname').as('user');
39+
cy.intercept('/api/v1/user*').as('user');
4040

4141
cy.get('[data-testid=modal-ok-button]').click();
4242

@@ -56,7 +56,7 @@ describe('User List', () => {
5656

5757
cy.get('[data-testid=modal-title]').should('contain', `Delete User`);
5858

59-
cy.intercept('/api/v1/user?take=10&skip=0&sort=displayname').as('user');
59+
cy.intercept('/api/v1/user*').as('user');
6060

6161
cy.get('[data-testid=modal-ok-button]').should('contain', 'Delete').click();
6262

@@ -67,4 +67,37 @@ describe('User List', () => {
6767
.contains(testUser.emailAddress)
6868
.should('not.exist');
6969
});
70+
71+
it('sorts by column headers and updates request params and row order', () => {
72+
cy.intercept('GET', '/api/v1/user?*').as('userListFetch');
73+
74+
cy.visit('/users');
75+
cy.wait('@userListFetch');
76+
77+
cy.get('[data-testid=column-header-displayname]').click();
78+
cy.wait('@userListFetch').then((interception) => {
79+
const url = interception.request.url;
80+
expect(url).to.include('sort=displayname');
81+
expect(url).to.include('sortDirection=asc');
82+
});
83+
84+
cy.get(
85+
'[data-testid=user-list-row] [data-testid=user-list-username-link]'
86+
).then(($links) => {
87+
const displayNames = $links
88+
.toArray()
89+
.map((el) => (el as HTMLElement).innerText.trim().toLowerCase());
90+
const sortedAsc = [...displayNames].sort((a, b) => a.localeCompare(b));
91+
expect(displayNames).to.deep.equal(sortedAsc);
92+
});
93+
94+
cy.get('[data-testid=column-header-created]').click();
95+
cy.wait('@userListFetch').then((interception) => {
96+
const url = interception.request.url;
97+
expect(url).to.include('sort=created');
98+
expect(url).to.include('sortDirection=desc');
99+
});
100+
101+
cy.get('[data-testid=user-list-row]').should('have.length.greaterThan', 0);
102+
});
70103
});

seerr-api.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4117,8 +4117,16 @@ paths:
41174117
name: sort
41184118
schema:
41194119
type: string
4120-
enum: [created, updated, requests, displayname]
4120+
enum: [created, updated, requests, displayname, usertype, role]
41214121
default: created
4122+
- in: query
4123+
name: sortDirection
4124+
description: |
4125+
Sort direction. When omitted, the server chooses the direction per sort
4126+
field (e.g. displayname defaults to asc, requests/updated to desc).
4127+
schema:
4128+
type: string
4129+
enum: [asc, desc]
41224130
- in: query
41234131
name: q
41244132
required: false

server/routes/user/index.ts

Lines changed: 61 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,35 @@ router.get('/', async (req, res, next) => {
4545
: Math.max(10, includeIds.length);
4646
const skip = req.query.skip ? Number(req.query.skip) : 0;
4747
const q = req.query.q ? req.query.q.toString().toLowerCase() : '';
48+
const sortParam = req.query.sort ? req.query.sort.toString() : undefined;
49+
const sortDirectionQuery = req.query.sortDirection
50+
? req.query.sortDirection.toString().toLowerCase()
51+
: undefined;
52+
53+
let sortDirection: 'ASC' | 'DESC';
54+
if (sortDirectionQuery === 'asc') {
55+
sortDirection = 'ASC';
56+
} else if (sortDirectionQuery === 'desc') {
57+
sortDirection = 'DESC';
58+
} else {
59+
switch (sortParam) {
60+
case 'displayname':
61+
sortDirection = 'ASC';
62+
break;
63+
case 'requests':
64+
case 'updated':
65+
sortDirection = 'DESC';
66+
break;
67+
case 'created':
68+
case 'usertype':
69+
case 'role':
70+
case undefined:
71+
default:
72+
sortDirection = 'ASC';
73+
break;
74+
}
75+
}
76+
4877
let query = getRepository(User).createQueryBuilder('user');
4978

5079
if (q) {
@@ -58,29 +87,32 @@ router.get('/', async (req, res, next) => {
5887
query.andWhereInIds(includeIds);
5988
}
6089

61-
switch (req.query.sort) {
90+
switch (sortParam) {
91+
case 'created':
92+
query = query.orderBy('user.createdAt', sortDirection);
93+
break;
6294
case 'updated':
63-
query = query.orderBy('user.updatedAt', 'DESC');
95+
query = query.orderBy('user.updatedAt', sortDirection);
6496
break;
6597
case 'displayname':
6698
query = query
6799
.addSelect(
68100
`CASE WHEN (user.username IS NULL OR user.username = '') THEN (
69-
CASE WHEN (user.plexUsername IS NULL OR user.plexUsername = '') THEN (
70-
CASE WHEN (user.jellyfinUsername IS NULL OR user.jellyfinUsername = '') THEN
71-
"user"."email"
101+
CASE WHEN (user.plexUsername IS NULL OR user.plexUsername = '') THEN (
102+
CASE WHEN (user.jellyfinUsername IS NULL OR user.jellyfinUsername = '') THEN
103+
"user"."email"
104+
ELSE
105+
LOWER(user.jellyfinUsername)
106+
END)
72107
ELSE
73-
LOWER(user.jellyfinUsername)
108+
LOWER(user.plexUsername)
74109
END)
75110
ELSE
76-
LOWER(user.jellyfinUsername)
77-
END)
78-
ELSE
79-
LOWER(user.username)
80-
END`,
111+
LOWER(user.username)
112+
END`,
81113
'displayname_sort_key'
82114
)
83-
.orderBy('displayname_sort_key', 'ASC');
115+
.orderBy('displayname_sort_key', sortDirection);
84116
break;
85117
case 'requests':
86118
query = query
@@ -90,10 +122,25 @@ router.get('/', async (req, res, next) => {
90122
.from(MediaRequest, 'request')
91123
.where('request.requestedBy.id = user.id');
92124
}, 'request_count')
93-
.orderBy('request_count', 'DESC');
125+
.orderBy('request_count', sortDirection);
126+
break;
127+
case 'usertype':
128+
query = query.orderBy('user.userType', sortDirection);
129+
break;
130+
case 'role':
131+
query = query
132+
.addSelect(
133+
`CASE
134+
WHEN user.id = 1 THEN 0
135+
WHEN (user.permissions & ${Permission.ADMIN}) != 0 THEN 1
136+
ELSE 2
137+
END`,
138+
'role_sort_key'
139+
)
140+
.orderBy('role_sort_key', sortDirection);
94141
break;
95142
default:
96-
query = query.orderBy('user.id', 'ASC');
143+
query = query.orderBy('user.id', sortDirection);
97144
break;
98145
}
99146

0 commit comments

Comments
 (0)