Skip to content

Commit

Permalink
[FEATURE] Pouvoir faire une recherche exacte d'utilisateurs dans Pix …
Browse files Browse the repository at this point in the history
…Admin (PIX-9567)

 #10637
  • Loading branch information
pix-service-auto-merge authored Dec 2, 2024
2 parents 46aca93 + 362b485 commit 1897308
Show file tree
Hide file tree
Showing 18 changed files with 470 additions and 306 deletions.
22 changes: 21 additions & 1 deletion admin/app/controllers/authenticated/users/list.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import Controller from '@ember/controller';
import { action } from '@ember/object';
import { service } from '@ember/service';
import { tracked } from '@glimmer/tracking';

const DEFAULT_PAGE_NUMBER = 1;
const DEFAULT_QUERY_TYPE = 'CONTAINS';

export default class ListController extends Controller {
queryParams = ['pageNumber', 'pageSize', 'id', 'firstName', 'lastName', 'email', 'username'];
@service intl;

queryParams = ['pageNumber', 'pageSize', 'id', 'firstName', 'lastName', 'email', 'username', 'queryType'];

get queryTypes() {
return [
{ value: 'CONTAINS', label: this.intl.t('pages.users-list.query.contains') },
{ value: 'EXACT_QUERY', label: this.intl.t('pages.users-list.query.exact') },
];
}

@tracked pageNumber = DEFAULT_PAGE_NUMBER;
@tracked queryType = DEFAULT_QUERY_TYPE;
@tracked pageSize = 10;
@tracked id = null;
@tracked firstName = null;
Expand All @@ -20,6 +32,7 @@ export default class ListController extends Controller {
@tracked lastNameForm = null;
@tracked emailForm = null;
@tracked usernameForm = null;
@tracked queryTypeForm = DEFAULT_QUERY_TYPE;

@action
async refreshModel(event) {
Expand All @@ -29,6 +42,7 @@ export default class ListController extends Controller {
this.lastName = this.lastNameForm;
this.email = this.emailForm;
this.username = this.usernameForm;
this.queryType = this.queryTypeForm;
this.pageNumber = DEFAULT_PAGE_NUMBER;
}

Expand Down Expand Up @@ -56,6 +70,10 @@ export default class ListController extends Controller {
onChangeUsername(event) {
this.usernameForm = event.target.value;
}
@action
onChangeQueryType(value) {
this.queryTypeForm = value;
}

@action
clearSearchFields() {
Expand All @@ -64,11 +82,13 @@ export default class ListController extends Controller {
this.lastName = null;
this.email = null;
this.username = null;
this.queryType = DEFAULT_QUERY_TYPE;

this.idForm = null;
this.firstNameForm = null;
this.lastNameForm = null;
this.emailForm = null;
this.usernameForm = null;
this.queryTypeForm = DEFAULT_QUERY_TYPE;
}
}
3 changes: 3 additions & 0 deletions admin/app/routes/authenticated/users/list.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default class ListRoute extends Route {
queryParams = {
pageNumber: { refreshModel: true },
pageSize: { refreshModel: true },
queryType: { refreshModel: true },
id: { refreshModel: true },
firstName: { refreshModel: true },
lastName: { refreshModel: true },
Expand All @@ -30,6 +31,7 @@ export default class ListRoute extends Route {
number: params.pageNumber,
size: params.pageSize,
},
queryType: params.queryType,
});
return users;
} catch (error) {
Expand All @@ -46,6 +48,7 @@ export default class ListRoute extends Route {
controller.lastName = null;
controller.email = null;
controller.username = null;
controller.queryType = 'CONTAINS';
}
}
}
17 changes: 14 additions & 3 deletions admin/app/styles/authenticated/users/list.scss
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
.user-list-form {
display: flex;
flex-wrap: wrap;
gap: 8px;
gap: var(--pix-spacing-2x);
justify-content: flex-end;

.pix-select {
min-width: 7.5rem;
margin-right: var(--pix-spacing-2x);
}

.pix-select .pix-icon {
width: 1.315rem;
height: 1.315rem;

}

&__input--small {
max-width: 190px;
max-width: 9.35rem;
}

&__actions {
display: flex;
gap: 8px;
gap: var(--pix-spacing-2x);
}
}
7 changes: 7 additions & 0 deletions admin/app/templates/authenticated/users/list.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
<h1 class="page-title">Rechercher un(des) utilisateur(s)</h1>
<div class="page-actions">
<form class="user-list-form" {{on "submit" this.refreshModel}}>
<PixSelect
@id="query-type-selector"
@onChange={{this.onChangeQueryType}}
@value={{this.queryTypeForm}}
@options={{this.queryTypes}}
@hideDefaultOption={{true}}
/>
<PixInput
@id="userId"
{{on "change" this.onChangeUserId}}
Expand Down
10 changes: 9 additions & 1 deletion admin/tests/acceptance/authenticated/users/list-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import { setupApplicationTest } from 'ember-qunit';
import { authenticateAdminMemberWithRole } from 'pix-admin/tests/helpers/test-init';
import { module, test } from 'qunit';

import setupIntl from '../../../helpers/setup-intl';

module('Acceptance | authenticated/users | list', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupIntl(hooks);

module('When user is not logged in', function () {
test('it should not be accessible by an unauthenticated user', async function (assert) {
Expand Down Expand Up @@ -171,7 +174,7 @@ module('Acceptance | authenticated/users | list', function (hooks) {
assert.strictEqual(currentURL(), '/users/list');
});

test('should empty all search fields', async function (assert) {
test('empties all search fields and resets query type to "Contains"', async function (assert) {
// given
await authenticateAdminMemberWithRole({ isSuperAdmin: true })(server);

Expand All @@ -182,6 +185,9 @@ module('Acceptance | authenticated/users | list', function (hooks) {
identifiant: 'emma123',
});
const screen = await visit('/users/list');
await click(screen.getByRole('button', { name: 'Contient' }));
await screen.findByRole('listbox');
await click(screen.getByRole('option', { name: 'Exacte' }));
await fillIn(screen.getByRole('textbox', { name: 'Nom' }), 'sardine');
await fillIn(screen.getByRole('textbox', { name: 'Adresse e-mail' }), '[email protected]');
await fillIn(screen.getByRole('textbox', { name: 'Identifiant' }), 'emma123');
Expand All @@ -194,6 +200,8 @@ module('Acceptance | authenticated/users | list', function (hooks) {
assert.dom(screen.getByRole('textbox', { name: 'Nom' })).hasNoValue();
assert.dom(screen.getByRole('textbox', { name: 'Adresse e-mail' })).hasNoValue();
assert.dom(screen.getByRole('textbox', { name: 'Identifiant' })).hasNoValue();
assert.dom(screen.getByRole('button', { name: 'Contient' })).exists();
assert.dom(screen.queryByRole('button', { name: 'Exacte' })).doesNotExist();
});

test('should let empty fields on search', async function (assert) {
Expand Down
6 changes: 6 additions & 0 deletions admin/tests/unit/routes/authenticated/users/list-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ module('Unit | Route | authenticated/users/list', function (hooks) {
number: 'somePageNumber',
size: 'somePageSize',
};
params.queryType = 'CONTAINS';
expectedQueryArgs.queryType = 'CONTAINS';
});

module('when queryParams filters are falsy', function () {
Expand All @@ -36,6 +38,7 @@ module('Unit | Route | authenticated/users/list', function (hooks) {
email: '',
username: '',
};
expectedQueryArgs.queryType = 'CONTAINS';

// then
sinon.assert.notCalled(route.store.query);
Expand Down Expand Up @@ -96,6 +99,7 @@ module('Unit | Route | authenticated/users/list', function (hooks) {
lastName: 'someLastName',
email: 'someEmail',
username: 'someUsername',
queryType: 'EXACT_QUERY',
};
});

Expand All @@ -112,6 +116,7 @@ module('Unit | Route | authenticated/users/list', function (hooks) {
assert.deepEqual(controller.lastName, null);
assert.deepEqual(controller.email, null);
assert.deepEqual(controller.username, null);
assert.deepEqual(controller.queryType, 'CONTAINS');
});
});

Expand All @@ -128,6 +133,7 @@ module('Unit | Route | authenticated/users/list', function (hooks) {
assert.deepEqual(controller.lastName, 'someLastName');
assert.deepEqual(controller.email, 'someEmail');
assert.deepEqual(controller.username, 'someUsername');
assert.deepEqual(controller.queryType, 'EXACT_QUERY');
});
});
});
Expand Down
6 changes: 6 additions & 0 deletions admin/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,12 @@
"update-certification-center-membership-role": "Le rôle du membre a été modifié."
}
}
},
"users-list": {
"query": {
"contains": "Contient",
"exact": "Exacte"
}
}
}
}
6 changes: 6 additions & 0 deletions admin/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,12 @@
"update-certification-center-membership-role": "Le rôle du membre a été modifié."
}
}
},
"users-list": {
"query": {
"contains": "Contient",
"exact": "Exacte"
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import * as userLoginSerializer from '../../infrastructure/serializers/jsonapi/u
* @returns {Promise<*>}
*/
const findPaginatedFilteredUsers = async function (request, h, dependencies = { userForAdminSerializer }) {
const { filter, page } = request.query;
const { filter, page, queryType } = request.query;

const { models: users, pagination } = await usecases.findPaginatedFilteredUsers({ filter, page });
const { models: users, pagination } = await usecases.findPaginatedFilteredUsers({ filter, page, queryType });
return dependencies.userForAdminSerializer.serialize(users, pagination);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { securityPreHandlers } from '../../../shared/application/security-pre-ha
import { SUPPORTED_LOCALES } from '../../../shared/domain/constants.js';
import { AVAILABLE_LANGUAGES } from '../../../shared/domain/services/language-service.js';
import { identifiersType } from '../../../shared/domain/types/identifiers-type.js';
import { QUERY_TYPES } from '../../domain/constants/user-query.js';
import { userAdminController } from './user.admin.controller.js';

export const userAdminRoutes = [
Expand Down Expand Up @@ -62,6 +63,10 @@ export const userAdminRoutes = [
number: Joi.number().integer().empty('').allow(null).optional(),
size: Joi.number().integer().empty('').allow(null).optional(),
}).default({}),
queryType: Joi.string()
.valid(QUERY_TYPES.CONTAINS, QUERY_TYPES.EXACT_QUERY)
.default(QUERY_TYPES.CONTAINS)
.optional(),
}),
},
handler: (request, h) => userAdminController.findPaginatedFilteredUsers(request, h),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const QUERY_TYPES = {
CONTAINS: 'CONTAINS',
EXACT_QUERY: 'EXACT_QUERY',
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const findPaginatedFilteredUsers = function ({ filter, page, userRepository }) {
return userRepository.findPaginatedFiltered({ filter, page });
const findPaginatedFilteredUsers = function ({ filter, page, queryType, userRepository }) {
return userRepository.findPaginatedFiltered({ filter, page, queryType });
};

export { findPaginatedFilteredUsers };
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { CertificationCenterMembership } from '../../../shared/domain/models/Cer
import { Membership } from '../../../shared/domain/models/Membership.js';
import { fetchPage, isUniqConstraintViolated } from '../../../shared/infrastructure/utils/knex-utils.js';
import { NON_OIDC_IDENTITY_PROVIDERS } from '../../domain/constants/identity-providers.js';
import { QUERY_TYPES } from '../../domain/constants/user-query.js';
import { User } from '../../domain/models/User.js';
import { UserDetailsForAdmin } from '../../domain/models/UserDetailsForAdmin.js';
import { UserLogin } from '../../domain/models/UserLogin.js';
Expand Down Expand Up @@ -159,9 +160,9 @@ const getUserDetailsForAdmin = async function (userId) {
});
};

const findPaginatedFiltered = async function ({ filter, page }) {
const findPaginatedFiltered = async function ({ filter, page, queryType = QUERY_TYPES.CONTAINS }) {
const query = knex('users')
.where((qb) => _setSearchFiltersForQueryBuilder(filter, qb))
.where((qb) => _setSearchFiltersForQueryBuilder(filter, qb, queryType))
.orderBy([{ column: 'firstName', order: 'asc' }, { column: 'lastName', order: 'asc' }, { column: 'id' }]);
const { results, pagination } = await fetchPage(query, page);

Expand Down Expand Up @@ -625,22 +626,24 @@ function _toDomainFromDTO({
});
}

function _setSearchFiltersForQueryBuilder(filter, qb) {
const { id, firstName, lastName, email, username } = filter;

function _setSearchFiltersForQueryBuilder(filter, qb, queryType) {
const id = filter.id;
const fields = ['email', 'firstName', 'lastName', 'email', 'username'];
if (id) {
qb.where({ id });
}
if (firstName) {
qb.whereILike('firstName', `%${firstName}%`);
}
if (lastName) {
qb.whereILike('lastName', `%${lastName}%`);
}
if (email) {
qb.whereILike('email', `%${email}%`);
}
if (username) {
qb.whereILike('username', `%${username}%`);

fields.forEach((field) => {
if (filter[field]) {
_applyQueryType(field, filter[field], qb, queryType);
}
});
}

function _applyQueryType(field, value, qb, queryType) {
if (queryType === QUERY_TYPES.EXACT_QUERY) {
qb.where(field, value);
} else {
qb.whereILike(field, `%${value}%`);
}
}
Loading

0 comments on commit 1897308

Please sign in to comment.