Skip to content
Merged
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
4 changes: 4 additions & 0 deletions src/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@
.text-prewrap {
white-space: pre-wrap;
}

.username .form-control::placeholder {
font-size: var(--pgn-typography-form-input-font-size-sm);
}
23 changes: 23 additions & 0 deletions src/components/UsernameFilter.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { screen } from '@testing-library/react';
import UsernameFilter from '@src/components/UsernameFilter';
import messages from '@src/components/messages';
import { renderWithIntl } from '@src/testUtils';

describe('UsernameFilter', () => {
const setup = (filterValue = '', setFilter = jest.fn()) => {
renderWithIntl(
<UsernameFilter column={{ filterValue, setFilter }} />
);
return { setFilter };
};

it('shows the placeholder correctly', () => {
setup();
expect(screen.getByPlaceholderText(messages.searchPlaceholder.defaultMessage)).toBeInTheDocument();
});

it('shows the initial value correctly', () => {
setup('value');
expect(screen.getByDisplayValue('value')).toBeInTheDocument();
});
});
29 changes: 29 additions & 0 deletions src/components/UsernameFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useIntl } from '@openedx/frontend-base';
import { FormControl, Icon } from '@openedx/paragon';
import { Search } from '@openedx/paragon/icons';
import { useDebouncedFilter } from '@src/hooks/useDebouncedFilter';
import messages from '@src/components/messages';

const UsernameFilter = ({ column: { filterValue, setFilter } }: { column: { filterValue: string, setFilter: (value: string) => void } }) => {
const intl = useIntl();
const { inputValue, handleChange } = useDebouncedFilter({
filterValue,
setFilter,
});

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
handleChange(e.target.value);
};

return (
<FormControl
className="mb-0 username"
onChange={handleInputChange}
placeholder={intl.formatMessage(messages.searchPlaceholder)}
trailingElement={<Icon src={Search} />}
value={inputValue}
/>
);
};

export default UsernameFilter;
5 changes: 5 additions & 0 deletions src/components/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ const messages = defineMessages({
defaultMessage: 'Could not find student matching identifier: {identifier}',
description: 'Error message displayed when a learner cannot be found based on the provided identifier (email or username)',
},
searchPlaceholder: {
id: 'instruct.usernameFilter.searchPlaceholder',
defaultMessage: 'Search By Username or Email',
description: 'Placeholder text for the username filter input',
}
});

export default messages;
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ describe('EnrollmentSummary', () => {
isLoading: false,
});
renderWithIntl(<EnrollmentSummary />);
screen.debug();

expect(screen.getByText(messages.allEnrollmentsLabel.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(formatNumberWithCommas(mockCounter.enrollmentCounts.total))).toBeInTheDocument();
Expand Down
10 changes: 7 additions & 3 deletions src/courseTeam/components/MembersContent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { renderWithIntl } from '@src/testUtils';
import MembersContent from '@src/courseTeam/components/MembersContent';
import { useTeamMembers } from '@src/courseTeam/data/apiHook';
import { useTeamMembers, useRoles } from '@src/courseTeam/data/apiHook';
import messages from '@src/courseTeam/messages';

const courseId = 'course-v1:edX+DemoX+Demo_Course';

jest.mock('@src/courseTeam/data/apiHook', () => ({
useTeamMembers: jest.fn(),
useRoles: jest.fn(),
}));

jest.mock('react-router-dom', () => ({
Expand All @@ -21,11 +22,14 @@ const mockTeamMembers = [
{ username: 'user2', email: 'user2@example.com', role: 'Staff' },
];

const mockRoles = { results: [{ role: 'Admin', displayName: 'Admin' }, { role: 'Staff', displayName: 'Staff' }] };

const renderComponent = () => renderWithIntl(<MembersContent />);

describe('MembersContent', () => {
beforeEach(() => {
jest.clearAllMocks();
(useRoles as jest.Mock).mockReturnValue({ data: mockRoles, isLoading: false });
});

it('renders loading state correctly', () => {
Expand All @@ -48,10 +52,10 @@ describe('MembersContent', () => {

expect(screen.getByText(mockTeamMembers[0].username)).toBeInTheDocument();
expect(screen.getByText(mockTeamMembers[0].email)).toBeInTheDocument();
expect(screen.getByText(mockTeamMembers[0].role)).toBeInTheDocument();
expect(screen.getByRole('cell', { name: mockTeamMembers[0].role })).toBeInTheDocument();
expect(screen.getByText(mockTeamMembers[1].username)).toBeInTheDocument();
expect(screen.getByText(mockTeamMembers[1].email)).toBeInTheDocument();
expect(screen.getByText(mockTeamMembers[1].role)).toBeInTheDocument();
expect(screen.getByRole('cell', { name: mockTeamMembers[1].role })).toBeInTheDocument();
});

it('renders empty state when no team members', () => {
Expand Down
76 changes: 63 additions & 13 deletions src/courseTeam/components/MembersContent.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,59 @@
import { useState, useCallback, useMemo } from 'react';
import { useState, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { useIntl } from '@openedx/frontend-base';
import { Button, DataTable } from '@openedx/paragon';
import { useTeamMembers } from '@src/courseTeam/data/apiHook';
import { Button, DataTable, FormControl, Icon } from '@openedx/paragon';
import { FilterList } from '@openedx/paragon/icons';
import UsernameFilter from '@src/components/UsernameFilter';
import { useRoles, useTeamMembers } from '@src/courseTeam/data/apiHook';
import messages from '@src/courseTeam/messages';
import { Role } from '@src/courseTeam/types';
import { DataTableFetchDataProps } from '@src/types';

const TEAM_MEMBERS_PAGE_SIZE = 25;

const RoleFilter = ({ column: { filterValue, setFilter } }: { column: { filterValue: string, setFilter: (value: string) => void } }) => {
Comment thread
diana-villalvazo-wgu marked this conversation as resolved.
const intl = useIntl();
const { courseId = '' } = useParams<{ courseId: string }>();
const { data } = useRoles(courseId);

const handleSelectChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setFilter(e.target.value);
};

const roles = useMemo(() => {
return [{ value: '', label: intl.formatMessage(messages.allRoles) }, ...(data?.results || []).map((role: Role) => ({ value: role.role, label: role.displayName }))];
}, [data, intl]);

return (
<FormControl
as="select"
className="mb-0"
disabled={!data}
name="role"
size="md"
value={filterValue}
onChange={handleSelectChange}
leadingElement={<Icon src={FilterList} />}
>
{roles.map(role => (
<option key={role.value} value={role.value}>
{role.label}
</option>
))}
</FormControl>
);
};

const MembersContent = () => {
const intl = useIntl();
const { courseId = '' } = useParams<{ courseId: string }>();
const [filters, setFilters] = useState({ page: 0, emailOrUsername: '', role: '' });
const { data: { results: teamMembers = [], numPages = 1, count = 0 } = {}, isLoading = false } = useTeamMembers(courseId, { ...filters, pageSize: TEAM_MEMBERS_PAGE_SIZE });

const tableColumns = useMemo(() => [
{ accessor: 'username', Header: intl.formatMessage(messages.username) },
{ accessor: 'email', Header: intl.formatMessage(messages.email) },
{ accessor: 'role', Header: intl.formatMessage(messages.role) },
{ accessor: 'username', Header: intl.formatMessage(messages.username), Filter: UsernameFilter },
{ accessor: 'email', Header: intl.formatMessage(messages.email), disableFilters: true },
{ accessor: 'role', Header: intl.formatMessage(messages.role), Filter: RoleFilter },
], [intl]);

const additionalColumns = useMemo(() => [{
Expand All @@ -29,16 +66,27 @@ const MembersContent = () => {
)
}], [intl]);

const handleFetchData = useCallback(({ pageIndex, filters: tableFilters }: { pageIndex: number, filters: { id: string, value: string }[] }) => {
// Filters will be handled in a future iteration, for now we will just update pagination
console.log(pageIndex, tableFilters);
if (pageIndex !== filters.page) {
setFilters(prevFilters => ({
const handleFetchData = (data: DataTableFetchDataProps) => {
const usernameFilter = data.filters?.find((f) => f.id === 'username');
const newEmailOrUsername = usernameFilter ? usernameFilter.value : '';
const rolesFilter = data.filters?.find((f) => f.id === 'role');
const newRole = rolesFilter ? rolesFilter.value : '';
const filtersChanged = (newEmailOrUsername !== filters.emailOrUsername) || (newRole !== filters.role);

if (filtersChanged) {
setFilters((prevFilters) => ({
...prevFilters,
page: pageIndex,
emailOrUsername: newEmailOrUsername,
role: newRole,
page: 0,
}));
return;
}

if (data.pageIndex !== filters.page) {
setFilters((prevFilters) => ({ ...prevFilters, page: data.pageIndex }));
}
}, [filters.page]);
};

const tableState = useMemo(() => ({
pageIndex: filters.page,
Expand All @@ -52,11 +100,13 @@ const MembersContent = () => {
data={teamMembers}
fetchData={handleFetchData}
state={tableState}
isFilterable
isLoading={isLoading}
isPaginated
itemCount={count}
manualFilters
manualPagination
numBreakoutFilters={2}
pageSize={TEAM_MEMBERS_PAGE_SIZE}
pageCount={numPages}
RowStatusComponent={() => null}
Expand Down
5 changes: 5 additions & 0 deletions src/courseTeam/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ const messages = defineMessages({
defaultMessage: 'No team members found.',
description: 'Message displayed when there are no team members',
},
allRoles: {
id: 'instruct.courseTeam.allRoles',
defaultMessage: 'All Roles',
description: 'Option label for filtering by all roles',
},
});

export default messages;
8 changes: 4 additions & 4 deletions src/dateExtensions/components/DateExtensionsList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ describe('DateExtensionsList', () => {
renderComponent({});
const user = userEvent.setup();

const searchInput = screen.getByPlaceholderText(/search for a learner/i);
const searchInput = screen.getByPlaceholderText(/search by username or email/i);
expect(searchInput).toBeInTheDocument();

await user.type(searchInput, 'test_user');
Expand All @@ -106,7 +106,7 @@ describe('DateExtensionsList', () => {
renderComponent({});
const user = userEvent.setup();

const searchInput = screen.getByPlaceholderText(/search for a learner/i);
const searchInput = screen.getByPlaceholderText(/search by username or email/i);

// Type multiple characters quickly
await user.type(searchInput, 'test');
Expand Down Expand Up @@ -171,7 +171,7 @@ describe('DateExtensionsList', () => {
}

// Now apply a filter
const searchInput = screen.getByPlaceholderText(/search for a learner/i);
const searchInput = screen.getByPlaceholderText(/search by username or email/i);
await user.type(searchInput, 'test');

// Should reset to page 0 when filter changes
Expand Down Expand Up @@ -217,7 +217,7 @@ describe('DateExtensionsList', () => {
const user = userEvent.setup();

// Apply a filter first
const searchInput = screen.getByPlaceholderText(/search for a learner/i);
const searchInput = screen.getByPlaceholderText(/search by username or email/i);
await user.type(searchInput, 'test');

// Wait for the filter to be applied
Expand Down
26 changes: 2 additions & 24 deletions src/dateExtensions/components/DateExtensionsList.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { useState } from 'react';
import { useParams } from 'react-router-dom';
import { useIntl } from '@openedx/frontend-base';
import { Button, DataTable, FormControl, Icon } from '@openedx/paragon';
import { Button, DataTable } from '@openedx/paragon';
import messages from '../messages';
import { LearnerDateExtension } from '../types';
import { useDateExtensions } from '../data/apiHook';
import { Search } from '@openedx/paragon/icons';
import SelectGradedSubsection from './SelectGradedSubsection';
import { useDebouncedFilter } from '../../hooks/useDebouncedFilter';
import { DataTableFetchDataProps } from '@src/types';
import UsernameFilter from '@src/components/UsernameFilter';

const DATE_EXTENSIONS_PAGE_SIZE = 25;

Expand All @@ -17,28 +17,6 @@ export interface DateExtensionListProps {
onClickAdd?: () => void,
}

const UsernameFilter = ({ column: { filterValue, setFilter } }: { column: { filterValue: string, setFilter: (value: string) => void } }) => {
const intl = useIntl();
const { inputValue, handleChange } = useDebouncedFilter({
filterValue,
setFilter,
});

const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
handleChange(e.target.value);
};

return (
<FormControl
className="mb-0"
onChange={handleInputChange}
placeholder={intl.formatMessage(messages.searchLearnerPlaceholder)}
trailingElement={<Icon src={Search} />}
value={inputValue}
/>
);
};

const GradedSubsectionFilter = ({ column: { filterValue, setFilter } }: { column: { filterValue: string, setFilter: (value: string) => void } }) => {
const intl = useIntl();
const { inputValue, handleChange } = useDebouncedFilter({
Expand Down
5 changes: 0 additions & 5 deletions src/dateExtensions/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,6 @@ const messages = defineMessages({
defaultMessage: 'All Graded Subsections',
description: 'Label for the all graded subsections option in filters',
},
searchLearnerPlaceholder: {
id: 'instruct.dateExtensions.page.filters.searchLearnerPlaceholder',
defaultMessage: 'Search for a Learner',
description: 'Placeholder text for the search learner input field',
},
noDateExtensions: {
id: 'instruct.dateExtensions.page.noDateExtensions',
defaultMessage: 'No results found',
Expand Down
Loading