Skip to content

Commit

Permalink
Add filter input to group members table
Browse files Browse the repository at this point in the history
Add a search box to filter group members. The filter currently only matches the
username, is applied client-side and is case-insensitive.
  • Loading branch information
robertknight committed Nov 20, 2024
1 parent 0a78569 commit 9fb8b57
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 8 deletions.
44 changes: 40 additions & 4 deletions h/static/scripts/group-forms/components/EditGroupMembersForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DataTable, Scroll } from '@hypothesis/frontend-shared';
import { useEffect, useState } from 'preact/hooks';
import { DataTable, Input, Scroll } from '@hypothesis/frontend-shared';
import { useEffect, useMemo, useState } from 'preact/hooks';

import { routes } from '../routes';
import type { Group } from '../config';
Expand Down Expand Up @@ -56,6 +56,8 @@ export default function EditGroupMembersForm({
},
];

const [filter, setFilter] = useState('');

const renderRow = (user: MemberRow, field: keyof MemberRow) => {
switch (field) {
case 'username':
Expand All @@ -75,12 +77,45 @@ export default function EditGroupMembersForm({
};

const memberText = pluralize(members?.length ?? 0, 'member', 'members');
const normalizedFilter = filter.toLowerCase();

const filteredMembers = useMemo(() => {
if (!normalizedFilter || !members) {
return members;
}
// nb. We can get away with lower-casing name and filter to do
// case-insensitive search because of the character set restrictions on
// usernames. This would be incorrect for Unicode text.
return members.filter(m =>
m.username.toLowerCase().includes(normalizedFilter),
);
}, [normalizedFilter, members]);

let emptyMessage;
if (members !== null && members.length > 0 && filter) {
emptyMessage = (
<div data-testid="no-filter-match">
No members match the filter {'"'}
<b>{filter}</b>
{'"'}
</div>
);
}

return (
<FormContainer title="Edit group members">
<GroupFormHeader group={group} />
<hr />
<div className="flex py-3">
<div className="flex py-3 items-center">
{/* Input has `w-full` style. Wrap in container to stop it stretching to full width of row. */}
<div>
<Input
aria-label="Search members"
data-testid="search-input"
placeholder="Search…"
onInput={e => setFilter((e.target as HTMLInputElement).value)}
/>
</div>
<div className="grow" />
<div data-testid="member-count">
{members?.length ?? '...'} {memberText}
Expand All @@ -91,9 +126,10 @@ export default function EditGroupMembersForm({
<Scroll>
<DataTable
title="Group members"
rows={members ?? []}
rows={filteredMembers ?? []}
columns={columns}
renderItem={renderRow}
emptyMessage={emptyMessage}
/>
</Scroll>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ describe('EditGroupMembersForm', () => {
{
username: 'bob',
},
{
username: 'johnsmith',
},
]);

$imports.$mock({
Expand All @@ -52,24 +55,37 @@ describe('EditGroupMembersForm', () => {
return wrapper.find('ErrorNotice').prop('message') !== null;
});

const getRenderedUsers = wrapper => {
return wrapper.find('[data-testid="username"]').map(node => node.text());
};

const enterFilterValue = (wrapper, value) => {
const filter = wrapper.find('input[data-testid="search-input"]');
filter.getDOMNode().value = value;
filter.simulate('input');
};

it('fetches and displays members', async () => {
const wrapper = createForm();
assert.calledWith(fakeCallAPI, '/api/groups/1234/members');

await waitForTable(wrapper);

const users = wrapper.find('[data-testid="username"]');
assert.equal(users.length, 1);
assert.equal(users.at(0).text(), 'bob');
const users = getRenderedUsers(wrapper);
assert.deepEqual(users, ['bob', 'johnsmith']);
});

it('displays member count', async () => {
fakeCallAPI.withArgs('/api/groups/1234/members').resolves([
{
username: 'bob',
},
]);
const wrapper = createForm();
const memberCount = wrapper.find('[data-testid="member-count"]');
assert.equal(memberCount.text(), '... members');

await waitForTable(wrapper);

assert.equal(memberCount.text(), '1 member');
});

Expand All @@ -94,4 +110,31 @@ describe('EditGroupMembersForm', () => {
// Unmount while fetching. This should abort the request.
wrapper.unmount();
});

it('filters members', async () => {
const wrapper = createForm();
await waitForTable(wrapper);

// Filter should remove non-matching users
enterFilterValue(wrapper, 'john');
const users = getRenderedUsers(wrapper);
assert.deepEqual(users, ['johnsmith']);

// Filter should match anywhere in username
enterFilterValue(wrapper, 'smith');
const users2 = getRenderedUsers(wrapper);
assert.deepEqual(users2, ['johnsmith']);

// Filter should be case-insensitive
enterFilterValue(wrapper, 'BOB');
const users3 = getRenderedUsers(wrapper);
assert.deepEqual(users3, ['bob']);
});

it('displays message if filter does not match any members', async () => {
const wrapper = createForm();
await waitForTable(wrapper);
enterFilterValue(wrapper, 'no-match');
assert.isTrue(wrapper.exists('[data-testid="no-filter-match"]'));
});
});

0 comments on commit 9fb8b57

Please sign in to comment.