richie-education 2.30.1-dev14 → 2.30.1-dev18

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.
Files changed (23) hide show
  1. package/js/components/CourseGlimpse/utils.ts +3 -3
  2. package/js/components/CourseGlimpseList/utils.ts +2 -2
  3. package/js/components/PurchaseButton/index.spec.tsx +20 -2
  4. package/js/components/PurchaseButton/index.tsx +3 -0
  5. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +1 -1
  6. package/js/components/SaleTunnel/WithdrawRightCheckbox/index.tsx +4 -4
  7. package/js/components/SaleTunnel/index.credential.spec.tsx +2 -2
  8. package/js/components/SaleTunnel/index.full-process.spec.tsx +6 -2
  9. package/js/components/SaleTunnel/index.spec.tsx +10 -10
  10. package/js/components/SaleTunnel/index.stories.tsx +1 -0
  11. package/js/components/SaleTunnel/index.tsx +1 -1
  12. package/js/components/TeacherDashboardCourseList/index.tsx +2 -2
  13. package/js/hooks/useCourseProductUnion/index.ts +2 -1
  14. package/js/hooks/useTeacherCoursesSearch/index.tsx +2 -2
  15. package/js/types/Joanie.ts +8 -5
  16. package/js/utils/test/factories/joanie.ts +1 -1
  17. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +2 -1
  18. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +29 -5
  19. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +7 -1
  20. package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +1 -0
  21. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +2 -0
  22. package/package.json +3 -2
  23. package/scss/components/templates/richie/_multiple-columns.scss +8 -5
@@ -3,14 +3,14 @@ import { generatePath } from 'react-router-dom';
3
3
  import { Course as RichieCourse, isRichieCourse } from 'types/Course';
4
4
  import {
5
5
  CourseListItem as JoanieCourse,
6
- CourseProductRelation,
6
+ CourseProductRelationLight,
7
7
  isCourseProductRelation,
8
8
  } from 'types/Joanie';
9
9
  import { TeacherDashboardPaths } from 'widgets/Dashboard/utils/teacherDashboardPaths';
10
10
  import { CourseGlimpseCourse } from '.';
11
11
 
12
12
  const getCourseGlimpsePropsFromCourseProductRelation = (
13
- courseProductRelation: CourseProductRelation,
13
+ courseProductRelation: CourseProductRelationLight,
14
14
  intl: IntlShape,
15
15
  organizationId?: string,
16
16
  ): CourseGlimpseCourse => {
@@ -95,7 +95,7 @@ const getCourseGlimpsePropsFromJoanieCourse = (
95
95
  };
96
96
 
97
97
  export const getCourseGlimpseProps = (
98
- course: RichieCourse | (JoanieCourse | CourseProductRelation),
98
+ course: RichieCourse | (JoanieCourse | CourseProductRelationLight),
99
99
  intl?: IntlShape,
100
100
  organizationId?: string,
101
101
  ): CourseGlimpseCourse => {
@@ -1,10 +1,10 @@
1
1
  import { IntlShape } from 'react-intl';
2
- import { CourseProductRelation, CourseListItem as JoanieCourse } from 'types/Joanie';
2
+ import { CourseProductRelationLight, CourseListItem as JoanieCourse } from 'types/Joanie';
3
3
  import { Course as RichieCourse } from 'types/Course';
4
4
  import { CourseGlimpseCourse, getCourseGlimpseProps } from 'components/CourseGlimpse';
5
5
 
6
6
  export const getCourseGlimpseListProps = (
7
- courses: RichieCourse[] | (JoanieCourse | CourseProductRelation)[],
7
+ courses: RichieCourse[] | (JoanieCourse | CourseProductRelationLight)[],
8
8
  intl?: IntlShape,
9
9
  organizationId?: string,
10
10
  ): CourseGlimpseCourse[] => {
@@ -91,6 +91,7 @@ describe('PurchaseButton', () => {
91
91
  product={product}
92
92
  disabled={false}
93
93
  course={PacedCourseFactory({ code: '00000' }).one()}
94
+ isWithdrawable={true}
94
95
  />
95
96
  </Wrapper>,
96
97
  );
@@ -122,6 +123,7 @@ describe('PurchaseButton', () => {
122
123
  product={product}
123
124
  disabled={false}
124
125
  course={PacedCourseFactory({ code: courseCode }).one()}
126
+ isWithdrawable={true}
125
127
  />
126
128
  </Wrapper>,
127
129
  );
@@ -164,6 +166,7 @@ describe('PurchaseButton', () => {
164
166
  product={product}
165
167
  disabled={false}
166
168
  course={PacedCourseFactory({ code: courseCode }).one()}
169
+ isWithdrawable={true}
167
170
  />
168
171
  </Wrapper>,
169
172
  );
@@ -207,6 +210,7 @@ describe('PurchaseButton', () => {
207
210
  product={product}
208
211
  disabled={false}
209
212
  course={PacedCourseFactory({ code: courseCode }).one()}
213
+ isWithdrawable={true}
210
214
  />
211
215
  </Wrapper>,
212
216
  );
@@ -243,6 +247,7 @@ describe('PurchaseButton', () => {
243
247
  product={product}
244
248
  disabled={false}
245
249
  course={PacedCourseFactory({ code: courseCode }).one()}
250
+ isWithdrawable={true}
246
251
  />
247
252
  </Wrapper>,
248
253
  );
@@ -284,6 +289,7 @@ describe('PurchaseButton', () => {
284
289
  product={product}
285
290
  disabled={false}
286
291
  course={PacedCourseFactory({ code: courseCode }).one()}
292
+ isWithdrawable={true}
287
293
  />
288
294
  </Wrapper>,
289
295
  );
@@ -333,7 +339,12 @@ describe('PurchaseButton', () => {
333
339
 
334
340
  render(
335
341
  <Wrapper client={createTestQueryClient({ user: true })}>
336
- <PurchaseButton product={product} disabled={false} enrollment={enrollment} />
342
+ <PurchaseButton
343
+ product={product}
344
+ disabled={false}
345
+ enrollment={enrollment}
346
+ isWithdrawable={true}
347
+ />
337
348
  </Wrapper>,
338
349
  );
339
350
 
@@ -390,7 +401,12 @@ describe('PurchaseButton', () => {
390
401
 
391
402
  render(
392
403
  <Wrapper client={createTestQueryClient({ user: true })}>
393
- <PurchaseButton product={product} disabled={false} enrollment={enrollment} />
404
+ <PurchaseButton
405
+ product={product}
406
+ disabled={false}
407
+ enrollment={enrollment}
408
+ isWithdrawable={true}
409
+ />
394
410
  </Wrapper>,
395
411
  );
396
412
 
@@ -428,6 +444,7 @@ describe('PurchaseButton', () => {
428
444
  product={product}
429
445
  disabled={false}
430
446
  course={PacedCourseFactory({ code: courseCode }).one()}
447
+ isWithdrawable={true}
431
448
  />
432
449
  </Wrapper>,
433
450
  );
@@ -462,6 +479,7 @@ describe('PurchaseButton', () => {
462
479
  product={product}
463
480
  disabled={true}
464
481
  course={PacedCourseFactory({ code: courseCode }).one()}
482
+ isWithdrawable={true}
465
483
  />
466
484
  </Wrapper>,
467
485
  );
@@ -43,6 +43,7 @@ const messages = defineMessages({
43
43
  interface PurchaseButtonPropsBase {
44
44
  product: Joanie.CredentialProduct | Joanie.CertificateProduct;
45
45
  orderGroup?: Joanie.OrderGroup;
46
+ isWithdrawable: boolean;
46
47
  disabled?: boolean;
47
48
  className?: string;
48
49
  buttonProps?: ButtonProps;
@@ -67,6 +68,7 @@ const PurchaseButton = ({
67
68
  course,
68
69
  enrollment,
69
70
  orderGroup,
71
+ isWithdrawable,
70
72
  organizations,
71
73
  disabled = false,
72
74
  className,
@@ -141,6 +143,7 @@ const PurchaseButton = ({
141
143
  enrollment={enrollment}
142
144
  orderGroup={orderGroup}
143
145
  course={course}
146
+ isWithdrawable={isWithdrawable}
144
147
  onFinish={onFinish}
145
148
  />
146
149
  </>
@@ -116,7 +116,7 @@ const SubscriptionButton = ({ buildOrderPayload }: Props) => {
116
116
  return;
117
117
  }
118
118
 
119
- if (!product.is_withdrawable && !hasWaivedWithdrawalRight) {
119
+ if (!saleTunnelProps.isWithdrawable && !hasWaivedWithdrawalRight) {
120
120
  handleError(SubscriptionErrorMessageId.ERROR_WITHDRAWAL_RIGHT);
121
121
  return;
122
122
  }
@@ -19,7 +19,7 @@ const messages = defineMessages({
19
19
 
20
20
  const WithdrawRightCheckbox = () => {
21
21
  const {
22
- product,
22
+ props: { isWithdrawable },
23
23
  registerSubmitCallback,
24
24
  unregisterSubmitCallback,
25
25
  hasWaivedWithdrawalRight,
@@ -27,8 +27,8 @@ const WithdrawRightCheckbox = () => {
27
27
  } = useSaleTunnelContext();
28
28
  const [hasErrorState, setHasError] = useState(false);
29
29
  const setError = useCallback(async () => {
30
- setHasError(!product.is_withdrawable && !hasWaivedWithdrawalRight);
31
- }, [hasWaivedWithdrawalRight, product.is_withdrawable]);
30
+ setHasError(!isWithdrawable && !hasWaivedWithdrawalRight);
31
+ }, [hasWaivedWithdrawalRight, isWithdrawable]);
32
32
 
33
33
  useEffect(() => {
34
34
  registerSubmitCallback('withdrawalRight', setError);
@@ -37,7 +37,7 @@ const WithdrawRightCheckbox = () => {
37
37
  };
38
38
  }, [setError]);
39
39
 
40
- if (product.is_withdrawable) return null;
40
+ if (isWithdrawable) return null;
41
41
  return (
42
42
  <section
43
43
  className="mt-t subscription-button__waiveCheckbox"
@@ -53,8 +53,8 @@ describe('SaleTunnel / Credential', () => {
53
53
  let richieUser: User;
54
54
  let openApiEdxProfile: OpenEdxApiProfile;
55
55
 
56
- const Wrapper = (props: Omit<SaleTunnelProps, 'isOpen' | 'onClose'>) => {
57
- return <SaleTunnel {...props} isOpen={true} onClose={() => {}} />;
56
+ const Wrapper = (props: Omit<SaleTunnelProps, 'isWithdrawable' | 'isOpen' | 'onClose'>) => {
57
+ return <SaleTunnel {...props} isWithdrawable={true} isOpen={true} onClose={() => {}} />;
58
58
  };
59
59
 
60
60
  setupJoanieSession();
@@ -98,8 +98,12 @@ describe('SaleTunnel', () => {
98
98
  * Initialization.
99
99
  */
100
100
  const course = PacedCourseFactory().one();
101
- const product = ProductFactory({ is_withdrawable: false }).one();
102
- const relation = CourseProductRelationFactory({ course, product }).one();
101
+ const product = ProductFactory().one();
102
+ const relation = CourseProductRelationFactory({
103
+ course,
104
+ product,
105
+ is_withdrawable: false,
106
+ }).one();
103
107
  const paymentSchedule = PaymentInstallmentFactory().many(2);
104
108
 
105
109
  fetchMock.get(
@@ -175,7 +175,7 @@ describe.each([
175
175
  overwriteRoutes: true,
176
176
  });
177
177
 
178
- render(<Wrapper product={product} />, {
178
+ render(<Wrapper product={product} isWithdrawable={true} />, {
179
179
  queryOptions: { client: createTestQueryClient({ user: richieUser }) },
180
180
  });
181
181
  nbApiCalls += 1; // useProductOrder call.
@@ -262,7 +262,7 @@ describe.each([
262
262
  overwriteRoutes: true,
263
263
  });
264
264
 
265
- render(<Wrapper product={product} />, {
265
+ render(<Wrapper product={product} isWithdrawable={true} />, {
266
266
  queryOptions: { client: createTestQueryClient({ user: richieUser }) },
267
267
  });
268
268
  nbApiCalls += 1; // useProductOrder get order with filters
@@ -337,7 +337,7 @@ describe.each([
337
337
  overwriteRoutes: true,
338
338
  });
339
339
 
340
- render(<Wrapper product={product} />, {
340
+ render(<Wrapper product={product} isWithdrawable={true} />, {
341
341
  queryOptions: { client: createTestQueryClient({ user: richieUser }) },
342
342
  });
343
343
 
@@ -373,7 +373,7 @@ describe.each([
373
373
  overwriteRoutes: true,
374
374
  });
375
375
 
376
- render(<Wrapper product={product} />, {
376
+ render(<Wrapper product={product} isWithdrawable={true} />, {
377
377
  queryOptions: { client: createTestQueryClient({ user: richieUser }) },
378
378
  });
379
379
 
@@ -398,7 +398,7 @@ describe.each([
398
398
  schedule,
399
399
  );
400
400
 
401
- render(<Wrapper product={product} />, {
401
+ render(<Wrapper product={product} isWithdrawable={true} />, {
402
402
  queryOptions: { client: createTestQueryClient({ user: richieUser }) },
403
403
  });
404
404
 
@@ -447,7 +447,7 @@ describe.each([
447
447
  schedule,
448
448
  );
449
449
 
450
- render(<Wrapper product={product} />, {
450
+ render(<Wrapper product={product} isWithdrawable={true} />, {
451
451
  queryOptions: { client: createTestQueryClient({ user: richieUser }) },
452
452
  });
453
453
 
@@ -455,7 +455,7 @@ describe.each([
455
455
  });
456
456
 
457
457
  it('should show a checkbox to waive withdrawal right if the product is not withdrawable', async () => {
458
- const product = ProductFactory({ is_withdrawable: false }).one();
458
+ const product = ProductFactory().one();
459
459
  const schedule = PaymentInstallmentFactory().many(2);
460
460
  fetchMock
461
461
  .get(
@@ -467,7 +467,7 @@ describe.each([
467
467
  schedule,
468
468
  );
469
469
 
470
- render(<Wrapper product={product} />, {
470
+ render(<Wrapper product={product} isWithdrawable={false} />, {
471
471
  queryOptions: { client: createTestQueryClient({ user: richieUser }) },
472
472
  });
473
473
 
@@ -475,7 +475,7 @@ describe.each([
475
475
  });
476
476
 
477
477
  it('should not show a checkbox to waive withdrawal right if the product is withdrawable', async () => {
478
- const product = ProductFactory({ is_withdrawable: true }).one();
478
+ const product = ProductFactory().one();
479
479
  const schedule = PaymentInstallmentFactory().many(2);
480
480
  fetchMock
481
481
  .get(
@@ -487,7 +487,7 @@ describe.each([
487
487
  schedule,
488
488
  );
489
489
 
490
- render(<Wrapper product={product} />, {
490
+ render(<Wrapper product={product} isWithdrawable={true} />, {
491
491
  queryOptions: { client: createTestQueryClient({ user: richieUser }) },
492
492
  });
493
493
 
@@ -12,6 +12,7 @@ export default {
12
12
  product: ProductFactory().one(),
13
13
  onClose: () => {},
14
14
  course: PacedCourseFactory().one(),
15
+ isWithdrawable: true,
15
16
  // enrollment?: Enrollment;
16
17
  // product: CredentialProduct | CertificateProduct;
17
18
  // orderGroup?: OrderGroup;
@@ -17,7 +17,7 @@ import { PacedCourse } from 'types';
17
17
  export interface SaleTunnelProps extends Pick<ModalProps, 'isOpen' | 'onClose'> {
18
18
  product: Product;
19
19
  organizations?: Organization[];
20
-
20
+ isWithdrawable: boolean;
21
21
  course?: PacedCourse | CourseLight;
22
22
  enrollment?: Enrollment;
23
23
  orderGroup?: OrderGroup;
@@ -6,7 +6,7 @@ import { CourseGlimpseList, getCourseGlimpseListProps } from 'components/CourseG
6
6
  import { Spinner } from 'components/Spinner';
7
7
  import context from 'utils/context';
8
8
  import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
9
- import { CourseListItem, CourseProductRelation } from 'types/Joanie';
9
+ import { CourseListItem, CourseProductRelationLight } from 'types/Joanie';
10
10
  import Banner from 'components/Banner';
11
11
 
12
12
  const messages = defineMessages({
@@ -31,7 +31,7 @@ interface TeacherDashboardCourseListProps {
31
31
  titleTranslated?: string;
32
32
  organizationId?: string;
33
33
  loadMore: () => void;
34
- courseAndProductList?: (CourseListItem | CourseProductRelation)[];
34
+ courseAndProductList?: (CourseListItem | CourseProductRelationLight)[];
35
35
  isLoadingMore?: boolean;
36
36
  hasMore?: boolean;
37
37
  isNewSearchLoading?: boolean;
@@ -7,6 +7,7 @@ import {
7
7
  CourseQueryFilters,
8
8
  CourseProductRelationQueryFilters,
9
9
  ProductType,
10
+ CourseProductRelationLight,
10
11
  } from 'types/Joanie';
11
12
  import useUnionResource, { ResourceUnionPaginationProps } from 'hooks/useUnionResource';
12
13
 
@@ -40,7 +41,7 @@ export const useCourseProductUnion = ({
40
41
  const api = useJoanieApi();
41
42
  return useUnionResource<
42
43
  CourseListItem,
43
- CourseProductRelation,
44
+ CourseProductRelation | CourseProductRelationLight,
44
45
  CourseQueryFilters,
45
46
  CourseProductRelationQueryFilters
46
47
  >({
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { useParams, useSearchParams } from 'react-router-dom';
3
3
  import { useCourseProductUnion } from 'hooks/useCourseProductUnion';
4
- import { CourseListItem, CourseProductRelation, ProductType } from 'types/Joanie';
4
+ import { CourseListItem, CourseProductRelationLight, ProductType } from 'types/Joanie';
5
5
  import { Maybe, Nullable } from 'types/utils';
6
6
 
7
7
  const useTeacherCoursesSearch = () => {
@@ -9,7 +9,7 @@ const useTeacherCoursesSearch = () => {
9
9
  const [searchParams, setSearchParams] = useSearchParams();
10
10
  const [count, setCount] = useState<Maybe<number>>(0);
11
11
  const [courseAndProductList, setCourseAndProductList] = useState<
12
- (CourseListItem | CourseProductRelation)[]
12
+ (CourseListItem | CourseProductRelationLight)[]
13
13
  >([]);
14
14
  const [isNewSearchLoading, setIsNewSearchLoading] = useState(false);
15
15
  const query = searchParams.get('query') || undefined;
@@ -149,7 +149,6 @@ export interface Product {
149
149
  state: CourseState;
150
150
  instructions: Nullable<string>;
151
151
  contract_definition?: ContractDefinition;
152
- is_withdrawable: boolean;
153
152
  }
154
153
 
155
154
  export interface CredentialProduct extends Product {
@@ -175,17 +174,21 @@ export interface DefinitionResourcesProduct {
175
174
  contract_definition_id: Nullable<ContractDefinition['id']>;
176
175
  }
177
176
 
178
- export interface CourseProductRelation {
177
+ export interface CourseProductRelationLight {
179
178
  id: string;
180
179
  course: CourseLight;
181
180
  organizations: Organization[];
182
181
  product: Product;
183
182
  created_on: string;
183
+ }
184
+
185
+ export interface CourseProductRelation extends CourseProductRelationLight {
184
186
  order_groups: OrderGroup[];
187
+ is_withdrawable: boolean;
185
188
  }
186
189
  export function isCourseProductRelation(
187
- entity: CourseListItem | CourseProductRelation | RichieCourse,
188
- ): entity is CourseProductRelation {
190
+ entity: CourseListItem | CourseProductRelationLight | RichieCourse,
191
+ ): entity is CourseProductRelationLight {
189
192
  return 'course' in entity && 'product' in entity;
190
193
  }
191
194
 
@@ -699,7 +702,7 @@ export interface API {
699
702
  filters?: Filters,
700
703
  ): Filters extends { id: string }
701
704
  ? Promise<Nullable<CourseProductRelation>>
702
- : Promise<PaginatedResponse<CourseProductRelation>>;
705
+ : Promise<PaginatedResponse<CourseProductRelationLight>>;
703
706
  };
704
707
  contractDefinitions: {
705
708
  previewTemplate(id: string): Promise<File>;
@@ -206,7 +206,6 @@ export const CredentialProductFactory = factory((): CredentialProduct => {
206
206
  remaining_order_count: faker.number.int({ min: 1, max: 100 }),
207
207
  state: CourseStateFactory().one(),
208
208
  instructions: null,
209
- is_withdrawable: true,
210
209
  };
211
210
  });
212
211
 
@@ -338,6 +337,7 @@ export const CourseProductRelationFactory = factory((): CourseProductRelation =>
338
337
  product: ProductFactory().one(),
339
338
  organizations: OrganizationFactory().many(1),
340
339
  order_groups: [],
340
+ is_withdrawable: true,
341
341
  };
342
342
  });
343
343
 
@@ -33,13 +33,14 @@ export const DashboardItemEnrollment = ({ enrollment }: DashboardItemCourseRunPr
33
33
  </div>
34
34
  </div>,
35
35
  ];
36
- enrollment.product_relations.forEach(({ product }) => {
36
+ enrollment.product_relations.forEach(({ product, is_withdrawable }) => {
37
37
  if (isCertificateProduct(product)) {
38
38
  partialFooterList.push(
39
39
  <ProductCertificateFooter
40
40
  key={[enrollment.id, product.id].join('_')}
41
41
  product={product}
42
42
  enrollment={enrollment}
43
+ isWithdrawable={is_withdrawable}
43
44
  />,
44
45
  );
45
46
  }
@@ -124,6 +124,7 @@ describe('<ProductCertificateFooter/>', () => {
124
124
  course,
125
125
  }).one(),
126
126
  }).one()}
127
+ isWithdrawable={true}
127
128
  />,
128
129
  );
129
130
  expect(screen.getByTestId('PurchaseButton__cta')).toBeInTheDocument();
@@ -155,6 +156,7 @@ describe('<ProductCertificateFooter/>', () => {
155
156
  course,
156
157
  }).one(),
157
158
  }).one()}
159
+ isWithdrawable={true}
158
160
  />,
159
161
  );
160
162
 
@@ -176,7 +178,9 @@ describe('<ProductCertificateFooter/>', () => {
176
178
  'https://joanie.endpoint/api/v1.0/certificates/FAKE_CERTIFICATE_ID/',
177
179
  CertificateFactory({ id: order.certificate_id }).one(),
178
180
  );
179
- render(<ProductCertificateFooter product={product} enrollment={enrollment} />);
181
+ render(
182
+ <ProductCertificateFooter product={product} enrollment={enrollment} isWithdrawable={true} />,
183
+ );
180
184
  expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument();
181
185
  expect(screen.queryByTestId('PurchaseButton__cta')).not.toBeInTheDocument();
182
186
  });
@@ -193,7 +197,13 @@ describe('<ProductCertificateFooter/>', () => {
193
197
  orders: [order],
194
198
  course_run: CourseRunFactory({ course }).one(),
195
199
  }).one();
196
- render(<ProductCertificateFooter product={product} enrollment={enrollment} />);
200
+ render(
201
+ <ProductCertificateFooter
202
+ product={product}
203
+ enrollment={enrollment}
204
+ isWithdrawable={true}
205
+ />,
206
+ );
197
207
  expect(screen.queryByRole('button', { name: 'Download' })).not.toBeInTheDocument();
198
208
  expect(screen.getByTestId('PurchaseButton__cta')).toBeInTheDocument();
199
209
  },
@@ -208,7 +218,9 @@ describe('<ProductCertificateFooter/>', () => {
208
218
  orders: [order],
209
219
  course_run: CourseRunFactory({ course }).one(),
210
220
  }).one();
211
- render(<ProductCertificateFooter product={product} enrollment={enrollment} />);
221
+ render(
222
+ <ProductCertificateFooter product={product} enrollment={enrollment} isWithdrawable={true} />,
223
+ );
212
224
  expect(screen.queryByRole('button', { name: 'Download' })).not.toBeInTheDocument();
213
225
  expect(screen.queryByTestId('PurchaseButton__cta')).not.toBeInTheDocument();
214
226
  });
@@ -274,7 +286,13 @@ describe('<ProductCertificateFooter/>', () => {
274
286
  }).one();
275
287
  const enrollment = EnrollmentFactory({ orders: [order] }).one();
276
288
 
277
- render(<ProductCertificateFooter product={product} enrollment={enrollment} />);
289
+ render(
290
+ <ProductCertificateFooter
291
+ product={product}
292
+ enrollment={enrollment}
293
+ isWithdrawable={true}
294
+ />,
295
+ );
278
296
 
279
297
  if (order.state === OrderState.PENDING) {
280
298
  // As the order is in pending state, the user should see the following message.
@@ -315,7 +333,13 @@ describe('<ProductCertificateFooter/>', () => {
315
333
 
316
334
  fetchMock.get(`https://joanie.endpoint/api/v1.0/orders/${order.id}/`, order);
317
335
 
318
- render(<ProductCertificateFooter product={product} enrollment={enrollment} />);
336
+ render(
337
+ <ProductCertificateFooter
338
+ product={product}
339
+ enrollment={enrollment}
340
+ isWithdrawable={true}
341
+ />,
342
+ );
319
343
 
320
344
  if (order.state === OrderState.NO_PAYMENT) {
321
345
  // As the order is in no_payment state, the user should see the following message.
@@ -63,9 +63,14 @@ const messages = defineMessages({
63
63
  export interface ProductCertificateFooterProps {
64
64
  product: CertificateProduct;
65
65
  enrollment: Enrollment;
66
+ isWithdrawable: boolean;
66
67
  }
67
68
 
68
- const ProductCertificateFooter = ({ product, enrollment }: ProductCertificateFooterProps) => {
69
+ const ProductCertificateFooter = ({
70
+ product,
71
+ enrollment,
72
+ isWithdrawable,
73
+ }: ProductCertificateFooterProps) => {
69
74
  const [order, setOrder] = useState(
70
75
  OrderHelper.getActiveEnrollmentOrder(enrollment.orders || [], product.id),
71
76
  );
@@ -103,6 +108,7 @@ const ProductCertificateFooter = ({ product, enrollment }: ProductCertificateFoo
103
108
  className="dashboard-item__button"
104
109
  product={product}
105
110
  enrollment={enrollment}
111
+ isWithdrawable={isWithdrawable}
106
112
  buttonProps={{ size: 'small' }}
107
113
  disabled={!isPurchasable}
108
114
  onFinish={(o) => {
@@ -119,6 +119,7 @@ const PaymentMethodManager = ({ order }: Props) => {
119
119
  {...modal}
120
120
  product={relation.product as CredentialProduct}
121
121
  course={relation.course}
122
+ isWithdrawable={relation.is_withdrawable}
122
123
  />
123
124
  </>
124
125
  );
@@ -43,6 +43,7 @@ const CourseProductItemFooter = ({
43
43
  course={course}
44
44
  product={courseProductRelation.product as CredentialProduct}
45
45
  organizations={courseProductRelation.organizations}
46
+ isWithdrawable={courseProductRelation.is_withdrawable}
46
47
  disabled={!canPurchase}
47
48
  buttonProps={{ fullWidth: true }}
48
49
  />
@@ -61,6 +62,7 @@ const CourseProductItemFooter = ({
61
62
  course={course}
62
63
  product={courseProductRelation.product as CredentialProduct}
63
64
  organizations={courseProductRelation.organizations}
65
+ isWithdrawable={courseProductRelation.is_withdrawable}
64
66
  disabled={!canPurchase}
65
67
  orderGroup={orderGroup}
66
68
  buttonProps={{ fullWidth: true }}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "2.30.1-dev14",
3
+ "version": "2.30.1-dev18",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {
@@ -148,7 +148,8 @@
148
148
  "workerDirectory": "../richie/static/richie/js"
149
149
  },
150
150
  "volta": {
151
- "node": "20.11.0"
151
+ "node": "20.11.0",
152
+ "yarn": "1.22.22"
152
153
  },
153
154
  "devDependencies": {
154
155
  "@storybook/addon-mdx-gfm": "8.3.6",
@@ -23,11 +23,6 @@
23
23
  padding: 1rem;
24
24
  }
25
25
 
26
- // Impose tiny horizontal padding to every section row
27
- .section__row {
28
- padding: $cell-gutter;
29
- }
30
-
31
26
  // ----------
32
27
  // Shared base adjustments for short columns (25+33+50)
33
28
  // ----------
@@ -117,6 +112,8 @@
117
112
  // ----------
118
113
  &__w25 {
119
114
  @include sv-flex(1, 0, 100%);
115
+ padding: $cell-gutter;
116
+
120
117
  @include media-breakpoint-up(lg) {
121
118
  @include sv-flex(1, 0, 25%);
122
119
  }
@@ -169,6 +166,8 @@
169
166
  // ----------
170
167
  &__w33 {
171
168
  @include sv-flex(1, 0, 100%);
169
+ padding: $cell-gutter;
170
+
172
171
  @include media-breakpoint-up(lg) {
173
172
  @include sv-flex(1, 0, 33.3333%);
174
173
  }
@@ -193,6 +192,8 @@
193
192
  // ----------
194
193
  &__w50 {
195
194
  @include sv-flex(1, 0, 100%);
195
+ padding: $cell-gutter;
196
+
196
197
  @include media-breakpoint-up(lg) {
197
198
  @include sv-flex(1, 0, 50%);
198
199
  }
@@ -268,6 +269,8 @@
268
269
  // ----------
269
270
  &__w75 {
270
271
  @include sv-flex(1, 0, 100%);
272
+ padding: $cell-gutter;
273
+
271
274
  @include media-breakpoint-up(lg) {
272
275
  @include sv-flex(1, 0, 75%);
273
276
  }