richie-education 2.29.3-dev48 → 2.29.3-dev52

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.
@@ -21,7 +21,7 @@ const useCourseRunOrder = (courseRun: CourseRun) => {
21
21
  product_id: resourceLinkResources?.product,
22
22
  });
23
23
 
24
- return { item: orders.length > 0 ? orders[0] : undefined, states: { fetching, isFetched } };
24
+ return { item: orders?.[0], states: { fetching, isFetched } };
25
25
  };
26
26
 
27
27
  export default useCourseRunOrder;
@@ -145,7 +145,9 @@ describe('useCreditCards', () => {
145
145
  responseDeferred.resolve({});
146
146
  });
147
147
 
148
- expect(result.current.states.tokenizing).toBe(false);
148
+ await waitFor(() => {
149
+ expect(result.current.states.tokenizing).toBe(false);
150
+ });
149
151
  expect(result.current.states.isPending).toBe(false);
150
152
  expect(result.current.states.error).toBe(undefined);
151
153
  });
@@ -995,10 +995,7 @@ describe('<DashboardItemOrder/>', () => {
995
995
  order.payment_schedule![1].state = PaymentScheduleState.REFUSED;
996
996
 
997
997
  const formatPrice = (price: number, currency: string) =>
998
- new Intl.NumberFormat('en', {
999
- currency,
1000
- style: 'currency',
1001
- }).format(price);
998
+ new Intl.NumberFormat('en', { currency, style: 'currency' }).format(price);
1002
999
 
1003
1000
  const { product } = mockCourseProductWithOrder(order);
1004
1001
  fetchMock.get(
@@ -1038,12 +1035,10 @@ describe('<DashboardItemOrder/>', () => {
1038
1035
 
1039
1036
  // Click on pay button.
1040
1037
  const payButton = screen.getByTestId('order-payment-retry-modal-submit-button');
1041
- expect(payButton.innerHTML.replace('&nbsp;', ' ')).toEqual(
1038
+ expect(payButton).toHaveTextContent(
1042
1039
  'Pay ' +
1043
- formatPrice(failedInstallment.amount, failedInstallment.currency).replace(
1044
- /(\u202F|\u00a0)/g,
1045
- ' ',
1046
- ),
1040
+ formatPrice(failedInstallment.amount, failedInstallment.currency).replaceAll(/\s/g, ' '),
1041
+ { normalizeWhitespace: true },
1047
1042
  );
1048
1043
  await user.click(payButton);
1049
1044
  // Pay via mocked payment interface
@@ -1054,7 +1049,7 @@ describe('<DashboardItemOrder/>', () => {
1054
1049
  expect(screen.queryByText('Retry payment')).not.toBeInTheDocument();
1055
1050
 
1056
1051
  // Success modal is shown, close it.
1057
- screen.getByText('Payment successful');
1052
+ await screen.findByText('Payment successful');
1058
1053
  screen.getByText('The payment was successful');
1059
1054
  const okButton = screen.getByRole('button', { name: 'Ok' });
1060
1055
  await user.click(okButton);
@@ -1,5 +1,6 @@
1
1
  import { screen, waitFor } from '@testing-library/react';
2
2
  import fetchMock from 'fetch-mock';
3
+ import { faker } from '@faker-js/faker';
3
4
  import {
4
5
  CourseRunFactory,
5
6
  RichieContextFactory as mockRichieContextFactory,
@@ -11,12 +12,12 @@ import { render } from 'utils/test/render';
11
12
  import {
12
13
  CourseLightFactory,
13
14
  CredentialOrderFactory,
14
- EnrollmentFactory,
15
15
  ProductFactory,
16
16
  } from 'utils/test/factories/joanie';
17
17
  import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
18
18
  import { mockPaginatedResponse } from 'utils/test/mockPaginatedResponse';
19
19
  import { expectNoSpinner } from 'utils/test/expectSpinner';
20
+ import { ACTIVE_ORDER_STATES, PURCHASABLE_ORDER_STATES } from 'types/Joanie';
20
21
  import CourseRunItemWithEnrollment from '.';
21
22
 
22
23
  jest.mock('utils/context', () => ({
@@ -61,10 +62,9 @@ describe("CourseRunItemWithEnrollment for joanie's product's course run", () =>
61
62
  expect(screen.queryByRole('link')).not.toBeInTheDocument();
62
63
  });
63
64
 
64
- it('should not render enrollment information when user is not enrolled to the course run', async () => {
65
+ it('should not render enrollment information when user has no order for the product', async () => {
65
66
  const course = CourseLightFactory().one();
66
67
  const product = ProductFactory().one();
67
- const order = CredentialOrderFactory().one();
68
68
  const user = UserFactory().one();
69
69
  const courseRun = CourseRunFactory({
70
70
  title: 'run',
@@ -73,6 +73,39 @@ describe("CourseRunItemWithEnrollment for joanie's product's course run", () =>
73
73
  resource_link: `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}`,
74
74
  }).one();
75
75
 
76
+ fetchMock.get(
77
+ `https://joanie.endpoint/api/v1.0/orders/?course_code=${course.code}&product_id=${product.id}`,
78
+ mockPaginatedResponse([], 0, false),
79
+ );
80
+
81
+ render(<CourseRunItemWithEnrollment item={courseRun} />, {
82
+ wrapper: BaseJoanieAppWrapper,
83
+ queryOptions: { client: createTestQueryClient({ user }) },
84
+ });
85
+ await expectNoSpinner();
86
+
87
+ // Only dates should be displayed.
88
+ screen.getByText('Run, from Jan 01, 2023 to Dec 31, 2023');
89
+ expect(fetchMock.called()).toBe(true);
90
+ const link = screen.queryByTitle('Go to course') as HTMLAnchorElement;
91
+ expect(link).not.toBeInTheDocument();
92
+ expect(screen.queryByLabelText('You are enrolled in this course run')).not.toBeInTheDocument();
93
+ });
94
+
95
+ it('should not render enrollment information when user has no active order for the product', async () => {
96
+ const course = CourseLightFactory().one();
97
+ const product = ProductFactory().one();
98
+ const user = UserFactory().one();
99
+ const order = CredentialOrderFactory({
100
+ state: faker.helpers.arrayElement(PURCHASABLE_ORDER_STATES),
101
+ }).one();
102
+ const courseRun = CourseRunFactory({
103
+ title: 'run',
104
+ start: new Date('2023-01-01').toISOString(),
105
+ end: new Date('2023-12-31').toISOString(),
106
+ resource_link: `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}`,
107
+ }).one();
108
+
76
109
  fetchMock.get(
77
110
  `https://joanie.endpoint/api/v1.0/orders/?course_code=${course.code}&product_id=${product.id}`,
78
111
  mockPaginatedResponse([order], 1, false),
@@ -84,20 +117,19 @@ describe("CourseRunItemWithEnrollment for joanie's product's course run", () =>
84
117
  });
85
118
  await expectNoSpinner();
86
119
 
87
- // Only dates should have been displayed.
120
+ // Only dates should be displayed.
88
121
  screen.getByText('Run, from Jan 01, 2023 to Dec 31, 2023');
89
122
  expect(fetchMock.called()).toBe(true);
90
123
  const link = screen.queryByTitle('Go to course') as HTMLAnchorElement;
91
- expect(link).toBeInTheDocument();
92
- expect(link.href).toBe(`https://localhost/en/dashboard/courses/orders/${order.id}`);
124
+ expect(link).not.toBeInTheDocument();
93
125
  expect(screen.queryByLabelText('You are enrolled in this course run')).not.toBeInTheDocument();
94
126
  });
95
127
 
96
- it('should render enrollment information when user is enrolled to the course run', async () => {
128
+ it('should render enrollment information when user has an active order for the product', async () => {
97
129
  const course = CourseLightFactory().one();
98
130
  const product = ProductFactory().one();
99
131
  const order = CredentialOrderFactory({
100
- target_enrollments: EnrollmentFactory({ is_active: true }).many(1),
132
+ state: faker.helpers.arrayElement(ACTIVE_ORDER_STATES),
101
133
  }).one();
102
134
  const user = UserFactory().one();
103
135
  const courseRun = CourseRunFactory({
@@ -121,7 +153,6 @@ describe("CourseRunItemWithEnrollment for joanie's product's course run", () =>
121
153
  expect(screen.queryByText('loading...')).not.toBeInTheDocument();
122
154
  });
123
155
 
124
- // Only dates should have been displayed.
125
156
  screen.getByText('Run, from Jan 01, 2023 to Dec 31, 2023');
126
157
  expect(fetchMock.called()).toBe(true);
127
158
 
@@ -8,6 +8,8 @@ import { getDashboardBasename } from 'widgets/Dashboard/hooks/useDashboardRouter
8
8
  import { LearnerDashboardPaths } from 'widgets/Dashboard/utils/learnerRoutesPaths';
9
9
  import useCourseRunOrder from 'hooks/useCourseRunOrder';
10
10
  import { Spinner } from 'components/Spinner';
11
+ import { OrderHelper } from 'utils/OrderHelper';
12
+ import { extractResourceMetadata } from 'api/lms/joanie';
11
13
 
12
14
  const messages = defineMessages({
13
15
  goToCourse: {
@@ -33,6 +35,8 @@ type Props = {
33
35
 
34
36
  const CourseRunItemWithEnrollment = ({ item }: Props) => {
35
37
  const intl = useIntl();
38
+ const resourceLinkResources = extractResourceMetadata(item.resource_link);
39
+ const isProduct = !!(resourceLinkResources?.course && resourceLinkResources?.product);
36
40
  const {
37
41
  item: order,
38
42
  states: { isFetched },
@@ -43,14 +47,12 @@ const CourseRunItemWithEnrollment = ({ item }: Props) => {
43
47
 
44
48
  const { enrollmentIsActive: courseRunEnrollmentIsActive } = useCourseEnrollment(
45
49
  item.resource_link,
46
- isFetched && !order,
50
+ !isProduct,
47
51
  );
48
52
 
49
- // user is enroll to a product if any of the product's target course have a active enrollment
50
- const productEnrollmentIsActive = order?.target_enrollments.some((enrollment) => {
51
- return enrollment.is_active;
52
- });
53
- const enrollmentIsActive = !!(courseRunEnrollmentIsActive || productEnrollmentIsActive);
53
+ // user is enrolled to a product if there is an active order for the product
54
+ const orderIsActive = OrderHelper.isActive(order);
55
+ const enrollmentIsActive = !!(courseRunEnrollmentIsActive || orderIsActive);
54
56
 
55
57
  if (!isFetched) {
56
58
  return <Spinner />;
@@ -58,7 +60,7 @@ const CourseRunItemWithEnrollment = ({ item }: Props) => {
58
60
 
59
61
  return (
60
62
  <>
61
- {enrollmentIsActive || !!order ? (
63
+ {enrollmentIsActive ? (
62
64
  // eslint-disable-next-line jsx-a11y/control-has-associated-label
63
65
  <a href={courseUrl} title={intl.formatMessage(messages.goToCourse)}>
64
66
  <CourseRunItem item={item} />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "2.29.3-dev48",
3
+ "version": "2.29.3-dev52",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {
@@ -51,7 +51,7 @@
51
51
  "@lyracom/embedded-form-glue": "1.4.2",
52
52
  "@openfun/cunningham-react": "2.9.4",
53
53
  "@openfun/cunningham-tokens": "2.1.1",
54
- "@sentry/browser": "8.32.0",
54
+ "@sentry/browser": "8.33.0",
55
55
  "@sentry/types": "8.32.0",
56
56
  "@storybook/addon-actions": "8.3.4",
57
57
  "@storybook/addon-essentials": "8.3.4",
@@ -128,6 +128,18 @@ $r-theme: (
128
128
  cta-color: r-color('firebrick6'),
129
129
  cta-border: r-color('white'),
130
130
  ),
131
+ compacted-banner: (
132
+ title-color: r-color('charcoal'),
133
+ title-alt-color: r-color('firebrick6'),
134
+ content-color: r-color('black'),
135
+ search-input-background: rgba(r-color('white'), 0.7),
136
+ search-btn-background: r-color('firebrick6'),
137
+ search-icon-fill: r-color('white'),
138
+ cta-variant-from: rgba(r-color('white'), 0.8),
139
+ cta-variant-to: rgba(r-color('white'), 0.8),
140
+ cta-color: r-color('firebrick6'),
141
+ cta-border: r-color('white'),
142
+ ),
131
143
  section-plugin: (
132
144
  title-emphased-color: r-color('firebrick6'),
133
145
  ),
@@ -28,6 +28,20 @@ body {
28
28
  background-size: 100% 100%;
29
29
  }
30
30
  }
31
+
32
+ // Apply the top padding behavior with topbar over mode only on the first child which
33
+ // is the only variant that can be exposed to issue with topbar over
34
+ .compacted-banner:first-child {
35
+ .compacted-banner__inner {
36
+ @include media-breakpoint-up(lg) {
37
+ @if $r-topbar-height {
38
+ padding: $r-topbar-height 0 1rem;
39
+ } @else {
40
+ padding: 1rem 0 1rem;
41
+ }
42
+ }
43
+ }
44
+ }
31
45
  }
32
46
 
33
47
  // Container relative to some modal components
@@ -61,6 +61,7 @@
61
61
  @import './templates/courses/plugins/licence_plugin';
62
62
  @import './templates/richie/section/section';
63
63
  @import './templates/richie/large_banner/large_banner';
64
+ @import './templates/richie/large_banner/compacted_banner';
64
65
  @import './templates/richie/nesteditem/nesteditem';
65
66
  @import './templates/richie/glimpse/glimpse';
66
67
 
@@ -0,0 +1,149 @@
1
+ // A compact banner without image contents
2
+ // Opposed to 'hero-intro' this variant delegates the top padding for topbar in 'over'
3
+ // mode in content component
4
+ //
5
+ .compacted-banner {
6
+ position: relative;
7
+ padding: 0;
8
+
9
+ &__inner {
10
+ padding: 1rem 0;
11
+
12
+ @include media-breakpoint-up(lg) {
13
+ display: flex;
14
+ }
15
+ }
16
+
17
+ &__body {
18
+ @include make-container();
19
+ @include make-container-max-widths();
20
+ padding: 1rem;
21
+ text-align: center;
22
+
23
+ @include media-breakpoint-up(lg) {
24
+ display: flex;
25
+ padding: 2rem;
26
+ flex-direction: column;
27
+ justify-content: space-between;
28
+ }
29
+ }
30
+
31
+ // NOTE: Force disabling of hardcoded hero title class from some already save
32
+ // contents. Sadly we can not disable the huge font size
33
+ .hero-intro__title {
34
+ margin-bottom: 1rem !important;
35
+ width: auto;
36
+ color: inherit;
37
+
38
+ strong {
39
+ color: inherit;
40
+ font-weight: inherit;
41
+ }
42
+ }
43
+
44
+ // NOTE: Apply the color+weight behavior with 'strong' element alike in
45
+ // 'hero-intro__title' but naturally on title elements without any class
46
+ h1,
47
+ h2,
48
+ h3,
49
+ h4,
50
+ h5,
51
+ h6 {
52
+ color: r-theme-val(compacted-banner, title-color);
53
+ margin-bottom: 0.5em;
54
+
55
+ strong {
56
+ color: r-theme-val(compacted-banner, title-alt-color);
57
+ font-weight: inherit;
58
+ }
59
+ }
60
+
61
+ // NOTE: Implement again the 'hero-intro__title' equivalent behavior
62
+ &__title {
63
+ @include responsive-spacer('margin-bottom', 1);
64
+ @include font-size($extra-font-size);
65
+ color: r-theme-val(compacted-banner, title-color);
66
+
67
+ strong {
68
+ color: r-theme-val(compacted-banner, title-alt-color);
69
+ font-weight: inherit;
70
+ }
71
+ }
72
+
73
+ &__content {
74
+ @include font-size($h5-font-size);
75
+ color: r-theme-val(compacted-banner, content-color);
76
+ }
77
+
78
+ &__search {
79
+ display: flex;
80
+ flex-wrap: wrap;
81
+ align-items: center;
82
+ justify-content: center;
83
+
84
+ .richie-react--root-search-suggest-field {
85
+ @include sv-flex(1, 0, 100%);
86
+ position: relative;
87
+
88
+ @include media-breakpoint-up(lg) {
89
+ @include sv-flex(1, 0, 320px);
90
+ }
91
+
92
+ .react-autosuggest__container {
93
+ margin-bottom: 0;
94
+ }
95
+
96
+ input {
97
+ background: r-theme-val(compacted-banner, search-input-background);
98
+ }
99
+
100
+ .search-input {
101
+ &__btn {
102
+ background: r-theme-val(compacted-banner, search-btn-background);
103
+ border-top-right-radius: 3rem;
104
+ border-bottom-right-radius: 3rem;
105
+
106
+ &__icon {
107
+ fill: r-theme-val(compacted-banner, search-icon-fill);
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ &__cta {
115
+ @include sv-flex(0, 0, auto);
116
+ @include button-size(
117
+ $btn-padding-y,
118
+ $btn-padding-x,
119
+ $btn-font-size,
120
+ $btn-line-height,
121
+ $btn-border-radius
122
+ );
123
+ @include button-variant(
124
+ r-theme-val(compacted-banner, cta-variant-from),
125
+ r-theme-val(compacted-banner, cta-variant-to)
126
+ );
127
+ margin: 1.2rem 0 0;
128
+ font-size: $font-size-base;
129
+ color: r-theme-val(compacted-banner, cta-color);
130
+ border-radius: 2rem;
131
+ @if r-theme-val(compacted-banner, cta-border) {
132
+ border: 1px solid r-theme-val(compacted-banner, cta-border);
133
+ }
134
+
135
+ @include media-breakpoint-up(lg) {
136
+ margin-top: 0;
137
+ @include responsive-spacer('margin-left', 0, $breakpoints: ('lg': 3));
138
+ }
139
+
140
+ &:after {
141
+ content: '→';
142
+ margin-left: 1rem;
143
+ }
144
+
145
+ &:hover {
146
+ text-decoration: none;
147
+ }
148
+ }
149
+ }