richie-education 3.1.2 → 3.1.3-dev12
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/api/joanie.ts +8 -8
- package/js/components/ContractFrame/OrganizationContractFrame.spec.tsx +11 -20
- package/js/components/ContractFrame/OrganizationContractFrame.tsx +4 -4
- package/js/components/CourseGlimpse/utils.ts +22 -35
- package/js/components/CourseGlimpseList/utils.ts +2 -2
- package/js/components/PurchaseButton/index.tsx +3 -3
- package/js/components/SaleTunnel/CredentialSaleTunnel/index.tsx +1 -3
- package/js/components/SaleTunnel/GenericSaleTunnel.tsx +3 -1
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +2 -2
- package/js/components/SaleTunnel/SubscriptionButton/index.tsx +1 -2
- package/js/components/SaleTunnel/index.credential.spec.tsx +5 -19
- package/js/components/SaleTunnel/index.full-process.spec.tsx +3 -3
- package/js/components/SaleTunnel/index.spec.tsx +75 -0
- package/js/components/SaleTunnel/index.stories.tsx +0 -1
- package/js/components/SaleTunnel/index.tsx +2 -2
- package/js/components/TeacherDashboardCourseList/index.spec.tsx +3 -3
- package/js/components/TeacherDashboardCourseList/index.tsx +2 -2
- package/js/hooks/useContractArchive/index.ts +3 -3
- package/js/hooks/useCourseProductUnion/index.spec.tsx +16 -18
- package/js/hooks/useCourseProductUnion/index.ts +7 -7
- package/js/hooks/useCourseProducts.ts +4 -8
- package/js/hooks/useDefaultOrganizationId/index.tsx +4 -7
- package/js/hooks/useOffer/index.ts +32 -0
- package/js/hooks/useTeacherCoursesSearch/index.tsx +4 -4
- package/js/hooks/useTeacherPendingContractsCount/index.ts +4 -4
- package/js/pages/DashboardCourses/index.spec.tsx +14 -17
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +8 -14
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +4 -12
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +10 -13
- package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +20 -28
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +8 -11
- package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +6 -6
- package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +7 -7
- package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +5 -5
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.spec.ts +21 -28
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +13 -23
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +11 -13
- package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +6 -6
- package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +3 -6
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.spec.tsx +16 -16
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.tsx +4 -4
- package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractsToSign.tsx +4 -7
- package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx +21 -21
- package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.ts +5 -10
- package/js/pages/TeacherDashboardCourseLearnersLayout/index.spec.tsx +61 -79
- package/js/pages/TeacherDashboardCourseLearnersLayout/index.tsx +1 -1
- package/js/pages/TeacherDashboardCoursesLoader/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +11 -11
- package/js/pages/TeacherDashboardTraining/TeacherDashboardTrainingLoader.tsx +7 -7
- package/js/pages/TeacherDashboardTraining/index.spec.tsx +25 -33
- package/js/pages/TeacherDashboardTraining/index.tsx +12 -20
- package/js/types/Joanie.ts +26 -30
- package/js/utils/ProductHelper/index.ts +1 -5
- package/js/utils/test/factories/joanie.ts +12 -25
- package/js/utils/test/mockCourseProductWithOrder.ts +4 -4
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +3 -3
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +4 -4
- package/js/widgets/Dashboard/components/DashboardItem/stories.mock.ts +1 -1
- package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.spec.tsx +23 -28
- package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +4 -8
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +17 -27
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +16 -25
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +4 -4
- package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +3 -7
- package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +4 -4
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +23 -38
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/_styles.scss +27 -7
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/CourseRunList.tsx +1 -1
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +174 -158
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +9 -2
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +108 -28
- package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +8 -8
- package/package.json +1 -1
- 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 {
|
|
4
|
+
import { Offer, 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
|
+
offer: Offer;
|
|
55
81
|
};
|
|
56
|
-
const Header = ({ product, order, hasPurchased, canPurchase, compact }: HeaderProps) => {
|
|
82
|
+
const Header = ({ product, order, offer, hasPurchased, canPurchase, compact }: HeaderProps) => {
|
|
57
83
|
const intl = useIntl();
|
|
58
84
|
const formatDate = useDateFormat();
|
|
59
85
|
|
|
@@ -72,21 +98,87 @@ const Header = ({ product, order, hasPurchased, canPurchase, compact }: HeaderPr
|
|
|
72
98
|
return ProductHelper.getLanguages(product, true, intl);
|
|
73
99
|
}, [canShowMetadata, product, intl]);
|
|
74
100
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
101
|
+
const displayPrice = useMemo(() => {
|
|
102
|
+
if (!canPurchase) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (offer.discounted_price) {
|
|
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
|
-
|
|
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={offer.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, offer.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
|
+
{offer?.description && (
|
|
148
|
+
<p className="product-widget__header-description">{offer.description}</p>
|
|
149
|
+
)}
|
|
150
|
+
{offer?.discounted_price && (
|
|
151
|
+
<p className="product-widget__header-discount">
|
|
152
|
+
{offer.discount_rate ? (
|
|
153
|
+
<span className="product-widget__header-discount-rate">
|
|
154
|
+
<FormattedNumber value={-offer.discount_rate} style="percent" />
|
|
155
|
+
</span>
|
|
156
|
+
) : (
|
|
157
|
+
<span className="product-widget__header-discount-amount">
|
|
158
|
+
<FormattedNumber
|
|
159
|
+
currency={product.price_currency}
|
|
160
|
+
value={-offer.discount_amount!}
|
|
161
|
+
style="currency"
|
|
162
|
+
/>
|
|
163
|
+
</span>
|
|
164
|
+
)}
|
|
165
|
+
{offer.discount_start && (
|
|
166
|
+
<span className="product-widget__header-discount-date">
|
|
167
|
+
|
|
168
|
+
<FormattedMessage
|
|
169
|
+
{...messages.from}
|
|
170
|
+
values={{ from: formatDate(offer.discount_start) }}
|
|
171
|
+
/>
|
|
172
|
+
</span>
|
|
173
|
+
)}
|
|
174
|
+
{offer.discount_end && (
|
|
175
|
+
<span className="product-widget__header-discount-date">
|
|
176
|
+
|
|
177
|
+
<FormattedMessage {...messages.to} values={{ to: formatDate(offer.discount_end) }} />
|
|
178
|
+
</span>
|
|
179
|
+
)}
|
|
180
|
+
</p>
|
|
181
|
+
)}
|
|
90
182
|
{canShowMetadata && (
|
|
91
183
|
<>
|
|
92
184
|
<p
|
|
@@ -131,7 +223,7 @@ const Content = ({ product, order }: { product: Product; order?: CredentialOrder
|
|
|
131
223
|
<ol className="product-widget__content">
|
|
132
224
|
{Children.toArray(
|
|
133
225
|
targetCourses.map((target_course) => (
|
|
134
|
-
<CourseRunItem targetCourse={target_course} order={order} />
|
|
226
|
+
<CourseRunItem key={target_course.code} targetCourse={target_course} order={order} />
|
|
135
227
|
)),
|
|
136
228
|
)}
|
|
137
229
|
{product.certificate_definition && (
|
|
@@ -144,12 +236,12 @@ const Content = ({ product, order }: { product: Product; order?: CredentialOrder
|
|
|
144
236
|
const CourseProductItem = ({ productId, course, compact = false }: CourseProductItemProps) => {
|
|
145
237
|
// FIXME(rlecellier): useCourseProduct need's a filter on product.type that only return
|
|
146
238
|
// CredentialOrder
|
|
147
|
-
const { item:
|
|
239
|
+
const { item: offer, states: productQueryStates } = useCourseProduct({
|
|
148
240
|
product_id: productId,
|
|
149
241
|
course_id: course.code,
|
|
150
242
|
});
|
|
151
243
|
|
|
152
|
-
const product =
|
|
244
|
+
const product = offer?.product;
|
|
153
245
|
const { item: productOrder, states: orderQueryStates } = useProductOrder({
|
|
154
246
|
productId,
|
|
155
247
|
courseCode: course.code,
|
|
@@ -179,13 +271,6 @@ const CourseProductItem = ({ productId, course, compact = false }: CourseProduct
|
|
|
179
271
|
return null;
|
|
180
272
|
}
|
|
181
273
|
|
|
182
|
-
const orderGroups = courseProductRelation
|
|
183
|
-
? ProductHelper.getActiveOrderGroups(courseProductRelation)
|
|
184
|
-
: [];
|
|
185
|
-
const orderGroupsAvailable = orderGroups.filter(
|
|
186
|
-
(orderGroup) => orderGroup.nb_available_seats > 0,
|
|
187
|
-
);
|
|
188
|
-
|
|
189
274
|
return (
|
|
190
275
|
<section
|
|
191
276
|
className={c('product-widget', {
|
|
@@ -214,19 +299,14 @@ const CourseProductItem = ({ productId, course, compact = false }: CourseProduct
|
|
|
214
299
|
<Header
|
|
215
300
|
product={product}
|
|
216
301
|
order={order}
|
|
302
|
+
offer={offer}
|
|
217
303
|
canPurchase={canPurchase}
|
|
218
304
|
hasPurchased={hasPurchased}
|
|
219
305
|
compact={compact}
|
|
220
306
|
/>
|
|
221
307
|
{canShowContent && <Content product={product} order={order} />}
|
|
222
308
|
<footer className="product-widget__footer">
|
|
223
|
-
<CourseProductItemFooter
|
|
224
|
-
course={course}
|
|
225
|
-
courseProductRelation={courseProductRelation}
|
|
226
|
-
orderGroups={orderGroups}
|
|
227
|
-
orderGroupsAvailable={orderGroupsAvailable}
|
|
228
|
-
canPurchase={canPurchase}
|
|
229
|
-
/>
|
|
309
|
+
<CourseProductItemFooter course={course} offer={offer} canPurchase={canPurchase} />
|
|
230
310
|
</footer>
|
|
231
311
|
</>
|
|
232
312
|
)}
|
|
@@ -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 {
|
|
26
|
-
import {
|
|
25
|
+
import { Offer } from 'types/Joanie';
|
|
26
|
+
import { OfferFactory } 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,
|
|
214
|
+
const expectCourseProduct = async (container: HTMLElement, offer: Offer) => {
|
|
215
215
|
const heading = await findByRole(container, 'heading', {
|
|
216
|
-
name:
|
|
216
|
+
name: offer.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
|
|
387
|
-
const resourceLink = `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${
|
|
388
|
-
fetchMock.get(resourceLink,
|
|
386
|
+
const offer = OfferFactory().one();
|
|
387
|
+
const resourceLink = `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${offer.product.id}/`;
|
|
388
|
+
fetchMock.get(resourceLink, offer);
|
|
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(),
|
|
409
|
+
await expectCourseProduct(getHeaderContainer(), offer);
|
|
410
410
|
|
|
411
411
|
// Portal.
|
|
412
412
|
expectEmptyPortalContainer();
|
package/package.json
CHANGED
|
@@ -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);
|