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.
- package/.storybook/__mocks__/utils/context.ts +4 -0
- package/js/api/joanie.ts +8 -8
- package/js/components/ContractFrame/OrganizationContractFrame.spec.tsx +11 -19
- package/js/components/ContractFrame/OrganizationContractFrame.tsx +4 -4
- package/js/components/CourseGlimpse/index.spec.tsx +2 -0
- package/js/components/CourseGlimpse/index.tsx +2 -0
- package/js/components/CourseGlimpse/utils.ts +29 -30
- 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 +13 -1
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +9 -7
- 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 +171 -29
- package/js/components/SaleTunnel/index.stories.tsx +17 -3
- 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/useOffering/index.ts +32 -0
- package/js/hooks/useTeacherCoursesSearch/index.tsx +2 -2
- package/js/hooks/useTeacherPendingContractsCount/index.ts +4 -4
- package/js/pages/DashboardCourses/index.spec.tsx +14 -14
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +11 -14
- package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +4 -9
- 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 -17
- 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 -3
- 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 -4
- 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 +21 -29
- package/js/pages/TeacherDashboardTraining/index.tsx +12 -16
- package/js/types/Course.ts +2 -0
- package/js/types/Joanie.ts +34 -29
- package/js/types/index.ts +4 -2
- package/js/utils/ProductHelper/index.ts +1 -5
- package/js/utils/test/factories/joanie.ts +17 -25
- package/js/utils/test/factories/richie.ts +6 -2
- 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 -24
- package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +18 -21
- 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 +19 -34
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/_styles.scss +34 -8
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/CourseRunList.tsx +3 -3
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/_styles.scss +9 -0
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +186 -140
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +11 -2
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +111 -24
- 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 { 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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
+
|
|
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
|
+
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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 {
|
|
26
|
-
import {
|
|
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,
|
|
214
|
+
const expectCourseProduct = async (container: HTMLElement, offering: Offering) => {
|
|
215
215
|
const heading = await findByRole(container, 'heading', {
|
|
216
|
-
name:
|
|
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
|
|
387
|
-
const resourceLink = `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${
|
|
388
|
-
fetchMock.get(resourceLink,
|
|
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(),
|
|
409
|
+
await expectCourseProduct(getHeaderContainer(), offering);
|
|
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);
|