richie-education 2.25.0-b2.dev146 → 2.25.0-b2.dev148
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/hooks/useLearnerCoursesSearch/index.tsx +74 -0
- package/js/hooks/useOrders.ts +1 -0
- package/js/pages/DashboardCourses/_styles.scss +0 -1
- package/js/pages/DashboardCourses/index.spec.tsx +5 -3
- package/js/pages/DashboardCourses/index.tsx +28 -13
- package/js/pages/DashboardCourses/useOrdersEnrollments.tsx +7 -9
- package/js/pages/TeacherDashboardCoursesLoader/index.tsx +4 -2
- package/js/types/Joanie.ts +1 -0
- package/js/widgets/Dashboard/components/SearchBar/_styles.scss +7 -1
- package/js/widgets/Dashboard/components/SearchBar/index.tsx +5 -1
- package/package.json +1 -1
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { useSearchParams } from 'react-router-dom';
|
|
3
|
+
import { Enrollment, CredentialOrder, OrderState, ProductType } from 'types/Joanie';
|
|
4
|
+
import { Maybe, Nullable } from 'types/utils';
|
|
5
|
+
import { useOrdersEnrollments } from 'pages/DashboardCourses/useOrdersEnrollments';
|
|
6
|
+
|
|
7
|
+
const useLearnerCoursesSearch = () => {
|
|
8
|
+
const [searchParams, setSearchParams] = useSearchParams();
|
|
9
|
+
const [count, setCount] = useState<Maybe<number>>();
|
|
10
|
+
const [orderAndEnrollmentList, setOrderAndEnrollmentList] = useState<
|
|
11
|
+
(CredentialOrder | Enrollment)[]
|
|
12
|
+
>([]);
|
|
13
|
+
const [isNewSearchLoading, setIsNewSearchLoading] = useState(false);
|
|
14
|
+
const query = searchParams.get('query') || undefined;
|
|
15
|
+
const {
|
|
16
|
+
data,
|
|
17
|
+
isLoading,
|
|
18
|
+
next,
|
|
19
|
+
hasMore,
|
|
20
|
+
count: currentCount,
|
|
21
|
+
error,
|
|
22
|
+
} = useOrdersEnrollments({
|
|
23
|
+
query,
|
|
24
|
+
orderFilters: {
|
|
25
|
+
product_type: [ProductType.CREDENTIAL],
|
|
26
|
+
state_exclude: [OrderState.CANCELED],
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!data.length && isLoading) {
|
|
32
|
+
setIsNewSearchLoading(true);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (isLoading) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (isNewSearchLoading) {
|
|
40
|
+
setIsNewSearchLoading(false);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (isNewSearchLoading || data.length > orderAndEnrollmentList?.length) {
|
|
44
|
+
setOrderAndEnrollmentList(data as (CredentialOrder | Enrollment)[]);
|
|
45
|
+
setCount(currentCount);
|
|
46
|
+
}
|
|
47
|
+
}, [data.length, isLoading, isNewSearchLoading, query]);
|
|
48
|
+
|
|
49
|
+
const submitSearch = (newQuery: Nullable<string>) => {
|
|
50
|
+
if (newQuery === null) {
|
|
51
|
+
searchParams.delete('query');
|
|
52
|
+
} else {
|
|
53
|
+
searchParams.set('query', newQuery);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
setSearchParams(searchParams);
|
|
57
|
+
if (!newQuery) {
|
|
58
|
+
setCount(undefined);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
query,
|
|
64
|
+
submitSearch,
|
|
65
|
+
data: orderAndEnrollmentList,
|
|
66
|
+
isNewSearchLoading,
|
|
67
|
+
isLoadingMore: isLoading && !isNewSearchLoading,
|
|
68
|
+
next,
|
|
69
|
+
hasMore,
|
|
70
|
+
count,
|
|
71
|
+
error,
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
export default useLearnerCoursesSearch;
|
package/js/hooks/useOrders.ts
CHANGED
|
@@ -119,10 +119,12 @@ describe('<DashboardCourses/>', () => {
|
|
|
119
119
|
expect(await screen.queryByRole('button', { name: 'Load more' })).not.toBeInTheDocument();
|
|
120
120
|
await expectNoBannerInfo('You have no enrollments nor orders yet');
|
|
121
121
|
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
act(() => {
|
|
123
|
+
ordersDeferred.resolve({ results: [], next: null, previous: null, count: 0 });
|
|
124
|
+
enrollmentsDeferred.resolve({ results: [], next: null, previous: null, count: 0 });
|
|
125
|
+
});
|
|
124
126
|
|
|
125
|
-
await expectNoSpinner('Loading orders and enrollments...');
|
|
127
|
+
// await expectNoSpinner('Loading orders and enrollments...');
|
|
126
128
|
await expectBannerInfo('You have no enrollments nor orders yet.');
|
|
127
129
|
expect(screen.queryByRole('button', { name: 'Load more' })).not.toBeInTheDocument();
|
|
128
130
|
});
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|
2
2
|
import { useRef } from 'react';
|
|
3
3
|
import { Button } from '@openfun/cunningham-react';
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
isEnrollment,
|
|
7
|
-
useOrdersEnrollments,
|
|
8
|
-
} from 'pages/DashboardCourses/useOrdersEnrollments';
|
|
4
|
+
import classNames from 'classnames';
|
|
5
|
+
import { isCredentialOrder, isEnrollment } from 'pages/DashboardCourses/useOrdersEnrollments';
|
|
9
6
|
import { Spinner } from 'components/Spinner';
|
|
10
7
|
import { DashboardItemEnrollment } from 'widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment';
|
|
11
8
|
import { DashboardItemOrder } from 'widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder';
|
|
12
9
|
import Banner, { BannerType } from 'components/Banner';
|
|
13
10
|
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
|
|
14
|
-
import
|
|
11
|
+
import SearchBar from 'widgets/Dashboard/components/SearchBar';
|
|
12
|
+
import SearchResultsCount from 'widgets/Dashboard/components/SearchResultsCount';
|
|
13
|
+
import useLearnerCoursesSearch from 'hooks/useLearnerCoursesSearch';
|
|
15
14
|
|
|
16
15
|
const messages = defineMessages({
|
|
17
16
|
loading: {
|
|
@@ -33,9 +32,17 @@ const messages = defineMessages({
|
|
|
33
32
|
|
|
34
33
|
export const DashboardCourses = () => {
|
|
35
34
|
const intl = useIntl();
|
|
36
|
-
const {
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
const {
|
|
36
|
+
data,
|
|
37
|
+
query,
|
|
38
|
+
isLoadingMore,
|
|
39
|
+
isNewSearchLoading,
|
|
40
|
+
next,
|
|
41
|
+
hasMore,
|
|
42
|
+
submitSearch,
|
|
43
|
+
count,
|
|
44
|
+
error,
|
|
45
|
+
} = useLearnerCoursesSearch();
|
|
39
46
|
|
|
40
47
|
const loadMoreButtonRef = useRef<HTMLButtonElement & HTMLAnchorElement>(null);
|
|
41
48
|
useIntersectionObserver({
|
|
@@ -50,12 +57,20 @@ export const DashboardCourses = () => {
|
|
|
50
57
|
<Banner message={error} type={BannerType.ERROR} />
|
|
51
58
|
) : (
|
|
52
59
|
<>
|
|
53
|
-
|
|
60
|
+
<SearchBar.Container>
|
|
61
|
+
<SearchBar onSubmit={submitSearch} />
|
|
62
|
+
<SearchResultsCount nbResults={count} />
|
|
63
|
+
</SearchBar.Container>
|
|
64
|
+
{count === 0 && !query && (
|
|
54
65
|
<div className="dashboard__courses__empty">
|
|
55
66
|
<Banner message={intl.formatMessage(messages.emptyList)} />
|
|
56
67
|
</div>
|
|
57
68
|
)}
|
|
58
|
-
<div
|
|
69
|
+
<div
|
|
70
|
+
className={classNames('dashboard__courses__list', {
|
|
71
|
+
'dashboard-course-list--fade': isNewSearchLoading,
|
|
72
|
+
})}
|
|
73
|
+
>
|
|
59
74
|
{data.map((datum) => (
|
|
60
75
|
<div
|
|
61
76
|
key={datum.id}
|
|
@@ -67,7 +82,7 @@ export const DashboardCourses = () => {
|
|
|
67
82
|
</div>
|
|
68
83
|
))}
|
|
69
84
|
</div>
|
|
70
|
-
{
|
|
85
|
+
{(isLoadingMore || isNewSearchLoading) && (
|
|
71
86
|
<Spinner aria-labelledby="loading-orders-enrollments">
|
|
72
87
|
<span id="loading-orders-enrollments">
|
|
73
88
|
<FormattedMessage {...messages.loading} />
|
|
@@ -77,7 +92,7 @@ export const DashboardCourses = () => {
|
|
|
77
92
|
{hasMore && (
|
|
78
93
|
<Button
|
|
79
94
|
onClick={() => next()}
|
|
80
|
-
disabled={
|
|
95
|
+
disabled={isLoadingMore}
|
|
81
96
|
ref={loadMoreButtonRef}
|
|
82
97
|
color="tertiary"
|
|
83
98
|
>
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { defineMessages } from 'react-intl';
|
|
2
2
|
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
3
|
-
import {
|
|
4
|
-
Enrollment,
|
|
5
|
-
PaginatedResourceQuery,
|
|
6
|
-
CredentialOrder,
|
|
7
|
-
CertificateOrder,
|
|
8
|
-
} from 'types/Joanie';
|
|
3
|
+
import { Enrollment, CredentialOrder, CertificateOrder, EnrollmentsQuery } from 'types/Joanie';
|
|
9
4
|
import useUnionResource, { ResourceUnionPaginationProps } from 'hooks/useUnionResource';
|
|
10
5
|
import { PER_PAGE } from 'settings';
|
|
11
6
|
import { OrderResourcesQuery } from 'hooks/useOrders';
|
|
@@ -41,29 +36,32 @@ const messages = defineMessages({
|
|
|
41
36
|
|
|
42
37
|
interface UseOrdersEnrollmentsProps extends ResourceUnionPaginationProps {
|
|
43
38
|
orderFilters?: OrderResourcesQuery;
|
|
39
|
+
query?: string;
|
|
44
40
|
}
|
|
45
41
|
|
|
46
42
|
export const useOrdersEnrollments = ({
|
|
47
43
|
perPage = PER_PAGE.useOrdersEnrollments,
|
|
44
|
+
query,
|
|
48
45
|
orderFilters = {},
|
|
49
46
|
}: UseOrdersEnrollmentsProps = {}) => {
|
|
50
47
|
const api = useJoanieApi();
|
|
51
48
|
return useUnionResource<
|
|
52
49
|
CredentialOrder | CertificateOrder,
|
|
53
50
|
Enrollment,
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
OrderResourcesQuery,
|
|
52
|
+
EnrollmentsQuery
|
|
56
53
|
>({
|
|
57
54
|
queryAConfig: {
|
|
58
55
|
queryKey: ['user', 'orders'],
|
|
59
56
|
fn: api.user.orders.get,
|
|
60
|
-
filters: orderFilters,
|
|
57
|
+
filters: { ...orderFilters, query },
|
|
61
58
|
},
|
|
62
59
|
queryBConfig: {
|
|
63
60
|
queryKey: ['user', 'enrollments'],
|
|
64
61
|
fn: api.user.enrollments.get,
|
|
65
62
|
filters: {
|
|
66
63
|
was_created_by_order: false,
|
|
64
|
+
query,
|
|
67
65
|
},
|
|
68
66
|
},
|
|
69
67
|
perPage,
|
|
@@ -42,8 +42,10 @@ export const TeacherDashboardCoursesLoader = () => {
|
|
|
42
42
|
</h1>
|
|
43
43
|
</div>
|
|
44
44
|
|
|
45
|
-
<SearchBar
|
|
46
|
-
|
|
45
|
+
<SearchBar.Container>
|
|
46
|
+
<SearchBar onSubmit={submitSearch} />
|
|
47
|
+
<SearchResultsCount nbResults={count} />
|
|
48
|
+
</SearchBar.Container>
|
|
47
49
|
</div>
|
|
48
50
|
<div className="teacher-courses-page">
|
|
49
51
|
<TeacherDashboardCourseList
|
package/js/types/Joanie.ts
CHANGED
|
@@ -460,6 +460,7 @@ export interface PaginatedResourceQuery extends ResourcesQuery {
|
|
|
460
460
|
export interface EnrollmentsQuery extends PaginatedResourceQuery {
|
|
461
461
|
course_run_id?: CourseRun['id'];
|
|
462
462
|
was_created_by_order?: boolean;
|
|
463
|
+
query?: string;
|
|
463
464
|
}
|
|
464
465
|
|
|
465
466
|
interface EnrollmentCreationPayload {
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
.dashboard-search-bar {
|
|
2
|
+
$spacing: rem-calc(5px);
|
|
3
|
+
|
|
2
4
|
display: flex;
|
|
3
5
|
flex-direction: row;
|
|
4
6
|
align-items: center;
|
|
5
7
|
gap: rem-calc(8px);
|
|
6
|
-
margin-bottom:
|
|
8
|
+
margin-bottom: $spacing;
|
|
7
9
|
|
|
8
10
|
// TODO(rlecellier): cunningham should allow className on container
|
|
9
11
|
.c__field {
|
|
10
12
|
flex: 1 1 auto;
|
|
11
13
|
}
|
|
14
|
+
|
|
15
|
+
&__container {
|
|
16
|
+
padding-bottom: $spacing;
|
|
17
|
+
}
|
|
12
18
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Button, Input } from '@openfun/cunningham-react';
|
|
2
|
-
import { MouseEvent, useRef, useState } from 'react';
|
|
2
|
+
import { MouseEvent, PropsWithChildren, useRef, useState } from 'react';
|
|
3
3
|
import { defineMessages, useIntl } from 'react-intl';
|
|
4
4
|
import { useSearchParams } from 'react-router-dom';
|
|
5
5
|
import { Nullable } from 'types/utils';
|
|
@@ -81,4 +81,8 @@ const SearchBar = ({ onSubmit }: SearchBarProps) => {
|
|
|
81
81
|
);
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
+
SearchBar.Container = ({ children }: PropsWithChildren) => {
|
|
85
|
+
return <div className="dashboard-search-bar__container">{children}</div>;
|
|
86
|
+
};
|
|
87
|
+
|
|
84
88
|
export default SearchBar;
|