Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MS] UsersPage search input #8996

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions client/src/components/users/UserFilterPopover.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<user-status-tag
:revoked="false"
class="status-tag"
@click="users.filters.statusActive = !users.filters.statusActive"
@click="onUserTagClicked(() => (users.filters.statusActive = !users.filters.statusActive))"
/>
<ms-checkbox
v-model="users.filters.statusActive"
Expand All @@ -35,7 +35,7 @@
<user-status-tag
:revoked="true"
class="status-tag"
@click="users.filters.statusRevoked = !users.filters.statusRevoked"
@click="onUserTagClicked(() => (users.filters.statusRevoked = !users.filters.statusRevoked))"
/>
<ms-checkbox
v-model="users.filters.statusRevoked"
Expand All @@ -50,7 +50,7 @@
:revoked="false"
:frozen="true"
class="status-tag"
@click="users.filters.statusFrozen = !users.filters.statusFrozen"
@click="onUserTagClicked(() => (users.filters.statusFrozen = !users.filters.statusFrozen))"
/>
<ms-checkbox
v-model="users.filters.statusFrozen"
Expand All @@ -71,7 +71,7 @@
>
<ion-label
class="body"
@click="users.filters.profileAdmin = !users.filters.profileAdmin"
@click="onUserTagClicked(() => (users.filters.profileAdmin = !users.filters.profileAdmin))"
>
{{ $msTranslate('UsersPage.filter.admin') }}
</ion-label>
Expand All @@ -86,7 +86,7 @@
>
<ion-label
class="body"
@click="users.filters.profileStandard = !users.filters.profileStandard"
@click="onUserTagClicked(() => (users.filters.profileStandard = !users.filters.profileStandard))"
>
{{ $msTranslate('UsersPage.filter.standard') }}
</ion-label>
Expand All @@ -98,7 +98,7 @@
<ion-item class="list-group-item ion-no-padding">
<ion-label
class="body"
@click="users.filters.profileOutsider = !users.filters.profileOutsider"
@click="onUserTagClicked(() => (users.filters.profileOutsider = !users.filters.profileOutsider))"
>
{{ $msTranslate('UsersPage.filter.outsider') }}
</ion-label>
Expand All @@ -119,9 +119,14 @@ import { UserCollection } from '@/components/users/types';
import { IonContent, IonItem, IonItemGroup, IonList, IonText, IonLabel } from '@ionic/vue';
import { MsCheckbox } from 'megashark-lib';

defineProps<{
const props = defineProps<{
users: UserCollection;
}>();

function onUserTagClicked(toggleCheckbox: () => void): void {
toggleCheckbox();
props.users.unselectHiddenUsers();
}
</script>

<style lang="scss" scoped>
Expand Down
22 changes: 21 additions & 1 deletion client/src/components/users/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface UserFilterChangeEvent {
export class UserCollection {
users: Array<UserModel>;
filters: UserFilterLabels;
searchFilter: string;

constructor() {
this.users = [];
Expand All @@ -57,6 +58,7 @@ export class UserCollection {
profileStandard: true,
profileOutsider: true,
};
this.searchFilter = '';
}

getFilters(): UserFilterLabels {
Expand Down Expand Up @@ -89,9 +91,23 @@ export class UserCollection {
});
}

getSearchFilterUsers(): Array<UserModel> {
const lowerSearchString = this.searchFilter.toLocaleLowerCase();

return this.users.filter((user) => {
return (
this.userIsVisible(user) &&
(user.humanHandle.label.toLocaleLowerCase().includes(lowerSearchString) ||
user.humanHandle.email.toLocaleLowerCase().includes(lowerSearchString))
);
});
}

unselectHiddenUsers(): void {
const filteredUsers = this.getSearchFilterUsers();

for (const entry of this.users) {
if (!this.userIsVisible(entry)) {
if (!this.userIsVisible(entry) || !filteredUsers.includes(entry)) {
entry.isSelected = false;
}
}
Expand All @@ -105,6 +121,10 @@ export class UserCollection {
return this.getUsers().length;
}

filteredUsersCount(): number {
return this.getSearchFilterUsers().length;
}

private userIsVisible(user: UserModel): boolean {
if (
(!this.filters.profileAdmin && user.currentProfile === UserProfile.Admin) ||
Expand Down
4 changes: 2 additions & 2 deletions client/src/views/users/UserGridDisplay.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<template>
<user-card
v-for="user in users.getUsers()"
v-for="user in users.getSearchFilterUsers()"
:key="user.id"
:user="user"
:disabled="user.isCurrent"
Expand All @@ -15,7 +15,7 @@
/>
<ion-text
class="no-match-result body"
v-show="users.getUsers().length === 0"
v-show="users.getSearchFilterUsers().length === 0"
>
{{ $msTranslate('UsersPage.noMatch') }}
</ion-text>
Expand Down
4 changes: 2 additions & 2 deletions client/src/views/users/UserListDisplay.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@
</ion-list-header>
<ion-text
class="no-match-result body"
v-show="users.getUsers().length === 0"
v-show="users.getSearchFilterUsers().length === 0"
>
{{ $msTranslate('UsersPage.noMatch') }}
</ion-text>
<user-list-item
v-for="user in users.getUsers()"
v-for="user in users.getSearchFilterUsers()"
:key="user.id"
:user="user"
:disabled="user.isCurrent"
Expand Down
9 changes: 8 additions & 1 deletion client/src/views/users/UsersPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@
}}
</ion-text>
</div>
<ms-search-input
:placeholder="'HomePage.organizationList.search'"
v-model="users.searchFilter"
@change="users.unselectHiddenUsers()"
id="search-input-users"
/>
<!-- prettier-ignore -->
<user-filter
:users="(users as UserCollection)"
Expand Down Expand Up @@ -121,6 +127,7 @@ import {
MsActionBar,
MsActionBarButton,
MsGridListToggle,
MsSearchInput,
MsSorter,
MsSorterChangeEvent,
Translatable,
Expand Down Expand Up @@ -268,7 +275,7 @@ async function revokeUser(user: UserInfo): Promise<void> {
}

function getUsersCount(): number {
return users.value.usersCount();
return users.value.filteredUsersCount();
}

async function revokeSelectedUsers(): Promise<void> {
Expand Down
126 changes: 126 additions & 0 deletions client/tests/e2e/specs/users_list.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,132 @@ msTest('Sort users list', async ({ usersPage }) => {
);
});

msTest('Search user list', async ({ usersPage }) => {
const searchInput = usersPage.locator('#search-input-users').locator('ion-input');
const actionBar = usersPage.locator('#activate-users-ms-action-bar');
const usersList = usersPage.locator('#users-page-user-list');
const items = usersList.getByRole('listitem');
const gridItems = usersPage.locator('.users-container-grid').locator('.user-card-item');

await expect(actionBar.locator('.counter')).toHaveText('8 users', { useInnerText: true });
await expect(items).toHaveCount(8);

// No matches
await fillIonInput(searchInput, 'abc');
await expect(actionBar.locator('.counter')).toHaveText('No user', { useInnerText: true });
await expect(usersList).toContainText('No users matching your filters');
await expect(items).toHaveCount(0);

// Search on email
await fillIonInput(searchInput, 'gmail');
await expect(actionBar.locator('.counter')).toHaveText('6 users', { useInnerText: true });
await expect(items).toHaveCount(6);
for (let i = 0; i < 6; i++) {
await expect(items.nth(i).locator('.user-email')).toContainText('gmail');
}

// Search on name
await fillIonInput(searchInput, 'he');
await expect(actionBar.locator('.counter')).toHaveText('2 users', { useInnerText: true });
// cspell:disable-next-line
await expect(items.nth(0).locator('.user-name')).toContainText('Jaheira');
await expect(items.nth(1).locator('.user-name')).toContainText('Patches');
// cspell:disable-next-line
await fillIonInput(searchInput, 'Valygar');
await expect(actionBar.locator('.counter')).toHaveText('One user', { useInnerText: true });
// cspell:disable-next-line
await expect(items.nth(0).locator('.user-name')).toContainText('Valygar');

// Check that selection resets on filter
await fillIonInput(searchInput, '');
await usersPage.locator('.user-list-header').locator('ion-checkbox').click();
await expect(actionBar.locator('.counter')).toHaveText('4 users selected', { useInnerText: true });
await fillIonInput(searchInput, 'he');
await expect(actionBar.locator('.counter')).toHaveText('2 users selected', { useInnerText: true });
await fillIonInput(searchInput, '');
await expect(actionBar.locator('.counter')).toHaveText('2 users selected', { useInnerText: true });
await expect(items).toHaveCount(8);
// cspell:disable-next-line
await expect(items.nth(1).locator('.user-name')).toContainText('Jaheira');
await expect(items.nth(1).locator('ion-checkbox')).toHaveState('checked');
await expect(items.nth(4).locator('.user-name')).toContainText('Patches');
await expect(items.nth(4).locator('ion-checkbox')).toHaveState('checked');

// Check that search persists on grid mode
await fillIonInput(searchInput, 'he');
await expect(items).toHaveCount(2);
await actionBar.locator('.ms-grid-list-toggle').locator('#grid-view').click();
await expect(gridItems).toHaveCount(2);
await fillIonInput(searchInput, 'hei');
await expect(gridItems).toHaveCount(1);
// cspell:disable-next-line
await expect(gridItems.nth(0).locator('.user-card-info__name')).toContainText('Jaheira');
await actionBar.locator('.ms-grid-list-toggle').locator('#list-view').click();
await expect(items).toHaveCount(1);
// cspell:disable-next-line
await expect(items.nth(0).locator('.user-name')).toContainText('Jaheira');
});

msTest('Search user grid', async ({ usersPage }) => {
const searchInput = usersPage.locator('#search-input-users').locator('ion-input');
const actionBar = usersPage.locator('#activate-users-ms-action-bar');
const usersList = usersPage.locator('.users-container-grid');
const items = usersList.locator('.user-card-item');

await usersPage.locator('#activate-users-ms-action-bar').locator('.ms-grid-list-toggle').locator('#grid-view').click();

await expect(actionBar.locator('.counter')).toHaveText('8 users', { useInnerText: true });
await expect(items).toHaveCount(8);

// No matches
await fillIonInput(searchInput, 'abc');
await expect(actionBar.locator('.counter')).toHaveText('No user', { useInnerText: true });
await expect(usersList).toContainText('No users matching your filters');
await expect(items).toHaveCount(0);

// Search on email
await fillIonInput(searchInput, 'gmail');
await expect(actionBar.locator('.counter')).toHaveText('6 users', { useInnerText: true });
await expect(items).toHaveCount(6);
for (let i = 0; i < 6; i++) {
await expect(items.nth(i).locator('.user-card-info__email')).toContainText('gmail');
}

// Search on name
await fillIonInput(searchInput, 'he');
await expect(actionBar.locator('.counter')).toHaveText('2 users', { useInnerText: true });
// cspell:disable-next-line
await expect(items.nth(0).locator('.user-card-info__name')).toContainText('Jaheira');
await expect(items.nth(1).locator('.user-card-info__name')).toContainText('Patches');
// cspell:disable-next-line
await fillIonInput(searchInput, 'Valygar');
await expect(actionBar.locator('.counter')).toHaveText('One user', { useInnerText: true });
// cspell:disable-next-line
await expect(items.nth(0).locator('.user-card-info__name')).toContainText('Valygar');

// Check that selection resets on filter
await fillIonInput(searchInput, '');
await expect(items).toHaveCount(8);
await items.nth(1).hover();
await items.nth(1).locator('ion-checkbox').click();
await items.nth(3).hover();
await items.nth(3).locator('ion-checkbox').click();
await items.nth(4).hover();
await items.nth(4).locator('ion-checkbox').click();
await items.nth(7).hover();
await items.nth(7).locator('ion-checkbox').click();
await expect(actionBar.locator('.counter')).toHaveText('4 users selected', { useInnerText: true });
await fillIonInput(searchInput, 'he');
await expect(actionBar.locator('.counter')).toHaveText('2 users selected', { useInnerText: true });
await fillIonInput(searchInput, '');
await expect(actionBar.locator('.counter')).toHaveText('2 users selected', { useInnerText: true });
// cspell:disable-next-line
await expect(items.nth(1).locator('.user-card-info__name')).toContainText('Jaheira');
await expect(items.nth(1).locator('ion-checkbox')).toHaveState('checked');
await expect(items.nth(4).locator('.user-card-info__name')).toContainText('Patches');
await expect(items.nth(4).locator('ion-checkbox')).toHaveState('checked');
});

msTest('Invite new user', async ({ usersPage }) => {
await usersPage.locator('#activate-users-ms-action-bar').locator('#button-invite-user').click();
// cspell:disable-next-line
Expand Down
1 change: 1 addition & 0 deletions newsfragments/8964.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added a search input for the organization user list