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.
@@ -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;
@@ -22,6 +22,7 @@ export type OrderResourcesQuery = PaginatedResourceQuery & {
22
22
  state_exclude?: OrderState[];
23
23
  product_type?: ProductType[];
24
24
  product_type_exclude?: ProductType[];
25
+ query?: string;
25
26
  };
26
27
 
27
28
  const messages = defineMessages({
@@ -13,7 +13,6 @@
13
13
  .dashboard__courses {
14
14
  display: flex;
15
15
  flex-direction: column;
16
- gap: 1rem;
17
16
 
18
17
  &__list {
19
18
  gap: 1rem;
@@ -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
- ordersDeferred.resolve({ results: [], next: null, previous: null, count: 0 });
123
- enrollmentsDeferred.resolve({ results: [], next: null, previous: null, count: 0 });
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
- isCredentialOrder,
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 { OrderState, ProductType } from 'types/Joanie';
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 { next, data, hasMore, error, isLoading, count } = useOrdersEnrollments({
37
- orderFilters: { product_type: [ProductType.CREDENTIAL], state_exclude: [OrderState.CANCELED] },
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
- {count === 0 && (
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 className="dashboard__courses__list">
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
- {isLoading && (
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={isLoading}
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
- PaginatedResourceQuery,
55
- { was_created_by_order: boolean } & PaginatedResourceQuery
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 onSubmit={submitSearch} />
46
- <SearchResultsCount nbResults={count} />
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
@@ -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: rem-calc(5px);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "2.25.0-b2.dev146",
3
+ "version": "2.25.0-b2.dev148",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {