richie-education 2.25.0-b2.dev82 → 2.25.0-b2.dev91
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/components/CourseGlimpseList/index.spec.tsx +1 -1
- package/js/components/CourseGlimpseList/index.tsx +1 -1
- package/js/components/TeacherDashboardCourseList/index.spec.tsx +4 -4
- package/js/components/TeacherDashboardCourseList/index.tsx +2 -1
- package/js/hooks/useCourseProductUnion/index.ts +4 -1
- package/js/pages/TeacherDashboardContractsLayout/components/ContractFiltersBar/index.tsx +10 -49
- package/js/pages/TeacherDashboardCoursesLoader/index.spec.tsx +2 -2
- package/js/types/Joanie.ts +1 -0
- package/js/utils/OrderHelper/index.ts +32 -0
- package/js/widgets/Dashboard/components/DashboardAvatar/_styles.scss +4 -3
- package/js/widgets/Dashboard/components/DashboardAvatar/index.tsx +1 -1
- package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.tsx +5 -2
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +2 -2
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +9 -6
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.spec.tsx +2 -5
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +5 -5
- package/js/widgets/Dashboard/components/DashboardListAvatar/_styles.scss +8 -0
- package/js/widgets/Dashboard/components/DashboardListAvatar/index.tsx +11 -0
- package/js/widgets/Dashboard/components/DashboardOrderLoader/index.tsx +5 -2
- package/js/widgets/Dashboard/components/DashboardSidebar/_styles.scss +2 -0
- package/js/widgets/Dashboard/components/FilterOrganization/index.tsx +58 -0
- package/js/widgets/Dashboard/components/FiltersBar/index.tsx +9 -0
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/EnrollableCourseRunList.tsx +4 -2
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +4 -2
- package/package.json +1 -1
- package/scss/components/_index.scss +1 -0
- package/scss/objects/_course_glimpses.scss +0 -7
- package/scss/objects/_index.scss +1 -0
- package/scss/objects/_list.scss +8 -0
- package/js/widgets/Dashboard/components/DashboardItem/utils/order.ts +0 -15
|
@@ -59,7 +59,7 @@ describe('widgets/Search/components/CourseGlimpseList', () => {
|
|
|
59
59
|
expect(srOnlyCount).toHaveAttribute('aria-live', 'polite');
|
|
60
60
|
expect(srOnlyCount).toHaveAttribute('aria-atomic', 'true');
|
|
61
61
|
// the message shown in the UI
|
|
62
|
-
expect(container.querySelector('.
|
|
62
|
+
expect(container.querySelector('.list__count-description')).toHaveAttribute(
|
|
63
63
|
'aria-hidden',
|
|
64
64
|
'true',
|
|
65
65
|
);
|
|
@@ -61,7 +61,7 @@ export const CourseGlimpseList = ({
|
|
|
61
61
|
}}
|
|
62
62
|
/>
|
|
63
63
|
</div>
|
|
64
|
-
<div className="course-glimpse-list__count" aria-hidden="true">
|
|
64
|
+
<div className="course-glimpse-list__count list__count-description" aria-hidden="true">
|
|
65
65
|
<FormattedMessage
|
|
66
66
|
{...messages.courseCount}
|
|
67
67
|
values={{
|
|
@@ -68,7 +68,7 @@ describe('components/TeacherDashboardCourseList', () => {
|
|
|
68
68
|
title: "Full training: Let's dance, the online lesson",
|
|
69
69
|
}).one();
|
|
70
70
|
fetchMock.get(
|
|
71
|
-
`https://joanie.endpoint/api/v1.0/course-product-relations/?page=1&page_size=${perPage}`,
|
|
71
|
+
`https://joanie.endpoint/api/v1.0/course-product-relations/?product_type=credential&page=1&page_size=${perPage}`,
|
|
72
72
|
mockPaginatedResponse([productCooking, productDancing], 15, false),
|
|
73
73
|
);
|
|
74
74
|
|
|
@@ -98,7 +98,7 @@ describe('components/TeacherDashboardCourseList', () => {
|
|
|
98
98
|
`https://joanie.endpoint/api/v1.0/courses/?has_listed_course_runs=true&page=1&page_size=${perPage}`,
|
|
99
99
|
);
|
|
100
100
|
expect(calledUrls).toContain(
|
|
101
|
-
`https://joanie.endpoint/api/v1.0/course-product-relations/?page=1&page_size=${perPage}`,
|
|
101
|
+
`https://joanie.endpoint/api/v1.0/course-product-relations/?product_type=credential&page=1&page_size=${perPage}`,
|
|
102
102
|
);
|
|
103
103
|
|
|
104
104
|
expect(
|
|
@@ -124,7 +124,7 @@ describe('components/TeacherDashboardCourseList', () => {
|
|
|
124
124
|
},
|
|
125
125
|
);
|
|
126
126
|
fetchMock.get(
|
|
127
|
-
`https://joanie.endpoint/api/v1.0/course-product-relations/?page=1&page_size=${perPage}`,
|
|
127
|
+
`https://joanie.endpoint/api/v1.0/course-product-relations/?product_type=credential&page=1&page_size=${perPage}`,
|
|
128
128
|
mockPaginatedResponse([], 0, false),
|
|
129
129
|
{
|
|
130
130
|
overwriteRoutes: true,
|
|
@@ -154,7 +154,7 @@ describe('components/TeacherDashboardCourseList', () => {
|
|
|
154
154
|
`https://joanie.endpoint/api/v1.0/courses/?has_listed_course_runs=true&page=1&page_size=${perPage}`,
|
|
155
155
|
);
|
|
156
156
|
expect(calledUrls).toContain(
|
|
157
|
-
`https://joanie.endpoint/api/v1.0/course-product-relations/?page=1&page_size=${perPage}`,
|
|
157
|
+
`https://joanie.endpoint/api/v1.0/course-product-relations/?product_type=credential&page=1&page_size=${perPage}`,
|
|
158
158
|
);
|
|
159
159
|
|
|
160
160
|
expect(await screen.findByText('You have no courses yet.')).toBeInTheDocument();
|
|
@@ -6,6 +6,7 @@ import { Spinner } from 'components/Spinner';
|
|
|
6
6
|
import context from 'utils/context';
|
|
7
7
|
import { useCourseProductUnion } from 'hooks/useCourseProductUnion';
|
|
8
8
|
import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
|
|
9
|
+
import { ProductType } from 'types/Joanie';
|
|
9
10
|
|
|
10
11
|
const messages = defineMessages({
|
|
11
12
|
loading: {
|
|
@@ -41,7 +42,7 @@ const TeacherDashboardCourseList = ({
|
|
|
41
42
|
isLoading,
|
|
42
43
|
next,
|
|
43
44
|
hasMore,
|
|
44
|
-
} = useCourseProductUnion({ perPage: 25, organizationId });
|
|
45
|
+
} = useCourseProductUnion({ perPage: 25, organizationId, productType: ProductType.CREDENTIAL });
|
|
45
46
|
useIntersectionObserver({
|
|
46
47
|
target: loadMoreButtonRef,
|
|
47
48
|
onIntersect: next,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
CourseProductRelation,
|
|
7
7
|
CourseQueryFilters,
|
|
8
8
|
CourseProductRelationQueryFilters,
|
|
9
|
+
ProductType,
|
|
9
10
|
} from 'types/Joanie';
|
|
10
11
|
import useUnionResource, { ResourceUnionPaginationProps } from 'hooks/useUnionResource';
|
|
11
12
|
|
|
@@ -26,11 +27,13 @@ const messages = defineMessages({
|
|
|
26
27
|
|
|
27
28
|
interface UseCourseProductUnionProps extends ResourceUnionPaginationProps {
|
|
28
29
|
organizationId?: string;
|
|
30
|
+
productType?: ProductType;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export const useCourseProductUnion = ({
|
|
32
34
|
perPage = 50,
|
|
33
35
|
organizationId,
|
|
36
|
+
productType,
|
|
34
37
|
}: UseCourseProductUnionProps = {}) => {
|
|
35
38
|
const api = useJoanieApi();
|
|
36
39
|
return useUnionResource<
|
|
@@ -47,7 +50,7 @@ export const useCourseProductUnion = ({
|
|
|
47
50
|
queryBConfig: {
|
|
48
51
|
queryKey: ['user', 'course_product_relations'],
|
|
49
52
|
fn: api.courseProductRelations.get,
|
|
50
|
-
filters: { organization_id: organizationId },
|
|
53
|
+
filters: { organization_id: organizationId, product_type: productType },
|
|
51
54
|
},
|
|
52
55
|
perPage,
|
|
53
56
|
errorGetMessage: messages.errorGet,
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Select, SelectProps } from '@openfun/cunningham-react';
|
|
2
2
|
import { defineMessages, useIntl } from 'react-intl';
|
|
3
|
-
import
|
|
4
|
-
import { useOrganizations } from 'hooks/useOrganizations';
|
|
3
|
+
import FiltersBar from 'widgets/Dashboard/components/FiltersBar';
|
|
5
4
|
import { ContractState } from 'types/Joanie';
|
|
6
5
|
import { ContractHelper, ContractStatePoV } from 'utils/ContractHelper';
|
|
7
|
-
import
|
|
6
|
+
import FilterOrganization from 'widgets/Dashboard/components/FilterOrganization';
|
|
8
7
|
|
|
9
8
|
export const messages = defineMessages({
|
|
10
9
|
organizationFilterLabel: {
|
|
@@ -31,11 +30,6 @@ interface ContractFiltersBarProps {
|
|
|
31
30
|
hideFilterSignatureState?: boolean;
|
|
32
31
|
}
|
|
33
32
|
|
|
34
|
-
interface FilterProps {
|
|
35
|
-
defaultValue?: SelectProps['defaultValue'];
|
|
36
|
-
onChange: (value: Partial<ContractListFilters>) => void;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
33
|
const ContractFiltersBar = ({
|
|
40
34
|
defaultValues,
|
|
41
35
|
onFiltersChange,
|
|
@@ -43,9 +37,9 @@ const ContractFiltersBar = ({
|
|
|
43
37
|
hideFilterSignatureState = false,
|
|
44
38
|
}: ContractFiltersBarProps) => {
|
|
45
39
|
return (
|
|
46
|
-
<
|
|
40
|
+
<FiltersBar>
|
|
47
41
|
{!hideFilterOrganization && (
|
|
48
|
-
<
|
|
42
|
+
<FilterOrganization
|
|
49
43
|
defaultValue={defaultValues?.organization_id}
|
|
50
44
|
onChange={onFiltersChange}
|
|
51
45
|
/>
|
|
@@ -56,49 +50,16 @@ const ContractFiltersBar = ({
|
|
|
56
50
|
onChange={onFiltersChange}
|
|
57
51
|
/>
|
|
58
52
|
)}
|
|
59
|
-
</
|
|
53
|
+
</FiltersBar>
|
|
60
54
|
);
|
|
61
55
|
};
|
|
62
56
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
states: { isFetched },
|
|
68
|
-
} = useOrganizations();
|
|
69
|
-
|
|
70
|
-
const organizationOptions = organizations.map((organization) => ({
|
|
71
|
-
label: organization.title,
|
|
72
|
-
value: organization.id,
|
|
73
|
-
}));
|
|
74
|
-
|
|
75
|
-
const handleChange: SelectProps['onChange'] = (e) => {
|
|
76
|
-
const value = e.target.value as string;
|
|
77
|
-
onChange({ organization_id: value });
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
if (isFetched && defaultValue === undefined) {
|
|
82
|
-
onChange({ organization_id: organizationOptions[0]?.value });
|
|
83
|
-
}
|
|
84
|
-
}, [defaultValue, isFetched]);
|
|
85
|
-
|
|
86
|
-
if (!isFetched) return <Spinner />;
|
|
87
|
-
|
|
88
|
-
return (
|
|
89
|
-
<Select
|
|
90
|
-
label={intl.formatMessage(messages.organizationFilterLabel)}
|
|
91
|
-
options={organizationOptions}
|
|
92
|
-
defaultValue={defaultValue || organizationOptions[0].value}
|
|
93
|
-
onChange={handleChange}
|
|
94
|
-
disabled={!isFetched}
|
|
95
|
-
clearable={false}
|
|
96
|
-
searchable={true}
|
|
97
|
-
/>
|
|
98
|
-
);
|
|
99
|
-
};
|
|
57
|
+
interface ContractFilterProps {
|
|
58
|
+
defaultValue?: SelectProps['defaultValue'];
|
|
59
|
+
onChange: (value: Partial<ContractListFilters>) => void;
|
|
60
|
+
}
|
|
100
61
|
|
|
101
|
-
const SignatureStateFilter = ({ defaultValue, onChange }:
|
|
62
|
+
const SignatureStateFilter = ({ defaultValue, onChange }: ContractFilterProps) => {
|
|
102
63
|
const intl = useIntl();
|
|
103
64
|
const contractStateOptions = Object.values(ContractState)
|
|
104
65
|
.filter((value) => value !== ContractState.UNSIGNED)
|
|
@@ -57,7 +57,7 @@ describe('components/TeacherDashboardCoursesLoader', () => {
|
|
|
57
57
|
mockPaginatedResponse(CourseListItemFactory().many(15), 15, false),
|
|
58
58
|
);
|
|
59
59
|
fetchMock.get(
|
|
60
|
-
`https://joanie.endpoint/api/v1.0/course-product-relations/?page=1&page_size=${perPage}`,
|
|
60
|
+
`https://joanie.endpoint/api/v1.0/course-product-relations/?product_type=credential&page=1&page_size=${perPage}`,
|
|
61
61
|
mockPaginatedResponse(CourseProductRelationFactory().many(15), 15, false),
|
|
62
62
|
);
|
|
63
63
|
|
|
@@ -87,7 +87,7 @@ describe('components/TeacherDashboardCoursesLoader', () => {
|
|
|
87
87
|
const calledUrls = fetchMock.calls().map((call) => call[0]);
|
|
88
88
|
expect(calledUrls).toHaveLength(nbApiCalls);
|
|
89
89
|
expect(calledUrls).toContain(
|
|
90
|
-
`https://joanie.endpoint/api/v1.0/course-product-relations/?page=1&page_size=${perPage}`,
|
|
90
|
+
`https://joanie.endpoint/api/v1.0/course-product-relations/?product_type=credential&page=1&page_size=${perPage}`,
|
|
91
91
|
);
|
|
92
92
|
|
|
93
93
|
// section titles
|
package/js/types/Joanie.ts
CHANGED
|
@@ -435,6 +435,7 @@ export interface CourseProductQueryFilters extends ResourcesQuery {
|
|
|
435
435
|
export interface CourseProductRelationQueryFilters extends PaginatedResourceQuery {
|
|
436
436
|
id?: CourseProductRelation['id'];
|
|
437
437
|
organization_id?: Organization['id'];
|
|
438
|
+
product_type?: ProductType;
|
|
438
439
|
}
|
|
439
440
|
|
|
440
441
|
export enum ContractState {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OrderEnrollment,
|
|
3
|
+
ACTIVE_ORDER_STATES,
|
|
4
|
+
Order,
|
|
5
|
+
OrderState,
|
|
6
|
+
ContractDefinition,
|
|
7
|
+
} from 'types/Joanie';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Helper class for orders
|
|
11
|
+
*/
|
|
12
|
+
export class OrderHelper {
|
|
13
|
+
/**
|
|
14
|
+
* return an Order from the given list that match given productId
|
|
15
|
+
*/
|
|
16
|
+
static getActiveEnrollmentOrder(orders: OrderEnrollment[], productId: string) {
|
|
17
|
+
const filter = (order: OrderEnrollment) =>
|
|
18
|
+
ACTIVE_ORDER_STATES.includes(order.state) && order.product_id === productId;
|
|
19
|
+
return orders.find(filter);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* tell us if a order need to be sign by it's owner (the learner user).
|
|
24
|
+
*/
|
|
25
|
+
static orderNeedsSignature(order: Order, contractDefinition?: ContractDefinition) {
|
|
26
|
+
return (
|
|
27
|
+
order?.state === OrderState.VALIDATED &&
|
|
28
|
+
contractDefinition &&
|
|
29
|
+
!(order.contract && order.contract.student_signed_on)
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
$avatar-size
|
|
1
|
+
// when parent em-value is 40px, $avatar-size is 100px
|
|
2
|
+
$avatar-size: 2.5em;
|
|
2
3
|
|
|
3
4
|
.dashboard {
|
|
4
5
|
&__avatar {
|
|
5
6
|
box-shadow: r-theme-val(dashboard-sidebar, base-shadow);
|
|
6
7
|
background-color: r-theme-val(dashboard-avatar, background-color);
|
|
7
8
|
border-radius: 100%;
|
|
8
|
-
width: $avatar-size;
|
|
9
9
|
height: $avatar-size;
|
|
10
|
+
width: $avatar-size;
|
|
10
11
|
display: flex;
|
|
11
12
|
justify-content: center;
|
|
12
13
|
align-items: center;
|
|
@@ -14,7 +15,7 @@ $avatar-size: 100px;
|
|
|
14
15
|
padding: rem-calc(3px);
|
|
15
16
|
|
|
16
17
|
&__letter {
|
|
17
|
-
font-size:
|
|
18
|
+
font-size: 1em;
|
|
18
19
|
font-family: $r-font-family-montserrat;
|
|
19
20
|
font-weight: $font-weight-boldest;
|
|
20
21
|
line-height: 1;
|
|
@@ -14,10 +14,11 @@ import {
|
|
|
14
14
|
import { Spinner } from 'components/Spinner';
|
|
15
15
|
import Banner, { BannerType } from 'components/Banner';
|
|
16
16
|
import { Icon, IconTypeEnum } from 'components/Icon';
|
|
17
|
+
|
|
17
18
|
import useDateFormat from 'hooks/useDateFormat';
|
|
18
|
-
import { orderNeedsSignature } from 'widgets/Dashboard/components/DashboardItem/utils/order';
|
|
19
19
|
import { RouterButton } from 'widgets/Dashboard/components/RouterButton';
|
|
20
20
|
import { useEnroll } from 'widgets/Dashboard/hooks/useEnroll';
|
|
21
|
+
import { OrderHelper } from 'utils/OrderHelper';
|
|
21
22
|
import useCourseRunPeriodMessage from './hooks/useCourseRunPeriodMessage';
|
|
22
23
|
|
|
23
24
|
const messages = defineMessages({
|
|
@@ -212,7 +213,9 @@ export const DashboardItemCourseEnrollingRun = ({
|
|
|
212
213
|
const intl = useIntl();
|
|
213
214
|
const formatDate = useDateFormat();
|
|
214
215
|
const courseRunPeriodMessage = useCourseRunPeriodMessage(courseRun, selected);
|
|
215
|
-
const haveToSignContract = order
|
|
216
|
+
const haveToSignContract = order
|
|
217
|
+
? OrderHelper.orderNeedsSignature(order, product?.contract_definition)
|
|
218
|
+
: false;
|
|
216
219
|
const isOpenedForEnrollment = useMemo(
|
|
217
220
|
() => courseRun.state.priority < Priority.FUTURE_NOT_YET_OPEN,
|
|
218
221
|
[courseRun],
|
package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx
CHANGED
|
@@ -6,8 +6,8 @@ import { CertificateProduct, Enrollment, ProductType } from 'types/Joanie';
|
|
|
6
6
|
import DownloadCertificateButton from 'components/DownloadCertificateButton';
|
|
7
7
|
import { useCertificate } from 'hooks/useCertificates';
|
|
8
8
|
import { isOpenedCourseRunCertificate } from 'utils/CourseRuns';
|
|
9
|
+
import { OrderHelper } from 'utils/OrderHelper';
|
|
9
10
|
import CertificateStatus from '../../CertificateStatus';
|
|
10
|
-
import { getActiveEnrollmentOrder } from '../../utils/order';
|
|
11
11
|
|
|
12
12
|
const messages = defineMessages({
|
|
13
13
|
buyProductCertificateLabel: {
|
|
@@ -37,7 +37,7 @@ const ProductCertificateFooter = ({ product, enrollment }: ProductCertificateFoo
|
|
|
37
37
|
return null;
|
|
38
38
|
}
|
|
39
39
|
const [activeOrder, setActiveOrder] = useState(
|
|
40
|
-
getActiveEnrollmentOrder(enrollment.orders || [], product.id),
|
|
40
|
+
OrderHelper.getActiveEnrollmentOrder(enrollment.orders || [], product.id),
|
|
41
41
|
);
|
|
42
42
|
const { item: certificate } = useCertificate(activeOrder?.certificate_id);
|
|
43
43
|
|
|
@@ -10,7 +10,7 @@ import { RouterButton } from 'widgets/Dashboard/components/RouterButton';
|
|
|
10
10
|
import { LearnerDashboardPaths } from 'widgets/Dashboard/utils/learnerRouteMessages';
|
|
11
11
|
import { getDashboardRoutePath } from 'widgets/Dashboard/utils/dashboardRoutes';
|
|
12
12
|
import { useCourseProduct } from 'hooks/useCourseProducts';
|
|
13
|
-
import {
|
|
13
|
+
import { OrderHelper } from 'utils/OrderHelper';
|
|
14
14
|
|
|
15
15
|
import { DashboardSubItemsList } from '../DashboardSubItemsList';
|
|
16
16
|
import { DashboardItemCourseEnrolling } from '../CourseEnrolling';
|
|
@@ -88,7 +88,7 @@ export const DashboardItemOrder = ({
|
|
|
88
88
|
states: { isFetched: isCourseProductRelationFetched },
|
|
89
89
|
} = useCourseProduct({ product_id: order.product_id, course_id: course.code });
|
|
90
90
|
const { product } = courseProductRelation || {};
|
|
91
|
-
const needsSignature = orderNeedsSignature(order, product);
|
|
91
|
+
const needsSignature = OrderHelper.orderNeedsSignature(order, product?.contract_definition);
|
|
92
92
|
const getRoutePath = getDashboardRoutePath(useIntl());
|
|
93
93
|
|
|
94
94
|
return (
|
|
@@ -98,7 +98,7 @@ export const DashboardItemOrder = ({
|
|
|
98
98
|
key={`DashboardItemOrderContract_${order.id}`}
|
|
99
99
|
title={product.title}
|
|
100
100
|
order={order}
|
|
101
|
-
contract_definition={product
|
|
101
|
+
contract_definition={product?.contract_definition!}
|
|
102
102
|
contract={order.contract}
|
|
103
103
|
writable={writable}
|
|
104
104
|
mode="compact"
|
|
@@ -114,7 +114,10 @@ export const DashboardItemOrder = ({
|
|
|
114
114
|
<div className="dashboard-item-order__footer">
|
|
115
115
|
<div className="dashboard-item__block__status">
|
|
116
116
|
<Icon name={IconTypeEnum.SCHOOL} />
|
|
117
|
-
<OrderStateMessage
|
|
117
|
+
<OrderStateMessage
|
|
118
|
+
order={order}
|
|
119
|
+
contractDefinition={product?.contract_definition}
|
|
120
|
+
/>
|
|
118
121
|
</div>
|
|
119
122
|
{showDetailsButton && (
|
|
120
123
|
<RouterButton
|
|
@@ -132,7 +135,7 @@ export const DashboardItemOrder = ({
|
|
|
132
135
|
key={`DashboardItemOrderContract_${order.id}`}
|
|
133
136
|
title={product.title}
|
|
134
137
|
order={order}
|
|
135
|
-
contract_definition={product
|
|
138
|
+
contract_definition={product?.contract_definition!}
|
|
136
139
|
contract={order.contract}
|
|
137
140
|
writable={writable}
|
|
138
141
|
mode="compact"
|
|
@@ -173,7 +176,7 @@ export const DashboardItemOrder = ({
|
|
|
173
176
|
key={`DashboardItemOrderContract_${order.id}`}
|
|
174
177
|
title={product.title}
|
|
175
178
|
order={order}
|
|
176
|
-
contract_definition={product
|
|
179
|
+
contract_definition={product?.contract_definition!}
|
|
177
180
|
contract={order.contract}
|
|
178
181
|
writable={writable}
|
|
179
182
|
mode="compact"
|
package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.spec.tsx
CHANGED
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
ContractDefinitionFactory,
|
|
6
6
|
ContractFactory,
|
|
7
7
|
CredentialOrderFactory,
|
|
8
|
-
ProductFactory,
|
|
9
8
|
} from 'utils/test/factories/joanie';
|
|
10
9
|
import { OrderState } from 'types/Joanie';
|
|
11
10
|
import OrderStateMessage, { messages } from '.';
|
|
@@ -74,13 +73,11 @@ describe('<DashboardItemOrder/>', () => {
|
|
|
74
73
|
contract: null,
|
|
75
74
|
}).one();
|
|
76
75
|
|
|
77
|
-
const
|
|
78
|
-
contract_definition: ContractDefinitionFactory().one(),
|
|
79
|
-
}).one();
|
|
76
|
+
const contractDefinition = ContractDefinitionFactory().one();
|
|
80
77
|
|
|
81
78
|
render(
|
|
82
79
|
<Wrapper>
|
|
83
|
-
<OrderStateMessage order={order}
|
|
80
|
+
<OrderStateMessage order={order} contractDefinition={contractDefinition} />
|
|
84
81
|
</Wrapper>,
|
|
85
82
|
);
|
|
86
83
|
expect(screen.getByText('Signature required')).toBeInTheDocument();
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { FormattedMessage, defineMessages } from 'react-intl';
|
|
2
2
|
import { useEffect } from 'react';
|
|
3
|
-
import { CertificateOrder, CredentialOrder, OrderState,
|
|
3
|
+
import { CertificateOrder, CredentialOrder, OrderState, ContractDefinition } from 'types/Joanie';
|
|
4
4
|
import { StringHelper } from 'utils/StringHelper';
|
|
5
5
|
import { handle } from 'utils/errors/handle';
|
|
6
|
-
import {
|
|
6
|
+
import { OrderHelper } from 'utils/OrderHelper';
|
|
7
7
|
|
|
8
8
|
export const messages = defineMessages({
|
|
9
9
|
statusDraft: {
|
|
@@ -53,10 +53,10 @@ export const messages = defineMessages({
|
|
|
53
53
|
|
|
54
54
|
interface OrderStateMessageProps {
|
|
55
55
|
order: CredentialOrder | CertificateOrder;
|
|
56
|
-
|
|
56
|
+
contractDefinition?: ContractDefinition;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const OrderStateMessage = ({ order,
|
|
59
|
+
const OrderStateMessage = ({ order, contractDefinition }: OrderStateMessageProps) => {
|
|
60
60
|
const { certificate_id: certificateId } = order;
|
|
61
61
|
const orderStatusMessages = {
|
|
62
62
|
[OrderState.DRAFT]: messages.statusDraft,
|
|
@@ -72,7 +72,7 @@ const OrderStateMessage = ({ order, product }: OrderStateMessageProps) => {
|
|
|
72
72
|
}, [order.state]);
|
|
73
73
|
|
|
74
74
|
if (order.state === OrderState.VALIDATED) {
|
|
75
|
-
if (orderNeedsSignature(order,
|
|
75
|
+
if (OrderHelper.orderNeedsSignature(order, contractDefinition)) {
|
|
76
76
|
return <FormattedMessage {...messages.statusWaitingSignature} />;
|
|
77
77
|
}
|
|
78
78
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { DashboardAvatar, DashboardAvatarProps } from '../DashboardAvatar';
|
|
2
|
+
|
|
3
|
+
const DashboardListAvatar = (props: DashboardAvatarProps) => {
|
|
4
|
+
return (
|
|
5
|
+
<div className="dashboard-list-avatar__container">
|
|
6
|
+
<DashboardAvatar {...props} />
|
|
7
|
+
</div>
|
|
8
|
+
);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export default DashboardListAvatar;
|
|
@@ -7,7 +7,7 @@ import Banner, { BannerType } from 'components/Banner';
|
|
|
7
7
|
import { useCourseProduct } from 'hooks/useCourseProducts';
|
|
8
8
|
import { isCredentialOrder } from 'pages/DashboardCourses/useOrdersEnrollments';
|
|
9
9
|
import { handle } from 'utils/errors/handle';
|
|
10
|
-
import {
|
|
10
|
+
import { OrderHelper } from 'utils/OrderHelper';
|
|
11
11
|
import { DashboardItemOrder } from '../DashboardItem/Order/DashboardItemOrder';
|
|
12
12
|
|
|
13
13
|
const messages = defineMessages({
|
|
@@ -50,7 +50,10 @@ export const DashboardOrderLoader = () => {
|
|
|
50
50
|
const error = errorOrder || errorCourseProduct || wrongLinkedProductError;
|
|
51
51
|
|
|
52
52
|
const fetching = fetchingOrder || fetchingCourseProduct;
|
|
53
|
-
const needsSignature = orderNeedsSignature(
|
|
53
|
+
const needsSignature = OrderHelper.orderNeedsSignature(
|
|
54
|
+
order,
|
|
55
|
+
courseProduct?.product.contract_definition,
|
|
56
|
+
);
|
|
54
57
|
|
|
55
58
|
return (
|
|
56
59
|
<>
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
2
|
+
import { Select, SelectProps } from '@openfun/cunningham-react';
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
import { useOrganizations } from 'hooks/useOrganizations';
|
|
5
|
+
import { Spinner } from 'components/Spinner';
|
|
6
|
+
|
|
7
|
+
export const messages = defineMessages({
|
|
8
|
+
organizationFilterLabel: {
|
|
9
|
+
defaultMessage: 'Organization',
|
|
10
|
+
description: 'Use as organization filter label',
|
|
11
|
+
id: 'components.ListFilterOrganization.organizationFilterLabel',
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
interface FilterOrganizationProps {
|
|
16
|
+
defaultValue?: string;
|
|
17
|
+
onChange: ({ organization_id }: { organization_id?: string }) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const FilterOrganization = ({ defaultValue, onChange }: FilterOrganizationProps) => {
|
|
21
|
+
const intl = useIntl();
|
|
22
|
+
const {
|
|
23
|
+
items: organizations,
|
|
24
|
+
states: { isFetched },
|
|
25
|
+
} = useOrganizations();
|
|
26
|
+
|
|
27
|
+
const organizationOptions = organizations.map((organization) => ({
|
|
28
|
+
label: organization.title,
|
|
29
|
+
value: organization.id,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const handleChange: SelectProps['onChange'] = (e) => {
|
|
33
|
+
const value = e.target.value as string;
|
|
34
|
+
onChange({ organization_id: value });
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (isFetched && defaultValue === undefined) {
|
|
39
|
+
onChange({ organization_id: organizationOptions[0]?.value });
|
|
40
|
+
}
|
|
41
|
+
}, [defaultValue, isFetched]);
|
|
42
|
+
|
|
43
|
+
if (!isFetched) return <Spinner />;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Select
|
|
47
|
+
label={intl.formatMessage(messages.organizationFilterLabel)}
|
|
48
|
+
options={organizationOptions}
|
|
49
|
+
defaultValue={defaultValue || organizationOptions[0].value}
|
|
50
|
+
onChange={handleChange}
|
|
51
|
+
disabled={!isFetched}
|
|
52
|
+
clearable={false}
|
|
53
|
+
searchable={true}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default FilterOrganization;
|
|
@@ -12,7 +12,7 @@ import { IntlHelper } from 'utils/IntlHelper';
|
|
|
12
12
|
import WebAnalyticsAPIHandler from 'api/web-analytics';
|
|
13
13
|
import EnrollmentDate from 'components/EnrollmentDate';
|
|
14
14
|
import { Product } from 'types/Joanie';
|
|
15
|
-
import {
|
|
15
|
+
import { OrderHelper } from 'utils/OrderHelper';
|
|
16
16
|
import { messages as sharedMessages } from '../CourseRunItem';
|
|
17
17
|
import CourseRunSection, { messages as sectionMessages } from './CourseRunSection';
|
|
18
18
|
|
|
@@ -60,7 +60,9 @@ const EnrollableCourseRunList = ({ courseRuns, order, product }: Props) => {
|
|
|
60
60
|
const intl = useIntl();
|
|
61
61
|
const formatDate = useDateFormat();
|
|
62
62
|
const formRef = useRef<HTMLFormElement>(null);
|
|
63
|
-
const needsSignature = order
|
|
63
|
+
const needsSignature = order
|
|
64
|
+
? OrderHelper.orderNeedsSignature(order, product.contract_definition)
|
|
65
|
+
: false;
|
|
64
66
|
|
|
65
67
|
const [selectedCourseRun, setSelectedCourseRun] = useState<Maybe<Joanie.CourseRun>>();
|
|
66
68
|
const [submitted, setSubmitted] = useState(false);
|
|
@@ -16,7 +16,7 @@ import { Maybe } from 'types/utils';
|
|
|
16
16
|
import useDateFormat from 'hooks/useDateFormat';
|
|
17
17
|
import { ProductHelper } from 'utils/ProductHelper';
|
|
18
18
|
import useProductOrder from 'hooks/useProductOrder';
|
|
19
|
-
import {
|
|
19
|
+
import { OrderHelper } from 'utils/OrderHelper';
|
|
20
20
|
import { handle } from 'utils/errors/handle';
|
|
21
21
|
import { ProductSignatureHeader } from 'widgets/SyllabusCourseRunsList/components/CourseProductItem/components/ProductSignatureHeader';
|
|
22
22
|
import CertificateItem from './components/CourseProductCertificateItem';
|
|
@@ -129,7 +129,9 @@ const Header = ({ product, order, hasPurchased, canPurchase, compact }: HeaderPr
|
|
|
129
129
|
);
|
|
130
130
|
};
|
|
131
131
|
const Content = ({ product, order }: { product: Product; order?: CredentialOrder }) => {
|
|
132
|
-
const needsSignature = order
|
|
132
|
+
const needsSignature = order
|
|
133
|
+
? OrderHelper.orderNeedsSignature(order, product.contract_definition)
|
|
134
|
+
: false;
|
|
133
135
|
const targetCourses = useMemo(() => {
|
|
134
136
|
if (order) {
|
|
135
137
|
return order.target_courses;
|
package/package.json
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
@import '../../js/widgets/Dashboard/components/DashboardItem/Contract/_styles';
|
|
34
34
|
@import '../../js/widgets/Dashboard/components/DashboardItem/styles';
|
|
35
35
|
@import '../../js/widgets/Dashboard/components/DashboardLayout/styles';
|
|
36
|
+
@import '../../js/widgets/Dashboard/components/DashboardListAvatar/styles';
|
|
36
37
|
@import '../../js/widgets/Dashboard/components/DashboardOrderLoader/styles';
|
|
37
38
|
@import '../../js/widgets/Dashboard/components/DashboardBreadcrumbs/styles';
|
|
38
39
|
@import '../../js/widgets/Dashboard/components/DashboardSidebar/styles';
|
|
@@ -28,14 +28,7 @@ $r-course-glimpse-gutter: 0.8rem !default;
|
|
|
28
28
|
|
|
29
29
|
&__count {
|
|
30
30
|
margin-right: $r-course-glimpse-gutter;
|
|
31
|
-
padding: 0;
|
|
32
|
-
flex-basis: 100%; // Should not wrap with actual course glimpses
|
|
33
|
-
color: r-theme-val(course-glimpse-list, count-color);
|
|
34
|
-
text-align: right;
|
|
35
|
-
|
|
36
31
|
@include media-breakpoint-up(lg) {
|
|
37
|
-
padding: 0;
|
|
38
|
-
|
|
39
32
|
&:first-child {
|
|
40
33
|
margin-top: -1rem; // Cancel out top padding
|
|
41
34
|
}
|
package/scss/objects/_index.scss
CHANGED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { OrderEnrollment, ACTIVE_ORDER_STATES, Order, Product, OrderState } from 'types/Joanie';
|
|
2
|
-
|
|
3
|
-
export const getActiveEnrollmentOrder = (orders: OrderEnrollment[], productId: string) => {
|
|
4
|
-
const filter = (order: OrderEnrollment) =>
|
|
5
|
-
ACTIVE_ORDER_STATES.includes(order.state) && order.product_id === productId;
|
|
6
|
-
return orders.find(filter);
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export const orderNeedsSignature = (order: Order, product?: Product) => {
|
|
10
|
-
return (
|
|
11
|
-
order?.state === OrderState.VALIDATED &&
|
|
12
|
-
product?.contract_definition &&
|
|
13
|
-
!(order.contract && order.contract.student_signed_on)
|
|
14
|
-
);
|
|
15
|
-
};
|