richie-education 3.1.3-dev2 → 3.1.3-dev24

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 (87) hide show
  1. package/.storybook/__mocks__/utils/context.ts +4 -0
  2. package/js/api/joanie.ts +8 -8
  3. package/js/components/ContractFrame/OrganizationContractFrame.spec.tsx +11 -19
  4. package/js/components/ContractFrame/OrganizationContractFrame.tsx +4 -4
  5. package/js/components/CourseGlimpse/index.spec.tsx +2 -0
  6. package/js/components/CourseGlimpse/index.tsx +2 -0
  7. package/js/components/CourseGlimpse/utils.ts +29 -30
  8. package/js/components/CourseGlimpseList/utils.ts +2 -2
  9. package/js/components/PurchaseButton/index.tsx +3 -3
  10. package/js/components/SaleTunnel/CredentialSaleTunnel/index.tsx +1 -3
  11. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +13 -1
  12. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +9 -7
  13. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +1 -2
  14. package/js/components/SaleTunnel/index.credential.spec.tsx +5 -19
  15. package/js/components/SaleTunnel/index.full-process.spec.tsx +3 -3
  16. package/js/components/SaleTunnel/index.spec.tsx +171 -29
  17. package/js/components/SaleTunnel/index.stories.tsx +17 -3
  18. package/js/components/SaleTunnel/index.tsx +2 -2
  19. package/js/components/TeacherDashboardCourseList/index.spec.tsx +3 -3
  20. package/js/components/TeacherDashboardCourseList/index.tsx +2 -2
  21. package/js/hooks/useContractArchive/index.ts +3 -3
  22. package/js/hooks/useCourseProductUnion/index.spec.tsx +16 -18
  23. package/js/hooks/useCourseProductUnion/index.ts +7 -7
  24. package/js/hooks/useCourseProducts.ts +4 -8
  25. package/js/hooks/useDefaultOrganizationId/index.tsx +4 -7
  26. package/js/hooks/useOffering/index.ts +32 -0
  27. package/js/hooks/useTeacherCoursesSearch/index.tsx +2 -2
  28. package/js/hooks/useTeacherPendingContractsCount/index.ts +4 -4
  29. package/js/pages/DashboardCourses/index.spec.tsx +14 -14
  30. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +11 -14
  31. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +4 -9
  32. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +11 -11
  33. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +10 -13
  34. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +4 -4
  35. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +20 -28
  36. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +8 -11
  37. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +6 -6
  38. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +4 -4
  39. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +7 -7
  40. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +5 -5
  41. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.spec.ts +21 -28
  42. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +13 -17
  43. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +11 -13
  44. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +6 -6
  45. package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +3 -3
  46. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.spec.tsx +16 -16
  47. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.tsx +4 -4
  48. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractsToSign.tsx +4 -4
  49. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx +21 -21
  50. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.ts +5 -10
  51. package/js/pages/TeacherDashboardCourseLearnersLayout/index.spec.tsx +61 -79
  52. package/js/pages/TeacherDashboardCourseLearnersLayout/index.tsx +1 -1
  53. package/js/pages/TeacherDashboardCoursesLoader/index.spec.tsx +11 -11
  54. package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +11 -11
  55. package/js/pages/TeacherDashboardTraining/TeacherDashboardTrainingLoader.tsx +7 -7
  56. package/js/pages/TeacherDashboardTraining/index.spec.tsx +21 -29
  57. package/js/pages/TeacherDashboardTraining/index.tsx +12 -16
  58. package/js/types/Course.ts +2 -0
  59. package/js/types/Joanie.ts +34 -29
  60. package/js/types/index.ts +4 -2
  61. package/js/utils/ProductHelper/index.ts +1 -5
  62. package/js/utils/test/factories/joanie.ts +17 -25
  63. package/js/utils/test/factories/richie.ts +6 -2
  64. package/js/utils/test/mockCourseProductWithOrder.ts +4 -4
  65. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +1 -1
  66. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +1 -1
  67. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +3 -3
  68. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +1 -1
  69. package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +4 -4
  70. package/js/widgets/Dashboard/components/DashboardItem/stories.mock.ts +1 -1
  71. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.spec.tsx +23 -28
  72. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +4 -8
  73. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +17 -24
  74. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +18 -21
  75. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +4 -4
  76. package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +3 -7
  77. package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +4 -4
  78. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +19 -34
  79. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/_styles.scss +34 -8
  80. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/CourseRunList.tsx +3 -3
  81. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/_styles.scss +9 -0
  82. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +186 -140
  83. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +11 -2
  84. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +111 -24
  85. package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +8 -8
  86. package/package.json +1 -1
  87. package/js/hooks/useCourseProductRelation/index.ts +0 -44
@@ -1,7 +1,7 @@
1
1
  import { Children, useEffect, useMemo } from 'react';
2
2
  import { defineMessages, FormattedMessage, FormattedNumber, useIntl } from 'react-intl';
3
3
  import c from 'classnames';
4
- import { ProductType, Product, CredentialOrder } from 'types/Joanie';
4
+ import { Offering, CredentialOrder, Product, ProductType } from 'types/Joanie';
5
5
  import { useCourseProduct } from 'hooks/useCourseProducts';
6
6
  import { Spinner } from 'components/Spinner';
7
7
  import { Icon, IconTypeEnum } from 'components/Icon';
@@ -38,6 +38,31 @@ const messages = defineMessages({
38
38
  description: 'Course run languages',
39
39
  id: 'components.CourseProductItem.availableIn',
40
40
  },
41
+ original_price: {
42
+ defaultMessage: 'Original price:',
43
+ description: 'Label for the original price of a product',
44
+ id: 'components.CourseProductItem.original_price',
45
+ },
46
+ discounted_price: {
47
+ defaultMessage: 'Discounted price:',
48
+ description: 'Label for the discounted price of a product',
49
+ id: 'components.CourseProductItem.discounted_price',
50
+ },
51
+ discount_rate: {
52
+ defaultMessage: '-{rate}%',
53
+ description: 'Discount rate information',
54
+ id: 'components.CourseProductItem.discount_rate',
55
+ },
56
+ from: {
57
+ defaultMessage: 'from {from}',
58
+ description: 'Discount start date information',
59
+ id: 'components.CourseProductItem.from',
60
+ },
61
+ to: {
62
+ defaultMessage: 'to {to}',
63
+ description: 'Discount end date information',
64
+ id: 'components.CourseProductItem.to',
65
+ },
41
66
  });
42
67
 
43
68
  export interface CourseProductItemProps {
@@ -52,8 +77,9 @@ type HeaderProps = {
52
77
  canPurchase: boolean;
53
78
  order: Maybe<CredentialOrder>;
54
79
  product: Product;
80
+ offering: Offering;
55
81
  };
56
- const Header = ({ product, order, hasPurchased, canPurchase, compact }: HeaderProps) => {
82
+ const Header = ({ product, order, offering, hasPurchased, canPurchase, compact }: HeaderProps) => {
57
83
  const intl = useIntl();
58
84
  const formatDate = useDateFormat();
59
85
 
@@ -72,21 +98,90 @@ const Header = ({ product, order, hasPurchased, canPurchase, compact }: HeaderPr
72
98
  return ProductHelper.getLanguages(product, true, intl);
73
99
  }, [canShowMetadata, product, intl]);
74
100
 
75
- return (
76
- <header className="product-widget__header">
77
- <div className="product-widget__header-main">
78
- <h3 className="product-widget__title">{product.title}</h3>
79
- <strong className="product-widget__price h6">
80
- {hasPurchased && <FormattedMessage {...messages.purchased} />}
81
- {canPurchase && (
101
+ const displayPrice = useMemo(() => {
102
+ if (!canPurchase) {
103
+ return null;
104
+ }
105
+
106
+ if (offering.rules.discounted_price != null) {
107
+ return (
108
+ <>
109
+ <span id="original-price" className="offscreen">
110
+ <FormattedMessage {...messages.original_price} />
111
+ </span>
112
+ <del aria-describedby="original-price" className="product-widget__price-discounted">
82
113
  <FormattedNumber
83
114
  currency={product.price_currency}
84
115
  value={product.price}
85
116
  style="currency"
86
117
  />
87
- )}
88
- </strong>
118
+ </del>
119
+ <span id="discount-price" className="offscreen">
120
+ <FormattedMessage {...messages.discounted_price} />
121
+ </span>
122
+ <ins aria-describedby="discount-price" className="product-widget__price-discount">
123
+ <FormattedNumber
124
+ currency={product.price_currency}
125
+ value={offering.rules.discounted_price}
126
+ style="currency"
127
+ />
128
+ </ins>
129
+ </>
130
+ );
131
+ }
132
+
133
+ return (
134
+ <FormattedNumber currency={product.price_currency} value={product.price} style="currency" />
135
+ );
136
+ }, [canPurchase, offering.rules.discounted_price, product.price]);
137
+
138
+ return (
139
+ <header className="product-widget__header">
140
+ <div className="product-widget__header-main">
141
+ <h3 className="product-widget__title">{product.title}</h3>
89
142
  </div>
143
+ <strong className="product-widget__price h6">
144
+ {hasPurchased && <FormattedMessage {...messages.purchased} />}
145
+ {displayPrice}
146
+ </strong>
147
+ {offering?.rules.description && (
148
+ <p className="product-widget__header-description">{offering.rules.description}</p>
149
+ )}
150
+ {offering?.rules.discounted_price && (
151
+ <p className="product-widget__header-discount">
152
+ {offering.rules.discount_rate ? (
153
+ <span className="product-widget__header-discount-rate">
154
+ <FormattedNumber value={-offering.rules.discount_rate} style="percent" />
155
+ </span>
156
+ ) : (
157
+ <span className="product-widget__header-discount-amount">
158
+ <FormattedNumber
159
+ currency={product.price_currency}
160
+ value={-offering.rules.discount_amount!}
161
+ style="currency"
162
+ />
163
+ </span>
164
+ )}
165
+ {offering.rules.discount_start && (
166
+ <span className="product-widget__header-discount-date">
167
+ &nbsp;
168
+ <FormattedMessage
169
+ {...messages.from}
170
+ values={{ from: formatDate(offering.rules.discount_start) }}
171
+ />
172
+ </span>
173
+ )}
174
+ {offering.rules.discount_end && (
175
+ <span className="product-widget__header-discount-date">
176
+ &nbsp;
177
+ <FormattedMessage
178
+ {...messages.to}
179
+ values={{ to: formatDate(offering.rules.discount_end) }}
180
+ />
181
+ </span>
182
+ )}
183
+ </p>
184
+ )}
90
185
  {canShowMetadata && (
91
186
  <>
92
187
  <p
@@ -131,7 +226,7 @@ const Content = ({ product, order }: { product: Product; order?: CredentialOrder
131
226
  <ol className="product-widget__content">
132
227
  {Children.toArray(
133
228
  targetCourses.map((target_course) => (
134
- <CourseRunItem targetCourse={target_course} order={order} />
229
+ <CourseRunItem key={target_course.code} targetCourse={target_course} order={order} />
135
230
  )),
136
231
  )}
137
232
  {product.certificate_definition && (
@@ -144,12 +239,12 @@ const Content = ({ product, order }: { product: Product; order?: CredentialOrder
144
239
  const CourseProductItem = ({ productId, course, compact = false }: CourseProductItemProps) => {
145
240
  // FIXME(rlecellier): useCourseProduct need's a filter on product.type that only return
146
241
  // CredentialOrder
147
- const { item: courseProductRelation, states: productQueryStates } = useCourseProduct({
242
+ const { item: offering, states: productQueryStates } = useCourseProduct({
148
243
  product_id: productId,
149
244
  course_id: course.code,
150
245
  });
151
246
 
152
- const product = courseProductRelation?.product;
247
+ const product = offering?.product;
153
248
  const { item: productOrder, states: orderQueryStates } = useProductOrder({
154
249
  productId,
155
250
  courseCode: course.code,
@@ -179,13 +274,6 @@ const CourseProductItem = ({ productId, course, compact = false }: CourseProduct
179
274
  return null;
180
275
  }
181
276
 
182
- const orderGroups = courseProductRelation
183
- ? ProductHelper.getActiveOrderGroups(courseProductRelation)
184
- : [];
185
- const orderGroupsAvailable = orderGroups.filter(
186
- (orderGroup) => orderGroup.nb_available_seats > 0,
187
- );
188
-
189
277
  return (
190
278
  <section
191
279
  className={c('product-widget', {
@@ -214,6 +302,7 @@ const CourseProductItem = ({ productId, course, compact = false }: CourseProduct
214
302
  <Header
215
303
  product={product}
216
304
  order={order}
305
+ offering={offering}
217
306
  canPurchase={canPurchase}
218
307
  hasPurchased={hasPurchased}
219
308
  compact={compact}
@@ -222,9 +311,7 @@ const CourseProductItem = ({ productId, course, compact = false }: CourseProduct
222
311
  <footer className="product-widget__footer">
223
312
  <CourseProductItemFooter
224
313
  course={course}
225
- courseProductRelation={courseProductRelation}
226
- orderGroups={orderGroups}
227
- orderGroupsAvailable={orderGroupsAvailable}
314
+ offering={offering}
228
315
  canPurchase={canPurchase}
229
316
  />
230
317
  </footer>
@@ -22,8 +22,8 @@ import {
22
22
  import SyllabusCourseRunsList from 'widgets/SyllabusCourseRunsList/index';
23
23
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
24
24
  import { CourseRun, Priority } from 'types';
25
- import { CourseProductRelation } from 'types/Joanie';
26
- import { CourseProductRelationFactory } from 'utils/test/factories/joanie';
25
+ import { Offering } from 'types/Joanie';
26
+ import { OfferingFactory } from 'utils/test/factories/joanie';
27
27
  import { DEFAULT_DATE_FORMAT } from 'hooks/useDateFormat';
28
28
  import { StringHelper } from 'utils/StringHelper';
29
29
  import { computeStates } from 'utils/CourseRuns';
@@ -211,9 +211,9 @@ describe('<SyllabusCourseRunsList/>', () => {
211
211
  });
212
212
  };
213
213
 
214
- const expectCourseProduct = async (container: HTMLElement, relation: CourseProductRelation) => {
214
+ const expectCourseProduct = async (container: HTMLElement, offering: Offering) => {
215
215
  const heading = await findByRole(container, 'heading', {
216
- name: relation.product.title,
216
+ name: offering.product.title,
217
217
  });
218
218
  expect(Array.from(heading.classList)).toContain('product-widget__title');
219
219
  };
@@ -383,9 +383,9 @@ describe('<SyllabusCourseRunsList/>', () => {
383
383
 
384
384
  it('has one opened product', async () => {
385
385
  const course = PacedCourseFactory().one();
386
- const relation = CourseProductRelationFactory().one();
387
- const resourceLink = `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${relation.product.id}/`;
388
- fetchMock.get(resourceLink, relation);
386
+ const offering = OfferingFactory().one();
387
+ const resourceLink = `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${offering.product.id}/`;
388
+ fetchMock.get(resourceLink, offering);
389
389
 
390
390
  const courseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
391
391
  resource_link: resourceLink,
@@ -406,7 +406,7 @@ describe('<SyllabusCourseRunsList/>', () => {
406
406
  expect(getHeaderContainer().querySelectorAll('.course-detail__run-descriptions').length).toBe(
407
407
  1,
408
408
  );
409
- await expectCourseProduct(getHeaderContainer(), relation);
409
+ await expectCourseProduct(getHeaderContainer(), offering);
410
410
 
411
411
  // Portal.
412
412
  expectEmptyPortalContainer();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "3.1.3-dev2",
3
+ "version": "3.1.3-dev24",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {
@@ -1,44 +0,0 @@
1
- import { defineMessages } from 'react-intl';
2
- import { useJoanieApi } from 'contexts/JoanieApiContext';
3
- import { API, CourseProductRelation, CourseProductRelationQueryFilters } from 'types/Joanie';
4
- import { useResource, useResources, UseResourcesProps } from 'hooks/useResources';
5
-
6
- const messages = defineMessages({
7
- errorGet: {
8
- id: 'hooks.useCourseProductRelations.errorGet',
9
- description:
10
- 'Error message shown to the user when course product relation fetch request fails.',
11
- defaultMessage: 'An error occurred while fetching trainings. Please retry later.',
12
- },
13
- errorNotFound: {
14
- id: 'hooks.useCourseProductRelations.errorNotFound',
15
- description: 'Error message shown to the user when no course product relation matches.',
16
- defaultMessage: 'Cannot find the training.',
17
- },
18
- });
19
-
20
- /**
21
- * Joanie Api hook to retrieve/create/update/delete course
22
- * owned by the authenticated user.
23
- */
24
- const props: UseResourcesProps<
25
- CourseProductRelation,
26
- CourseProductRelationQueryFilters,
27
- API['courseProductRelations']
28
- > = {
29
- queryKey: ['courseProductRelations'],
30
- apiInterface: () => useJoanieApi().courseProductRelations,
31
- session: true,
32
- messages,
33
- };
34
-
35
- export const useCourseProductRelations = useResources<
36
- CourseProductRelation,
37
- CourseProductRelationQueryFilters,
38
- API['courseProductRelations']
39
- >(props);
40
-
41
- export const useCourseProductRelation = useResource<
42
- CourseProductRelation,
43
- CourseProductRelationQueryFilters
44
- >(props);