richie-education 2.25.0-b2.dev103 → 2.25.0-b2.dev110
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/js/api/joanie.ts +13 -0
- package/js/hooks/useCourseOrders/index.ts +32 -0
- package/js/{pages/TeacherDashboardContractsLayout/hooks → hooks}/useDefaultOrganizationId/index.spec.tsx +6 -42
- package/js/hooks/useOrganizations/index.ts +4 -4
- package/js/hooks/useResources/index.tsx +2 -0
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +68 -83
- package/js/pages/TeacherDashboardContractsLayout/components/ContractFiltersBar/index.spec.tsx +21 -62
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.tsx +1 -1
- package/js/pages/TeacherDashboardContractsLayout/styles.scss +0 -4
- package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.spec.tsx +62 -0
- package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx +123 -0
- package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnersFiltersBar/index.spec.tsx +70 -0
- package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnersFiltersBar/index.tsx +31 -0
- package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx +158 -0
- package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.ts +44 -0
- package/js/pages/TeacherDashboardCourseLearnersLayout/index.spec.tsx +385 -0
- package/js/pages/TeacherDashboardCourseLearnersLayout/index.tsx +141 -0
- package/js/settings/index.ts +2 -2
- package/js/settings/settings.prod.ts +1 -0
- package/js/types/Joanie.ts +47 -0
- package/js/utils/OrderHelper/index.ts +5 -1
- package/js/utils/test/factories/cunningham.ts +13 -0
- package/js/utils/test/factories/joanie.ts +44 -0
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +8 -2
- package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +1 -1
- package/js/widgets/Dashboard/components/FilterOrganization/index.tsx +27 -10
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +36 -76
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +6 -1
- package/js/widgets/Dashboard/utils/teacherRouteMessages.tsx +23 -0
- package/js/widgets/Dashboard/utils/teacherRoutes.tsx +31 -0
- package/package.json +1 -1
- package/scss/objects/_dashboard.scss +7 -0
- /package/js/{pages/TeacherDashboardContractsLayout/hooks → hooks}/useDefaultOrganizationId/index.tsx +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createIntl } from 'react-intl';
|
|
2
|
+
import { screen } from '@testing-library/react';
|
|
3
|
+
import { getAllByRole, within } from '@testing-library/dom';
|
|
4
|
+
import { faker } from '@faker-js/faker';
|
|
5
|
+
import { NestedCourseOrderFactory } from 'utils/test/factories/joanie';
|
|
6
|
+
import { expectNoSpinner } from 'utils/test/expectSpinner';
|
|
7
|
+
import { PaginationFactory } from 'utils/test/factories/cunningham';
|
|
8
|
+
import { DEFAULT_DATE_FORMAT } from 'hooks/useDateFormat';
|
|
9
|
+
import { OrderState } from 'types/Joanie';
|
|
10
|
+
import { render } from 'utils/test/render';
|
|
11
|
+
import { PresentationalAppWrapper } from 'utils/test/wrappers/PresentationalAppWrapper';
|
|
12
|
+
import CourseLearnerDataGrid from '.';
|
|
13
|
+
|
|
14
|
+
describe('pages/CourseLearnerDataGrid', () => {
|
|
15
|
+
it('should render a list of user', async () => {
|
|
16
|
+
const courseOrderList = NestedCourseOrderFactory({
|
|
17
|
+
state: OrderState.VALIDATED,
|
|
18
|
+
certificate_id: faker.string.uuid(),
|
|
19
|
+
}).many(3);
|
|
20
|
+
render(
|
|
21
|
+
<CourseLearnerDataGrid
|
|
22
|
+
courseOrders={courseOrderList}
|
|
23
|
+
sortModel={[
|
|
24
|
+
{
|
|
25
|
+
field: 'created_on',
|
|
26
|
+
sort: 'asc',
|
|
27
|
+
},
|
|
28
|
+
]}
|
|
29
|
+
setSortModel={jest.fn()}
|
|
30
|
+
pagination={PaginationFactory().one()}
|
|
31
|
+
isLoading={false}
|
|
32
|
+
/>,
|
|
33
|
+
{ wrapper: PresentationalAppWrapper },
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
await expectNoSpinner();
|
|
37
|
+
|
|
38
|
+
// Table header should have been rendered with 5 columns
|
|
39
|
+
const columnHeaders = screen.getAllByRole('columnheader');
|
|
40
|
+
expect(columnHeaders.length).toBe(5);
|
|
41
|
+
// avatar column have no header
|
|
42
|
+
expect(columnHeaders[0]).toHaveTextContent('');
|
|
43
|
+
expect(columnHeaders[1]).toHaveTextContent('Learner');
|
|
44
|
+
expect(columnHeaders[2]).toHaveTextContent('Enrolled on');
|
|
45
|
+
expect(columnHeaders[3]).toHaveTextContent('State');
|
|
46
|
+
expect(columnHeaders[4]).toHaveTextContent('Actions');
|
|
47
|
+
|
|
48
|
+
const intl = createIntl({ locale: 'en' });
|
|
49
|
+
// Table body should have been rendered with 3 rows (one per contract)
|
|
50
|
+
courseOrderList.forEach((courseOrder) => {
|
|
51
|
+
const row = screen.getByTestId(courseOrder.id);
|
|
52
|
+
const cells = getAllByRole(row, 'cell');
|
|
53
|
+
expect(cells.length).toBe(5);
|
|
54
|
+
expect(cells[1]).toHaveTextContent(courseOrder.owner.full_name);
|
|
55
|
+
expect(cells[2]).toHaveTextContent(
|
|
56
|
+
intl.formatDate(new Date(courseOrder.created_on), DEFAULT_DATE_FORMAT),
|
|
57
|
+
);
|
|
58
|
+
expect(cells[3]).toHaveTextContent('Completed');
|
|
59
|
+
expect(within(cells[4]).getByText('Contact')).toBeInTheDocument();
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
|
2
|
+
import { Button, DataGrid, DataGridProps, PaginationProps, Row } from '@openfun/cunningham-react';
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { NestedCourseOrder } from 'types/Joanie';
|
|
5
|
+
import { DEFAULT_DATE_FORMAT } from 'hooks/useDateFormat';
|
|
6
|
+
import OrderStateMessage from 'widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage';
|
|
7
|
+
import DashboardListAvatar from 'widgets/Dashboard/components/DashboardListAvatar';
|
|
8
|
+
|
|
9
|
+
const messages = defineMessages({
|
|
10
|
+
columnLearnerName: {
|
|
11
|
+
defaultMessage: 'Learner',
|
|
12
|
+
description: "Label for learner's name column",
|
|
13
|
+
id: 'pages.CourseLearnerDataGrid.columnLearnerName',
|
|
14
|
+
},
|
|
15
|
+
columnActions: {
|
|
16
|
+
defaultMessage: 'Actions',
|
|
17
|
+
description: 'Label for actions column',
|
|
18
|
+
id: 'pages.CourseLearnerDataGrid.columnActions',
|
|
19
|
+
},
|
|
20
|
+
columnPurchaseDate: {
|
|
21
|
+
defaultMessage: 'Enrolled on',
|
|
22
|
+
description: 'Label for enrolled date column',
|
|
23
|
+
id: 'pages.CourseLearnerDataGrid.columnPurchaseDate',
|
|
24
|
+
},
|
|
25
|
+
columnState: {
|
|
26
|
+
defaultMessage: 'State',
|
|
27
|
+
description: 'Label for state column',
|
|
28
|
+
id: 'pages.CourseLearnerDataGrid.columnState',
|
|
29
|
+
},
|
|
30
|
+
contactButton: {
|
|
31
|
+
defaultMessage: 'Contact',
|
|
32
|
+
description: 'Label for the contact learner button',
|
|
33
|
+
id: 'pages.CourseLearnerDataGrid.contactButton',
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
interface CourseLearnerDataGridProps {
|
|
38
|
+
courseOrders: NestedCourseOrder[];
|
|
39
|
+
sortModel: DataGridProps['sortModel'];
|
|
40
|
+
setSortModel: DataGridProps['onSortModelChange'];
|
|
41
|
+
pagination: PaginationProps;
|
|
42
|
+
isLoading: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const CourseLearnerDataGrid = ({
|
|
46
|
+
courseOrders,
|
|
47
|
+
sortModel,
|
|
48
|
+
setSortModel,
|
|
49
|
+
pagination,
|
|
50
|
+
isLoading,
|
|
51
|
+
}: CourseLearnerDataGridProps) => {
|
|
52
|
+
const intl = useIntl();
|
|
53
|
+
|
|
54
|
+
const columns = [
|
|
55
|
+
{
|
|
56
|
+
id: 'avatar',
|
|
57
|
+
enableSorting: false,
|
|
58
|
+
renderCell: (params: { row: Row }) => {
|
|
59
|
+
return <DashboardListAvatar title={params.row.owner__full_name} />;
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
field: 'owner__full_name',
|
|
64
|
+
headerName: intl.formatMessage(messages.columnLearnerName),
|
|
65
|
+
enableSorting: false,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
field: 'created_on',
|
|
69
|
+
headerName: intl.formatMessage(messages.columnPurchaseDate),
|
|
70
|
+
enableSorting: false,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'orderState',
|
|
74
|
+
headerName: intl.formatMessage(messages.columnState),
|
|
75
|
+
enableSorting: false,
|
|
76
|
+
renderCell: (params: { row: Row }) => {
|
|
77
|
+
// TODO(rlecellier): create NestedOrderCourseStateMessage that's get label dedicated to teatchers.
|
|
78
|
+
// like signature needed for studdent is awaiting signature for a teacher.
|
|
79
|
+
return (
|
|
80
|
+
<OrderStateMessage
|
|
81
|
+
order={params.row.courseOrder}
|
|
82
|
+
contractDefinition={params.row.courseOrder.product.contract_definition_id}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'actions',
|
|
89
|
+
headerName: intl.formatMessage(messages.columnActions),
|
|
90
|
+
renderCell: (params: { row: Row }) => {
|
|
91
|
+
return (
|
|
92
|
+
<Button href={`mailto:${params.row.owner__email}`} size="small" color="secondary">
|
|
93
|
+
<FormattedMessage {...messages.contactButton} />
|
|
94
|
+
</Button>
|
|
95
|
+
);
|
|
96
|
+
},
|
|
97
|
+
enableSorting: false,
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const rows = useMemo(() => {
|
|
102
|
+
return courseOrders.map((courseOrder) => ({
|
|
103
|
+
id: courseOrder.id,
|
|
104
|
+
owner__full_name: courseOrder.owner.full_name || courseOrder.owner.username,
|
|
105
|
+
owner__email: courseOrder.owner.email,
|
|
106
|
+
created_on: intl.formatDate(new Date(courseOrder.created_on), DEFAULT_DATE_FORMAT),
|
|
107
|
+
courseOrder,
|
|
108
|
+
}));
|
|
109
|
+
}, [courseOrders]);
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<DataGrid
|
|
113
|
+
columns={columns}
|
|
114
|
+
rows={rows}
|
|
115
|
+
pagination={pagination}
|
|
116
|
+
sortModel={sortModel}
|
|
117
|
+
onSortModelChange={setSortModel}
|
|
118
|
+
isLoading={isLoading}
|
|
119
|
+
/>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export default CourseLearnerDataGrid;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { screen } from '@testing-library/react';
|
|
2
|
+
import { userEvent } from '@testing-library/user-event';
|
|
3
|
+
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
4
|
+
import { ContractState } from 'types/Joanie';
|
|
5
|
+
import { OrganizationFactory } from 'utils/test/factories/joanie';
|
|
6
|
+
import { noop } from 'utils';
|
|
7
|
+
import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
|
|
8
|
+
import { render } from 'utils/test/render';
|
|
9
|
+
import CourseLearnersFiltersBar from '.';
|
|
10
|
+
|
|
11
|
+
jest.mock('utils/context', () => ({
|
|
12
|
+
__esModule: true,
|
|
13
|
+
default: mockRichieContextFactory({
|
|
14
|
+
authentication: { backend: 'fonzie', endpoint: 'https://auth.endpoint.test' },
|
|
15
|
+
joanie_backend: { endpoint: 'https://joanie.endpoint' },
|
|
16
|
+
}).one(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
describe('<CourseLearnersFiltersBar/>', () => {
|
|
20
|
+
setupJoanieSession();
|
|
21
|
+
|
|
22
|
+
it('should render organization filter with default value', async () => {
|
|
23
|
+
const filterChange = jest.fn();
|
|
24
|
+
const organizations = OrganizationFactory().many(2);
|
|
25
|
+
const defaultValues = {
|
|
26
|
+
signature_state: ContractState.LEARNER_SIGNED,
|
|
27
|
+
organization_id: organizations[1].id,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
render(
|
|
31
|
+
<CourseLearnersFiltersBar
|
|
32
|
+
onFiltersChange={filterChange}
|
|
33
|
+
defaultValues={defaultValues}
|
|
34
|
+
organizationList={organizations}
|
|
35
|
+
/>,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
// Two selects should have rendered
|
|
39
|
+
const organizationFilter: HTMLInputElement = screen.getByRole('combobox', {
|
|
40
|
+
name: 'Organization',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(organizationFilter).toHaveAttribute('value', organizations[1].title);
|
|
44
|
+
expect(filterChange).not.toHaveBeenCalled();
|
|
45
|
+
|
|
46
|
+
// Now change value of organization filter should trigger the onFiltersChange callback
|
|
47
|
+
const user = userEvent.setup();
|
|
48
|
+
await user.click(organizationFilter);
|
|
49
|
+
|
|
50
|
+
const optionToSelect = screen.getByRole('option', { name: organizations[0].title });
|
|
51
|
+
await user.click(optionToSelect);
|
|
52
|
+
expect(filterChange).toHaveBeenNthCalledWith(1, { organization_id: organizations[0].id });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should allow to hide organization filter', async () => {
|
|
56
|
+
const organizations = OrganizationFactory().many(2);
|
|
57
|
+
|
|
58
|
+
render(
|
|
59
|
+
<CourseLearnersFiltersBar
|
|
60
|
+
onFiltersChange={noop}
|
|
61
|
+
hideFilterOrganization={true}
|
|
62
|
+
organizationList={organizations}
|
|
63
|
+
/>,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Organization filter should not have been rendered
|
|
67
|
+
const organizationFilter = screen.queryByRole('combobox', { name: 'Organization' });
|
|
68
|
+
expect(organizationFilter).not.toBeInTheDocument();
|
|
69
|
+
});
|
|
70
|
+
});
|
package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnersFiltersBar/index.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { CourseOrderResourceQuery, Organization } from 'types/Joanie';
|
|
2
|
+
import FilterOrganization from 'widgets/Dashboard/components/FilterOrganization';
|
|
3
|
+
import FiltersBar from 'widgets/Dashboard/components/FiltersBar';
|
|
4
|
+
|
|
5
|
+
export interface CourseLearnersFiltersBarProps {
|
|
6
|
+
defaultValues?: CourseOrderResourceQuery;
|
|
7
|
+
onFiltersChange: (filters: Partial<CourseOrderResourceQuery>) => void;
|
|
8
|
+
organizationList: Organization[];
|
|
9
|
+
hideFilterOrganization?: boolean;
|
|
10
|
+
}
|
|
11
|
+
const CourseLearnersFiltersBar = ({
|
|
12
|
+
defaultValues,
|
|
13
|
+
onFiltersChange,
|
|
14
|
+
organizationList,
|
|
15
|
+
hideFilterOrganization,
|
|
16
|
+
}: CourseLearnersFiltersBarProps) => {
|
|
17
|
+
return (
|
|
18
|
+
!hideFilterOrganization && (
|
|
19
|
+
<FiltersBar>
|
|
20
|
+
<FilterOrganization
|
|
21
|
+
defaultValue={defaultValues?.organization_id}
|
|
22
|
+
organizationList={organizationList}
|
|
23
|
+
onChange={onFiltersChange}
|
|
24
|
+
clearable={true}
|
|
25
|
+
/>
|
|
26
|
+
</FiltersBar>
|
|
27
|
+
)
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default CourseLearnersFiltersBar;
|
package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import fetchMock from 'fetch-mock';
|
|
2
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
3
|
+
import { act } from 'react-dom/test-utils';
|
|
4
|
+
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
5
|
+
import { CourseProductRelationFactory, OrganizationFactory } from 'utils/test/factories/joanie';
|
|
6
|
+
import { JoanieAppWrapper, setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
|
|
7
|
+
import useCourseLearnersFilters from '.';
|
|
8
|
+
|
|
9
|
+
jest.mock('utils/context', () => ({
|
|
10
|
+
__esModule: true,
|
|
11
|
+
default: mockRichieContextFactory({
|
|
12
|
+
authentication: { backend: 'fonzie', endpoint: 'https://auth.test' },
|
|
13
|
+
joanie_backend: { endpoint: 'https://joanie.endpoint' },
|
|
14
|
+
}).one(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
describe('useCourseLearnersFilters', () => {
|
|
18
|
+
setupJoanieSession();
|
|
19
|
+
|
|
20
|
+
it('should return default filter when called in a route without parameters', async () => {
|
|
21
|
+
const defaultOrganization = OrganizationFactory().one();
|
|
22
|
+
// fetching user's organizations to initialize default organizationId.
|
|
23
|
+
fetchMock.get('https://joanie.endpoint/api/v1.0/organizations/', [defaultOrganization]);
|
|
24
|
+
const { result } = renderHook(useCourseLearnersFilters, {
|
|
25
|
+
wrapper: ({ children }) => (
|
|
26
|
+
<JoanieAppWrapper routerOptions={{ path: '/', initialEntries: ['/'] }}>
|
|
27
|
+
{children}
|
|
28
|
+
</JoanieAppWrapper>
|
|
29
|
+
),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
await waitFor(() => {
|
|
33
|
+
expect(result.current.initialFilters).toStrictEqual({
|
|
34
|
+
organization_id: undefined,
|
|
35
|
+
course_id: undefined,
|
|
36
|
+
course_product_relation_id: undefined,
|
|
37
|
+
});
|
|
38
|
+
expect(result.current.filters).toStrictEqual({
|
|
39
|
+
organization_id: undefined,
|
|
40
|
+
course_id: undefined,
|
|
41
|
+
course_product_relation_id: undefined,
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should use route parameters values when given', async () => {
|
|
47
|
+
const defaultOrganization = OrganizationFactory().one();
|
|
48
|
+
const filteredOrganization = OrganizationFactory({ id: 'filtered' }).one();
|
|
49
|
+
const routeOrganization = OrganizationFactory({ id: 'route' }).one();
|
|
50
|
+
const routeCourseProductRelation = CourseProductRelationFactory().one();
|
|
51
|
+
// fetching user's organizations to initialize default organizationId.
|
|
52
|
+
fetchMock.get('https://joanie.endpoint/api/v1.0/organizations/', [
|
|
53
|
+
defaultOrganization,
|
|
54
|
+
filteredOrganization,
|
|
55
|
+
]);
|
|
56
|
+
const { result } = renderHook(useCourseLearnersFilters, {
|
|
57
|
+
wrapper: ({ children }) => (
|
|
58
|
+
<JoanieAppWrapper
|
|
59
|
+
routerOptions={{
|
|
60
|
+
path: '/:organizationId/:courseId/:courseProductRelationId',
|
|
61
|
+
initialEntries: [
|
|
62
|
+
`/${routeOrganization.id}/${routeCourseProductRelation.course.id}/${routeCourseProductRelation.id}?organization_id=${filteredOrganization.id}`,
|
|
63
|
+
],
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
{children}
|
|
67
|
+
</JoanieAppWrapper>
|
|
68
|
+
),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await waitFor(() => {
|
|
72
|
+
expect(result.current.initialFilters).toStrictEqual({
|
|
73
|
+
organization_id: routeOrganization.id,
|
|
74
|
+
course_id: routeCourseProductRelation.course.id,
|
|
75
|
+
course_product_relation_id: routeCourseProductRelation.id,
|
|
76
|
+
});
|
|
77
|
+
expect(result.current.filters).toStrictEqual({
|
|
78
|
+
organization_id: routeOrganization.id,
|
|
79
|
+
course_id: routeCourseProductRelation.course.id,
|
|
80
|
+
course_product_relation_id: routeCourseProductRelation.id,
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should use organizationId from query parameters when it's not in route params", async () => {
|
|
86
|
+
const defaultOrganization = OrganizationFactory().one();
|
|
87
|
+
const filteredOrganization = OrganizationFactory({ id: 'filtered' }).one();
|
|
88
|
+
const routeCourseProductRelation = CourseProductRelationFactory({ id: 'route' }).one();
|
|
89
|
+
// fetching user's organizations to initialize default organizationId.
|
|
90
|
+
fetchMock.get('https://joanie.endpoint/api/v1.0/organizations/', [
|
|
91
|
+
defaultOrganization,
|
|
92
|
+
filteredOrganization,
|
|
93
|
+
]);
|
|
94
|
+
const { result } = renderHook(useCourseLearnersFilters, {
|
|
95
|
+
wrapper: ({ children }) => (
|
|
96
|
+
<JoanieAppWrapper
|
|
97
|
+
routerOptions={{
|
|
98
|
+
path: '/:courseId/:courseProductRelationId',
|
|
99
|
+
initialEntries: [
|
|
100
|
+
`/${routeCourseProductRelation.course.id}/${routeCourseProductRelation.id}/?organization_id=${filteredOrganization.id}`,
|
|
101
|
+
],
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
{children}
|
|
105
|
+
</JoanieAppWrapper>
|
|
106
|
+
),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await waitFor(() => {
|
|
110
|
+
expect(result.current.initialFilters).toStrictEqual({
|
|
111
|
+
organization_id: filteredOrganization.id,
|
|
112
|
+
course_id: routeCourseProductRelation.course.id,
|
|
113
|
+
course_product_relation_id: routeCourseProductRelation.id,
|
|
114
|
+
});
|
|
115
|
+
expect(result.current.filters).toStrictEqual({
|
|
116
|
+
organization_id: filteredOrganization.id,
|
|
117
|
+
course_id: routeCourseProductRelation.course.id,
|
|
118
|
+
course_product_relation_id: routeCourseProductRelation.id,
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('setFilters should update filter state', async () => {
|
|
124
|
+
const defaultOrganization = OrganizationFactory({ id: 'all' }).one();
|
|
125
|
+
const routeOrganization = OrganizationFactory({ id: 'route' }).one();
|
|
126
|
+
const routeCourseProductRelation = CourseProductRelationFactory().one();
|
|
127
|
+
// fetching user's organizations to initialize default organizationId.
|
|
128
|
+
fetchMock.get('https://joanie.endpoint/api/v1.0/organizations/', [defaultOrganization]);
|
|
129
|
+
const { result } = renderHook(useCourseLearnersFilters, {
|
|
130
|
+
wrapper: ({ children }) => (
|
|
131
|
+
<JoanieAppWrapper routerOptions={{ path: '/', initialEntries: ['/'] }}>
|
|
132
|
+
{children}
|
|
133
|
+
</JoanieAppWrapper>
|
|
134
|
+
),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const expectedInitialFilters = {
|
|
138
|
+
organization_id: undefined,
|
|
139
|
+
course_id: undefined,
|
|
140
|
+
course_product_relation_id: undefined,
|
|
141
|
+
};
|
|
142
|
+
await waitFor(() => {
|
|
143
|
+
expect(result.current.initialFilters).toStrictEqual(expectedInitialFilters);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const newFilters = {
|
|
147
|
+
organization_id: routeOrganization.id,
|
|
148
|
+
course_id: routeCourseProductRelation.course.id,
|
|
149
|
+
course_product_relation_id: routeCourseProductRelation.id,
|
|
150
|
+
};
|
|
151
|
+
act(() => {
|
|
152
|
+
result.current.setFilters(newFilters);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(result.current.filters).toStrictEqual(newFilters);
|
|
156
|
+
expect(result.current.initialFilters).toStrictEqual(expectedInitialFilters);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { useParams, useSearchParams } from 'react-router-dom';
|
|
3
|
+
import {
|
|
4
|
+
CourseListItem,
|
|
5
|
+
CourseOrderResourceQuery,
|
|
6
|
+
CourseProductRelation,
|
|
7
|
+
Organization,
|
|
8
|
+
} from 'types/Joanie';
|
|
9
|
+
|
|
10
|
+
export type CourseLearnersParams = {
|
|
11
|
+
courseId: CourseListItem['id'];
|
|
12
|
+
courseProductRelationId?: CourseProductRelation['id'];
|
|
13
|
+
organizationId?: Organization['id'];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const useCourseLearnersFilters = () => {
|
|
17
|
+
const { courseId, courseProductRelationId, organizationId } = useParams<CourseLearnersParams>();
|
|
18
|
+
const [searchParams] = useSearchParams();
|
|
19
|
+
const searchFilters: CourseOrderResourceQuery = useMemo(() => {
|
|
20
|
+
return {
|
|
21
|
+
course_id: courseId,
|
|
22
|
+
organization_id: searchParams.get('organization_id') || undefined,
|
|
23
|
+
course_product_relation_id: searchParams.get('course_product_relation_id') || undefined,
|
|
24
|
+
};
|
|
25
|
+
}, Array.from(searchParams.entries()));
|
|
26
|
+
|
|
27
|
+
const initialFilters = useMemo(() => {
|
|
28
|
+
return {
|
|
29
|
+
...searchFilters,
|
|
30
|
+
organization_id: organizationId || searchFilters.organization_id,
|
|
31
|
+
course_product_relation_id: courseProductRelationId,
|
|
32
|
+
};
|
|
33
|
+
}, []);
|
|
34
|
+
const [filters, setFilters] = useState<CourseOrderResourceQuery>(initialFilters);
|
|
35
|
+
|
|
36
|
+
// update current filter with initial value when it's ready
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
setFilters(initialFilters);
|
|
39
|
+
}, [initialFilters]);
|
|
40
|
+
|
|
41
|
+
return { initialFilters, filters, setFilters };
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default useCourseLearnersFilters;
|