richie-education 3.4.1-dev3 → 3.5.1-dev1
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/.eslintrc.json +32 -0
- package/i18n/locales/ar-SA.json +66 -6
- package/i18n/locales/es-ES.json +66 -6
- package/i18n/locales/fa-IR.json +66 -6
- package/i18n/locales/fr-CA.json +66 -6
- package/i18n/locales/fr-FR.json +65 -5
- package/i18n/locales/ko-KR.json +66 -6
- package/i18n/locales/pt-PT.json +66 -6
- package/i18n/locales/ru-RU.json +66 -6
- package/i18n/locales/vi-VN.json +66 -6
- package/js/api/joanie.ts +26 -0
- package/js/api/lms/index.spec.ts +33 -0
- package/js/api/lms/index.ts +1 -1
- package/js/api/lms/openedx-hawthorn.spec.ts +49 -0
- package/js/api/lms/openedx-hawthorn.ts +5 -2
- package/js/api/utils.ts +4 -3
- package/js/components/DownloadAgreementButton/index.tsx +51 -0
- package/js/components/DownloadBatchOrderSeatsButton/index.spec.tsx +46 -0
- package/js/components/DownloadBatchOrderSeatsButton/index.tsx +97 -0
- package/js/components/Icon/index.tsx +1 -0
- package/js/components/SaleTunnel/SaleTunnelInformation/SaleTunnelInformationGroup.tsx +1 -1
- package/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx +1 -1
- package/js/components/SaleTunnel/SubscriptionButton/index.tsx +2 -1
- package/js/components/SaleTunnel/_styles.scss +4 -1
- package/js/components/SaleTunnel/index.full-process-b2b.spec.tsx +5 -5
- package/js/components/SaleTunnel/index.full-process-b2c.spec.tsx +1 -1
- package/js/hooks/useBatchOrder/index.tsx +21 -1
- package/js/hooks/useDownloadAgreement/index.spec.tsx +136 -0
- package/js/hooks/useDownloadAgreement/index.tsx +25 -0
- package/js/hooks/useDownloadBatchOrderSeats/index.spec.tsx +160 -0
- package/js/hooks/useDownloadBatchOrderSeats/index.tsx +34 -0
- package/js/pages/DashboardBatchOrderLayout/index.spec.tsx +19 -2
- package/js/pages/TeacherDashboardOrganizationQuotes/BatchOrderSeatInfoQuote.tsx +112 -0
- package/js/pages/TeacherDashboardOrganizationQuotes/_styles.scss +17 -0
- package/js/pages/TeacherDashboardOrganizationQuotes/index.full-process.spec.tsx +5 -2
- package/js/pages/TeacherDashboardOrganizationQuotes/index.spec.tsx +7 -3
- package/js/pages/TeacherDashboardOrganizationQuotes/index.tsx +39 -27
- package/js/translations/ar-SA.json +1 -1
- package/js/translations/es-ES.json +1 -1
- package/js/translations/fa-IR.json +1 -1
- package/js/translations/fr-CA.json +1 -1
- package/js/translations/fr-FR.json +1 -1
- package/js/translations/ko-KR.json +1 -1
- package/js/translations/pt-PT.json +1 -1
- package/js/translations/ru-RU.json +1 -1
- package/js/translations/vi-VN.json +1 -1
- package/js/types/Joanie.ts +21 -1
- package/js/types/api.ts +1 -0
- package/js/types/commonDataProps.ts +2 -0
- package/js/utils/download.ts +3 -1
- package/js/utils/errors/HttpError.ts +1 -0
- package/js/utils/test/factories/joanie.ts +15 -1
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderAgreementInfo.tsx +72 -0
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderSeatInfo.spec.tsx +114 -0
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderSeatInfo.tsx +137 -0
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/DashboardBatchOrderSubItems.tsx +22 -5
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/batchOrderSeatInfoMessages.ts +24 -0
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/index.tsx +16 -3
- package/js/widgets/Dashboard/components/DashboardItem/_styles.scss +6 -2
- package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +2 -2
- package/js/widgets/Slider/components/SlidePanel.tsx +13 -1
- package/js/widgets/Slider/index.spec.tsx +3 -5
- package/package.json +8 -8
- package/scss/components/templates/richie/slider/_slider.scss +10 -0
- package/scss/objects/_dashboard.scss +84 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
import fetchMock from 'fetch-mock';
|
|
4
|
+
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
5
|
+
import { BatchOrderReadFactory, BatchOrderSeatFactory } from 'utils/test/factories/joanie';
|
|
6
|
+
import { BatchOrderState } from 'types/Joanie';
|
|
7
|
+
import { HttpStatusCode } from 'utils/errors/HttpError';
|
|
8
|
+
import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
|
|
9
|
+
import { render } from 'utils/test/render';
|
|
10
|
+
import { expectBannerError } from 'utils/test/expectBanner';
|
|
11
|
+
import { BatchOrderSeatInfo } from './BatchOrderSeatInfo';
|
|
12
|
+
|
|
13
|
+
jest.mock('utils/context', () => ({
|
|
14
|
+
__esModule: true,
|
|
15
|
+
default: mockRichieContextFactory({
|
|
16
|
+
authentication: { backend: 'fonzie', endpoint: 'https://auth.test' },
|
|
17
|
+
joanie_backend: { endpoint: 'https://joanie.endpoint' },
|
|
18
|
+
}).one(),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
describe('<BatchOrderSeatInfo />', () => {
|
|
22
|
+
setupJoanieSession();
|
|
23
|
+
|
|
24
|
+
const paginatedResponse = (results: object[], count?: number) => ({
|
|
25
|
+
results,
|
|
26
|
+
count: count ?? results.length,
|
|
27
|
+
next: null,
|
|
28
|
+
previous: null,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('renders enrollment progress and seat list, and searches by query param', async () => {
|
|
32
|
+
const ownedSeat = BatchOrderSeatFactory({ owner_name: 'Alice Martin' }).one();
|
|
33
|
+
const voucherSeat = BatchOrderSeatFactory().one();
|
|
34
|
+
const batchOrder = BatchOrderReadFactory({
|
|
35
|
+
state: BatchOrderState.COMPLETED,
|
|
36
|
+
nb_seats: 10,
|
|
37
|
+
seats_owned: 1,
|
|
38
|
+
seats_to_own: 9,
|
|
39
|
+
}).one();
|
|
40
|
+
|
|
41
|
+
fetchMock.get(
|
|
42
|
+
`begin:https://joanie.endpoint/api/v1.0/batch-orders/${batchOrder.id}/seats/`,
|
|
43
|
+
paginatedResponse([ownedSeat, voucherSeat], 10),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
render(<BatchOrderSeatInfo batchOrder={batchOrder} />);
|
|
47
|
+
|
|
48
|
+
expect(await screen.findByText('1/10 enrolled participants')).toBeVisible();
|
|
49
|
+
expect(await screen.findByText('Alice Martin')).toBeVisible();
|
|
50
|
+
expect(await screen.findByText(voucherSeat.voucher!)).toBeVisible();
|
|
51
|
+
|
|
52
|
+
const user = userEvent.setup();
|
|
53
|
+
await user.type(screen.getByRole('textbox'), 'Alice');
|
|
54
|
+
|
|
55
|
+
await waitFor(() => {
|
|
56
|
+
const urls = fetchMock
|
|
57
|
+
.calls(`begin:https://joanie.endpoint/api/v1.0/batch-orders/${batchOrder.id}/seats/`)
|
|
58
|
+
.map(([url]) => url);
|
|
59
|
+
expect(urls.some((url) => url.includes('query=Alice'))).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('loads more seats when clicking the load more button', async () => {
|
|
64
|
+
const firstPage = BatchOrderSeatFactory().many(10);
|
|
65
|
+
const secondPage = BatchOrderSeatFactory().many(5);
|
|
66
|
+
const batchOrder = BatchOrderReadFactory({
|
|
67
|
+
state: BatchOrderState.COMPLETED,
|
|
68
|
+
nb_seats: 15,
|
|
69
|
+
seats_owned: 15,
|
|
70
|
+
seats_to_own: 0,
|
|
71
|
+
}).one();
|
|
72
|
+
|
|
73
|
+
fetchMock.get(
|
|
74
|
+
`https://joanie.endpoint/api/v1.0/batch-orders/${batchOrder.id}/seats/?page=1&page_size=10`,
|
|
75
|
+
{ results: firstPage, count: 15, next: 'next-url', previous: null },
|
|
76
|
+
);
|
|
77
|
+
fetchMock.get(
|
|
78
|
+
`https://joanie.endpoint/api/v1.0/batch-orders/${batchOrder.id}/seats/?page=2&page_size=10`,
|
|
79
|
+
{ results: secondPage, count: 15, next: null, previous: 'prev-url' },
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
render(<BatchOrderSeatInfo batchOrder={batchOrder} />);
|
|
83
|
+
|
|
84
|
+
expect(await screen.findByText(firstPage[0].owner_name ?? firstPage[0].voucher!)).toBeVisible();
|
|
85
|
+
expect(screen.queryByText(secondPage[0].owner_name ?? secondPage[0].voucher!)).toBeNull();
|
|
86
|
+
expect(screen.getByRole('button', { name: 'Load 5 more' })).toBeVisible();
|
|
87
|
+
|
|
88
|
+
const user = userEvent.setup();
|
|
89
|
+
await user.click(screen.getByRole('button', { name: 'Load 5 more' }));
|
|
90
|
+
|
|
91
|
+
expect(
|
|
92
|
+
await screen.findByText(secondPage[0].owner_name ?? secondPage[0].voucher!),
|
|
93
|
+
).toBeVisible();
|
|
94
|
+
expect(screen.queryByText('Load 5 more')).toBeNull();
|
|
95
|
+
expect(screen.getByText(firstPage[0].owner_name ?? firstPage[0].voucher!)).toBeVisible();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('shows an error banner when the seats API fails', async () => {
|
|
99
|
+
const batchOrder = BatchOrderReadFactory({
|
|
100
|
+
state: BatchOrderState.COMPLETED,
|
|
101
|
+
nb_seats: 10,
|
|
102
|
+
seats_owned: 1,
|
|
103
|
+
seats_to_own: 9,
|
|
104
|
+
}).one();
|
|
105
|
+
|
|
106
|
+
fetchMock.get(`begin:https://joanie.endpoint/api/v1.0/batch-orders/${batchOrder.id}/seats/`, {
|
|
107
|
+
status: HttpStatusCode.INTERNAL_SERVER_ERROR,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
render(<BatchOrderSeatInfo batchOrder={batchOrder} />);
|
|
111
|
+
|
|
112
|
+
await expectBannerError('An error occurred while fetching resources. Please retry later.');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|
3
|
+
import { Button, Input } from '@openfun/cunningham-react';
|
|
4
|
+
import { Icon, IconTypeEnum } from 'components/Icon';
|
|
5
|
+
import Banner, { BannerType } from 'components/Banner';
|
|
6
|
+
import { DashboardSubItem } from 'widgets/Dashboard/components/DashboardItem/DashboardSubItem';
|
|
7
|
+
import { useBatchOrderSeats } from 'hooks/useBatchOrder';
|
|
8
|
+
import DownloadBatchOrderSeatsButton from 'components/DownloadBatchOrderSeatsButton';
|
|
9
|
+
import { BatchOrderRead, BatchOrderSeat } from 'types/Joanie';
|
|
10
|
+
import { batchOrderSeatInfoMessages } from './batchOrderSeatInfoMessages';
|
|
11
|
+
|
|
12
|
+
const messages = defineMessages({
|
|
13
|
+
enrollmentManagement: {
|
|
14
|
+
id: 'batchOrder.enrollmentManagement.title',
|
|
15
|
+
description: 'Title for enrollment management section',
|
|
16
|
+
defaultMessage: 'Enrollment',
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const ITEMS_PER_PAGE = 10;
|
|
21
|
+
|
|
22
|
+
interface BatchOrderSeatInfoProps {
|
|
23
|
+
batchOrder: BatchOrderRead;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const BatchOrderSeatInfo = ({ batchOrder }: BatchOrderSeatInfoProps) => {
|
|
27
|
+
const intl = useIntl();
|
|
28
|
+
const [query, setQuery] = useState('');
|
|
29
|
+
const [page, setPage] = useState(1);
|
|
30
|
+
const [allSeats, setAllSeats] = useState<BatchOrderSeat[]>([]);
|
|
31
|
+
|
|
32
|
+
const seatsOwnedCount = batchOrder.seats_owned ?? 0;
|
|
33
|
+
|
|
34
|
+
const {
|
|
35
|
+
items: seats,
|
|
36
|
+
meta,
|
|
37
|
+
states,
|
|
38
|
+
} = useBatchOrderSeats(
|
|
39
|
+
{
|
|
40
|
+
batch_order_id: batchOrder.id,
|
|
41
|
+
query: query || undefined,
|
|
42
|
+
page,
|
|
43
|
+
page_size: ITEMS_PER_PAGE,
|
|
44
|
+
},
|
|
45
|
+
{ enabled: !!batchOrder.id },
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (page === 1) {
|
|
50
|
+
setAllSeats(seats);
|
|
51
|
+
} else if (seats.length > 0) {
|
|
52
|
+
setAllSeats((prev) => [...prev, ...seats]);
|
|
53
|
+
}
|
|
54
|
+
}, [seats]);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
setPage(1);
|
|
58
|
+
}, [query]);
|
|
59
|
+
|
|
60
|
+
const totalCount = meta?.pagination?.count ?? 0;
|
|
61
|
+
const remainingCount = Math.min(ITEMS_PER_PAGE, totalCount - allSeats.length);
|
|
62
|
+
|
|
63
|
+
if (
|
|
64
|
+
!batchOrder.nb_seats ||
|
|
65
|
+
batchOrder.seats_owned === undefined ||
|
|
66
|
+
batchOrder.seats_to_own === undefined
|
|
67
|
+
) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<DashboardSubItem
|
|
73
|
+
title={intl.formatMessage(messages.enrollmentManagement)}
|
|
74
|
+
footer={
|
|
75
|
+
<div className="content">
|
|
76
|
+
<div className="enrollment-progress">
|
|
77
|
+
<span className="dashboard-item__label">
|
|
78
|
+
{intl.formatMessage(batchOrderSeatInfoMessages.enrolledParticipants, {
|
|
79
|
+
seats_owned: seatsOwnedCount,
|
|
80
|
+
nb_seats: batchOrder.nb_seats,
|
|
81
|
+
})}
|
|
82
|
+
</span>
|
|
83
|
+
<div className="enrollment-progress__bar">
|
|
84
|
+
<div
|
|
85
|
+
className="enrollment-progress__bar__fill"
|
|
86
|
+
style={{ width: `${(seatsOwnedCount / batchOrder.nb_seats) * 100}%` }}
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
{states.error && <Banner message={states.error} type={BannerType.ERROR} />}
|
|
91
|
+
<div className="enrollment-nested-section__content">
|
|
92
|
+
<Input
|
|
93
|
+
className="enrollment-search"
|
|
94
|
+
label={intl.formatMessage(batchOrderSeatInfoMessages.searchPlaceholder)}
|
|
95
|
+
value={query}
|
|
96
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
97
|
+
rightIcon={<Icon name={IconTypeEnum.MAGNIFYING_GLASS} size="small" />}
|
|
98
|
+
/>
|
|
99
|
+
{allSeats.length === 0 && query ? (
|
|
100
|
+
<FormattedMessage {...batchOrderSeatInfoMessages.noResults} />
|
|
101
|
+
) : (
|
|
102
|
+
<>
|
|
103
|
+
<ul className="enrollment-list">
|
|
104
|
+
{allSeats.map((seat) => (
|
|
105
|
+
<li key={seat.id}>{seat.owner_name ?? seat.voucher}</li>
|
|
106
|
+
))}
|
|
107
|
+
</ul>
|
|
108
|
+
<div className="enrollment-actions">
|
|
109
|
+
{remainingCount > 0 && (
|
|
110
|
+
<Button
|
|
111
|
+
className="enrollment-load-more"
|
|
112
|
+
color="brand"
|
|
113
|
+
variant="secondary"
|
|
114
|
+
size="small"
|
|
115
|
+
onClick={() => setPage((p) => p + 1)}
|
|
116
|
+
disabled={states.fetching}
|
|
117
|
+
>
|
|
118
|
+
{intl.formatMessage(batchOrderSeatInfoMessages.loadMore, {
|
|
119
|
+
count: remainingCount,
|
|
120
|
+
})}
|
|
121
|
+
</Button>
|
|
122
|
+
)}
|
|
123
|
+
<div>
|
|
124
|
+
<DownloadBatchOrderSeatsButton
|
|
125
|
+
batchOrderId={batchOrder.id}
|
|
126
|
+
productTitle={batchOrder.offering?.product.title ?? ''}
|
|
127
|
+
/>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
}
|
|
135
|
+
/>
|
|
136
|
+
);
|
|
137
|
+
};
|
package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/DashboardBatchOrderSubItems.tsx
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|
2
2
|
import { PaymentMethod } from 'components/PaymentInterfaces/types';
|
|
3
|
-
import { BatchOrderRead } from 'types/Joanie';
|
|
3
|
+
import { BatchOrderRead, BatchOrderState } from 'types/Joanie';
|
|
4
4
|
import { DashboardSubItem } from 'widgets/Dashboard/components/DashboardItem/DashboardSubItem';
|
|
5
5
|
import { DashboardSubItemsList } from '../DashboardSubItemsList';
|
|
6
|
+
import { BatchOrderSeatInfo } from './BatchOrderSeatInfo';
|
|
7
|
+
import { BatchOrderAgreementInfo } from './BatchOrderAgreementInfo';
|
|
6
8
|
|
|
7
9
|
const messages = defineMessages({
|
|
8
10
|
stepCompany: {
|
|
@@ -30,9 +32,10 @@ const messages = defineMessages({
|
|
|
30
32
|
description: 'section with details about the person responsible for signing the quote',
|
|
31
33
|
defaultMessage: 'Signatory',
|
|
32
34
|
},
|
|
33
|
-
|
|
34
|
-
id: 'batchOrder.seats',
|
|
35
|
-
description:
|
|
35
|
+
labelSeats: {
|
|
36
|
+
id: 'batchOrder.label.seats',
|
|
37
|
+
description:
|
|
38
|
+
'Label displayed for the seats count field in the batch order participants sub-item',
|
|
36
39
|
defaultMessage: 'Seats',
|
|
37
40
|
},
|
|
38
41
|
labelName: {
|
|
@@ -144,6 +147,12 @@ const DashboardItemField = ({
|
|
|
144
147
|
export const DashboardBatchOrderSubItems = ({ batchOrder }: { batchOrder: BatchOrderRead }) => {
|
|
145
148
|
const intl = useIntl();
|
|
146
149
|
|
|
150
|
+
const displaySeatsInfo =
|
|
151
|
+
batchOrder.state === BatchOrderState.COMPLETED &&
|
|
152
|
+
!!batchOrder.nb_seats &&
|
|
153
|
+
batchOrder.seats_owned !== undefined &&
|
|
154
|
+
batchOrder.seats_to_own !== undefined;
|
|
155
|
+
|
|
147
156
|
const items = [
|
|
148
157
|
<DashboardSubItem
|
|
149
158
|
key="company"
|
|
@@ -235,7 +244,7 @@ export const DashboardBatchOrderSubItems = ({ batchOrder }: { batchOrder: BatchO
|
|
|
235
244
|
footer={
|
|
236
245
|
<div className="content">
|
|
237
246
|
<DashboardItemField
|
|
238
|
-
label={<FormattedMessage {...messages.
|
|
247
|
+
label={<FormattedMessage {...messages.labelSeats} />}
|
|
239
248
|
value={batchOrder.nb_seats}
|
|
240
249
|
/>
|
|
241
250
|
</div>
|
|
@@ -312,5 +321,13 @@ export const DashboardBatchOrderSubItems = ({ batchOrder }: { batchOrder: BatchO
|
|
|
312
321
|
);
|
|
313
322
|
}
|
|
314
323
|
|
|
324
|
+
if (batchOrder.contract_id) {
|
|
325
|
+
items.push(<BatchOrderAgreementInfo key="agreement" batchOrder={batchOrder} />);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (displaySeatsInfo) {
|
|
329
|
+
items.push(<BatchOrderSeatInfo key="enrollment-management" batchOrder={batchOrder} />);
|
|
330
|
+
}
|
|
331
|
+
|
|
315
332
|
return <DashboardSubItemsList subItems={items} />;
|
|
316
333
|
};
|
package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/batchOrderSeatInfoMessages.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { defineMessages } from 'react-intl';
|
|
2
|
+
|
|
3
|
+
export const batchOrderSeatInfoMessages = defineMessages({
|
|
4
|
+
enrolledParticipants: {
|
|
5
|
+
id: 'batchOrder.enrollmentManagement.enrolledParticipants',
|
|
6
|
+
description: 'Progress label showing enrolled participants out of total seats',
|
|
7
|
+
defaultMessage: '{seats_owned}/{nb_seats} enrolled participants',
|
|
8
|
+
},
|
|
9
|
+
searchPlaceholder: {
|
|
10
|
+
id: 'batchOrder.enrollmentManagement.searchPlaceholder',
|
|
11
|
+
description: 'Placeholder for the seat search input (student name or voucher)',
|
|
12
|
+
defaultMessage: 'Student name',
|
|
13
|
+
},
|
|
14
|
+
noResults: {
|
|
15
|
+
id: 'batchOrder.enrollmentManagement.noResults',
|
|
16
|
+
description: 'Message shown when the student search returns no results',
|
|
17
|
+
defaultMessage: 'No student matches your search.',
|
|
18
|
+
},
|
|
19
|
+
loadMore: {
|
|
20
|
+
id: 'batchOrder.enrollmentManagement.loadMore',
|
|
21
|
+
description: 'Button to load more seats',
|
|
22
|
+
defaultMessage: 'Load {count} more',
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -14,7 +14,12 @@ const messages = defineMessages({
|
|
|
14
14
|
seats: {
|
|
15
15
|
id: 'batchOrder.seats',
|
|
16
16
|
description: 'Text displayed for seats value in batch order',
|
|
17
|
-
defaultMessage: '
|
|
17
|
+
defaultMessage: '{nb_seats} seats',
|
|
18
|
+
},
|
|
19
|
+
seatsCount: {
|
|
20
|
+
id: 'batchOrder.seatsCount',
|
|
21
|
+
description: 'Text displayed for seats count in batch order (owned / total)',
|
|
22
|
+
defaultMessage: '{seats_owned}/{nb_seats} seats',
|
|
18
23
|
},
|
|
19
24
|
[BatchOrderState.DRAFT]: {
|
|
20
25
|
id: 'batchOrder.status.draft',
|
|
@@ -136,8 +141,16 @@ export const DashboardItemBatchOrder = ({
|
|
|
136
141
|
{batchOrder.nb_seats && (
|
|
137
142
|
<div className="dashboard-item__block__information">
|
|
138
143
|
<Icon name={IconTypeEnum.GROUPS} size="small" />
|
|
139
|
-
<span>
|
|
140
|
-
|
|
144
|
+
<span>
|
|
145
|
+
{batchOrder.seats_owned
|
|
146
|
+
? intl.formatMessage(messages.seatsCount, {
|
|
147
|
+
seats_owned: batchOrder.seats_owned,
|
|
148
|
+
nb_seats: batchOrder.nb_seats,
|
|
149
|
+
})
|
|
150
|
+
: intl.formatMessage(messages.seats, {
|
|
151
|
+
nb_seats: batchOrder.nb_seats,
|
|
152
|
+
})}
|
|
153
|
+
</span>
|
|
141
154
|
</div>
|
|
142
155
|
)}
|
|
143
156
|
{batchOrder.payment_method && (
|
|
@@ -162,12 +162,16 @@
|
|
|
162
162
|
padding: 0.5rem 1rem;
|
|
163
163
|
font-size: 0.8rem;
|
|
164
164
|
display: flex;
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
flex-direction: column;
|
|
166
|
+
gap: 0.5rem;
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
.dashboard-item__action-button {
|
|
172
|
+
align-self: flex-end;
|
|
173
|
+
}
|
|
174
|
+
|
|
171
175
|
.dashboard-item__course-enrolling {
|
|
172
176
|
&__infos {
|
|
173
177
|
align-items: center;
|
|
@@ -82,7 +82,7 @@ export const TEACHER_DASHBOARD_ROUTE_LABELS = defineMessages<TeacherDashboardPat
|
|
|
82
82
|
[TeacherDashboardPaths.ORGANIZATION_COURSE_PRODUCT_LEARNER_LIST]: {
|
|
83
83
|
id: 'components.TeacherDashboard.TeacherDashboardRoutes.organization.course.product.learnerList.label',
|
|
84
84
|
description: "Label to display the organization product's learner list view.",
|
|
85
|
-
defaultMessage: '
|
|
85
|
+
defaultMessage: 'Buyers',
|
|
86
86
|
},
|
|
87
87
|
[TeacherDashboardPaths.COURSE]: {
|
|
88
88
|
id: 'components.TeacherDashboard.TeacherDashboardRoutes.course.label',
|
|
@@ -102,7 +102,7 @@ export const TEACHER_DASHBOARD_ROUTE_LABELS = defineMessages<TeacherDashboardPat
|
|
|
102
102
|
[TeacherDashboardPaths.COURSE_PRODUCT_LEARNER_LIST]: {
|
|
103
103
|
id: 'components.TeacherDashboard.TeacherDashboardRoutes.course.product.learnerList.label',
|
|
104
104
|
description: "Label to display the product's learner list view.",
|
|
105
|
-
defaultMessage: '
|
|
105
|
+
defaultMessage: 'Buyers',
|
|
106
106
|
},
|
|
107
107
|
[TeacherDashboardPaths.COURSE_PRODUCT_CONTRACTS]: {
|
|
108
108
|
id: 'components.TeacherDashboard.TeacherDashboardRoutes.course.product.contracts.label',
|
|
@@ -53,7 +53,19 @@ const SlidePanel = ({
|
|
|
53
53
|
})}
|
|
54
54
|
>
|
|
55
55
|
<strong className="slide__title">
|
|
56
|
-
<span>
|
|
56
|
+
<span>
|
|
57
|
+
{slides[activeSlideIndex].link_url ? (
|
|
58
|
+
<a
|
|
59
|
+
href={slides[activeSlideIndex].link_url}
|
|
60
|
+
target={slides[activeSlideIndex].link_open_blank ? '_blank' : '_self'}
|
|
61
|
+
rel="noopener noreferrer"
|
|
62
|
+
>
|
|
63
|
+
{slides[activeSlideIndex].title}
|
|
64
|
+
</a>
|
|
65
|
+
) : (
|
|
66
|
+
slides[activeSlideIndex].title
|
|
67
|
+
)}
|
|
68
|
+
</span>
|
|
57
69
|
</strong>
|
|
58
70
|
{hasSlideContent && (
|
|
59
71
|
<div
|
|
@@ -71,13 +71,11 @@ describe('<Slider />', () => {
|
|
|
71
71
|
// Check if all slides are rendered
|
|
72
72
|
mockSlides.forEach((slide) => {
|
|
73
73
|
expect(screen.getByRole('img', { name: slide.title })).toBeInTheDocument();
|
|
74
|
-
// Check if the link is rendered
|
|
75
|
-
const link = screen.queryByRole('link', { name: slide.title });
|
|
76
74
|
if (slide.link_url) {
|
|
77
|
-
|
|
78
|
-
expect(link).
|
|
75
|
+
const links = screen.getAllByRole('link', { name: slide.title });
|
|
76
|
+
links.forEach((link) => expect(link).toHaveAttribute('href', slide.link_url));
|
|
79
77
|
} else {
|
|
80
|
-
expect(link).not.toBeInTheDocument();
|
|
78
|
+
expect(screen.queryByRole('link', { name: slide.title })).not.toBeInTheDocument();
|
|
81
79
|
}
|
|
82
80
|
});
|
|
83
81
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "richie-education",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.1-dev1",
|
|
4
4
|
"description": "A CMS to build learning portals for Open Education",
|
|
5
5
|
"main": "sandbox/manage.py",
|
|
6
6
|
"scripts": {
|
|
@@ -57,9 +57,6 @@
|
|
|
57
57
|
"@openfun/cunningham-tokens": "3.0.0",
|
|
58
58
|
"@sentry/browser": "10.40.0",
|
|
59
59
|
"@sentry/types": "10.40.0",
|
|
60
|
-
"@storybook/addon-links": "10.2.13",
|
|
61
|
-
"@storybook/react": "10.2.13",
|
|
62
|
-
"@storybook/react-webpack5": "10.2.13",
|
|
63
60
|
"@tanstack/query-core": "5.90.20",
|
|
64
61
|
"@tanstack/query-sync-storage-persister": "5.90.23",
|
|
65
62
|
"@tanstack/react-query": "5.90.21",
|
|
@@ -105,7 +102,6 @@
|
|
|
105
102
|
"eslint-plugin-prettier": "5.5.5",
|
|
106
103
|
"eslint-plugin-react": "7.37.5",
|
|
107
104
|
"eslint-plugin-react-hooks": "7.0.1",
|
|
108
|
-
"eslint-plugin-storybook": "10.2.13",
|
|
109
105
|
"fetch-mock": "<10",
|
|
110
106
|
"file-loader": "6.2.0",
|
|
111
107
|
"glob": "11.1.0",
|
|
@@ -132,11 +128,10 @@
|
|
|
132
128
|
"react-router": "7.12.0",
|
|
133
129
|
"sass": "1.97.3",
|
|
134
130
|
"source-map-loader": "5.0.0",
|
|
135
|
-
"storybook": "10.2.13",
|
|
136
131
|
"tsconfig-paths-webpack-plugin": "4.2.0",
|
|
137
132
|
"typescript": "5.9.3",
|
|
138
133
|
"uuid": "13.0.0",
|
|
139
|
-
"webpack": "5.
|
|
134
|
+
"webpack": "5.109.2",
|
|
140
135
|
"webpack-cli": "6.0.1",
|
|
141
136
|
"whatwg-fetch": "3.6.20",
|
|
142
137
|
"xhr-mock": "2.5.1",
|
|
@@ -158,6 +153,11 @@
|
|
|
158
153
|
"yarn": "1.22.22"
|
|
159
154
|
},
|
|
160
155
|
"devDependencies": {
|
|
161
|
-
"@storybook/addon-webpack5-compiler-babel": "4.0.0"
|
|
156
|
+
"@storybook/addon-webpack5-compiler-babel": "4.0.0",
|
|
157
|
+
"@storybook/addon-links": "10.2.13",
|
|
158
|
+
"@storybook/react": "10.2.13",
|
|
159
|
+
"@storybook/react-webpack5": "10.2.13",
|
|
160
|
+
"eslint-plugin-storybook": "10.2.13",
|
|
161
|
+
"storybook": "10.2.13"
|
|
162
162
|
}
|
|
163
163
|
}
|
|
@@ -201,6 +201,16 @@ $r-slider-content-line-clamp: 4 !default;
|
|
|
201
201
|
font-weight: $r-slider-title-fontweight;
|
|
202
202
|
font-family: $r-slider-title-fontfamily;
|
|
203
203
|
|
|
204
|
+
a {
|
|
205
|
+
color: inherit;
|
|
206
|
+
text-decoration: none;
|
|
207
|
+
|
|
208
|
+
&:hover,
|
|
209
|
+
&:focus {
|
|
210
|
+
opacity: 0.8;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
204
214
|
& > span {
|
|
205
215
|
display: inline-block;
|
|
206
216
|
transform: translateY(0%);
|
|
@@ -101,3 +101,87 @@
|
|
|
101
101
|
padding: 1rem;
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
+
|
|
105
|
+
.enrollment-progress {
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
gap: rem-calc(8px);
|
|
109
|
+
margin-bottom: rem-calc(8px);
|
|
110
|
+
|
|
111
|
+
&__bar {
|
|
112
|
+
flex: 1;
|
|
113
|
+
height: rem-calc(8px);
|
|
114
|
+
background-color: var(--c--globals--colors--gray-100);
|
|
115
|
+
border-radius: rem-calc(4px);
|
|
116
|
+
overflow: hidden;
|
|
117
|
+
|
|
118
|
+
&__fill {
|
|
119
|
+
height: 100%;
|
|
120
|
+
background-color: var(--c--theme--colors--primary-500);
|
|
121
|
+
transition: width 0.3s ease;
|
|
122
|
+
animation: progress-grow 2s ease-out forwards;
|
|
123
|
+
|
|
124
|
+
@keyframes progress-grow {
|
|
125
|
+
from {
|
|
126
|
+
width: 0;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.enrollment-nested-section__content {
|
|
134
|
+
display: flex;
|
|
135
|
+
flex-direction: column;
|
|
136
|
+
gap: rem-calc(4px);
|
|
137
|
+
font-size: rem-calc(13px);
|
|
138
|
+
|
|
139
|
+
.enrollment-search {
|
|
140
|
+
width: 50%;
|
|
141
|
+
margin-bottom: 0.5rem;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.enrollment-list {
|
|
145
|
+
list-style: disc inside;
|
|
146
|
+
padding-left: 0;
|
|
147
|
+
margin: 0;
|
|
148
|
+
|
|
149
|
+
li {
|
|
150
|
+
margin-bottom: rem-calc(4px);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.enrollment-actions {
|
|
156
|
+
display: flex;
|
|
157
|
+
align-items: flex-start;
|
|
158
|
+
justify-content: space-between;
|
|
159
|
+
gap: rem-calc(8px);
|
|
160
|
+
margin-top: rem-calc(8px);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.enrollment-load-more {
|
|
164
|
+
width: fit-content;
|
|
165
|
+
padding: rem-calc(4px) rem-calc(12px);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.enrollment-pagination-wrapper {
|
|
169
|
+
margin-top: rem-calc(8px);
|
|
170
|
+
|
|
171
|
+
.pagination {
|
|
172
|
+
margin: 0;
|
|
173
|
+
padding: 0;
|
|
174
|
+
justify-content: flex-start;
|
|
175
|
+
|
|
176
|
+
&__list {
|
|
177
|
+
margin: 0;
|
|
178
|
+
padding: 0;
|
|
179
|
+
gap: rem-calc(4px);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
&__item {
|
|
183
|
+
transform: scale(0.9);
|
|
184
|
+
transform-origin: center;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|