richie-education 3.3.2-dev6 → 3.4.1-dev13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/.eslintrc.json +0 -3
  2. package/.storybook/main.js +11 -12
  3. package/.storybook/preview.tsx +49 -24
  4. package/cunningham.cjs +31 -0
  5. package/i18n/compile-translations.js +12 -10
  6. package/i18n/locales/ar-SA.json +20 -0
  7. package/i18n/locales/es-ES.json +20 -0
  8. package/i18n/locales/fa-IR.json +20 -0
  9. package/i18n/locales/fr-CA.json +20 -0
  10. package/i18n/locales/fr-FR.json +21 -1
  11. package/i18n/locales/ko-KR.json +20 -0
  12. package/i18n/locales/pt-PT.json +42 -22
  13. package/i18n/locales/ru-RU.json +20 -0
  14. package/i18n/locales/vi-VN.json +20 -0
  15. package/jest.config.js +5 -0
  16. package/js/api/joanie.ts +20 -0
  17. package/js/api/utils.ts +4 -3
  18. package/js/components/AddressesManagement/AddressForm/index.stories.tsx +1 -1
  19. package/js/components/AddressesManagement/AddressForm/index.tsx +4 -3
  20. package/js/components/AddressesManagement/index.stories.tsx +1 -1
  21. package/js/components/AddressesManagement/index.tsx +5 -3
  22. package/js/components/Badge/index.stories.tsx +1 -1
  23. package/js/components/Badge/index.tsx +1 -1
  24. package/js/components/Banner/index.stories.tsx +1 -1
  25. package/js/components/CourseGlimpse/index.stories.tsx +1 -1
  26. package/js/components/CourseGlimpseList/index.stories.tsx +1 -1
  27. package/js/components/CreditCardSelector/_styles.scss +2 -2
  28. package/js/components/CreditCardSelector/index.tsx +11 -3
  29. package/js/components/DownloadAgreementButton/index.tsx +51 -0
  30. package/js/components/DownloadBatchOrderSeatsButton/index.spec.tsx +46 -0
  31. package/js/components/DownloadBatchOrderSeatsButton/index.tsx +80 -0
  32. package/js/components/DownloadCertificateButton/index.tsx +2 -1
  33. package/js/components/DownloadContractButton/index.tsx +7 -1
  34. package/js/components/Form/Form/index.tsx +4 -2
  35. package/js/components/Icon/index.stories.tsx +2 -1
  36. package/js/components/Modal/index.stories.tsx +1 -1
  37. package/js/components/Modal/index.tsx +2 -1
  38. package/js/components/OpenEdxFullNameForm/index.stories.tsx +1 -1
  39. package/js/components/OpenEdxFullNameForm/index.tsx +2 -2
  40. package/js/components/PaymentScheduleGrid/_styles.scss +2 -2
  41. package/js/components/PurchaseButton/index.stories.tsx +1 -1
  42. package/js/components/RegisteredAddress/index.stories.tsx +1 -1
  43. package/js/components/RegisteredAddress/index.tsx +4 -2
  44. package/js/components/SaleTunnel/AddressSelector/CreateAddressFormModal.tsx +1 -1
  45. package/js/components/SaleTunnel/AddressSelector/EditAddressFormModal.tsx +1 -1
  46. package/js/components/SaleTunnel/AddressSelector/index.tsx +4 -2
  47. package/js/components/SaleTunnel/SaleTunnelInformation/SaleTunnelInformationGroup.tsx +5 -4
  48. package/js/components/SaleTunnel/SaleTunnelInformation/SaleTunnelInformationSingular.tsx +27 -5
  49. package/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx +1 -1
  50. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +9 -6
  51. package/js/components/SaleTunnel/_styles.scss +9 -8
  52. package/js/components/SaleTunnel/index.credential.spec.tsx +50 -1
  53. package/js/components/SaleTunnel/index.full-process-b2b.spec.tsx +5 -5
  54. package/js/components/SaleTunnel/index.full-process-b2c.spec.tsx +1 -1
  55. package/js/components/SaleTunnel/index.stories.tsx +1 -1
  56. package/js/components/Spinner/index.stories.tsx +1 -1
  57. package/js/components/Tabs/index.stories.tsx +1 -1
  58. package/js/components/Tabs/index.tsx +2 -1
  59. package/js/components/TeacherDashboardCourseList/index.tsx +2 -1
  60. package/js/hooks/useAddressesManagement.tsx +4 -2
  61. package/js/hooks/useBatchOrder/index.tsx +21 -1
  62. package/js/hooks/useCreditCards/index.ts +6 -4
  63. package/js/hooks/useDashboardAddressForm.tsx +3 -3
  64. package/js/hooks/useDownloadAgreement/index.spec.tsx +136 -0
  65. package/js/hooks/useDownloadAgreement/index.tsx +25 -0
  66. package/js/hooks/useDownloadBatchOrderSeats/index.spec.tsx +132 -0
  67. package/js/hooks/useDownloadBatchOrderSeats/index.tsx +24 -0
  68. package/js/hooks/useMatchMedia.ts +1 -1
  69. package/js/hooks/useResources/useResourcesRoot.ts +1 -1
  70. package/js/hooks/useUnionResource/index.ts +6 -2
  71. package/js/hooks/useUnionResource/utils/fetchEntity.ts +1 -0
  72. package/js/pages/DashboardAddressesManagement/DashboardAddressBox.tsx +3 -3
  73. package/js/pages/DashboardAddressesManagement/DashboardCreateAddress.stories.tsx +1 -1
  74. package/js/pages/DashboardAddressesManagement/DashboardCreateAddress.tsx +1 -1
  75. package/js/pages/DashboardAddressesManagement/DashboardEditAddress.stories.tsx +1 -1
  76. package/js/pages/DashboardAddressesManagement/DashboardEditAddress.tsx +3 -2
  77. package/js/pages/DashboardAddressesManagement/index.stories.tsx +1 -1
  78. package/js/pages/DashboardAddressesManagement/index.tsx +1 -1
  79. package/js/pages/DashboardBatchOrderLayout/index.spec.tsx +19 -2
  80. package/js/pages/DashboardCourses/index.tsx +2 -1
  81. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.stories.tsx +1 -1
  82. package/js/pages/DashboardCreditCardsManagement/DashboardCreditCardBox.tsx +3 -3
  83. package/js/pages/DashboardCreditCardsManagement/DashboardEditCreditCard.stories.tsx +1 -1
  84. package/js/pages/DashboardCreditCardsManagement/DashboardEditCreditCard.tsx +3 -2
  85. package/js/pages/DashboardCreditCardsManagement/index.stories.tsx +1 -1
  86. package/js/pages/DashboardOpenEdxProfile/index.stories.tsx +1 -1
  87. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +4 -2
  88. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +2 -1
  89. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +4 -4
  90. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +8 -9
  91. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +14 -3
  92. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx +6 -1
  93. package/js/pages/TeacherDashboardCourseLoader/CourseRunList/utils.tsx +2 -1
  94. package/js/pages/TeacherDashboardOrganizationAgreements/BulkAgreementContractButton.tsx +4 -2
  95. package/js/pages/TeacherDashboardOrganizationAgreements/SignOrganizationAgreementButton.tsx +2 -1
  96. package/js/pages/TeacherDashboardOrganizationQuotes/BatchOrderSeatInfoQuote.tsx +112 -0
  97. package/js/pages/TeacherDashboardOrganizationQuotes/_styles.scss +17 -0
  98. package/js/pages/TeacherDashboardOrganizationQuotes/index.full-process.spec.tsx +7 -4
  99. package/js/pages/TeacherDashboardOrganizationQuotes/index.spec.tsx +8 -4
  100. package/js/pages/TeacherDashboardOrganizationQuotes/index.tsx +39 -26
  101. package/js/translations/ar-SA.json +1 -1
  102. package/js/translations/es-ES.json +1 -1
  103. package/js/translations/fa-IR.json +1 -1
  104. package/js/translations/fr-CA.json +1 -1
  105. package/js/translations/fr-FR.json +1 -1
  106. package/js/translations/ko-KR.json +1 -1
  107. package/js/translations/pt-PT.json +1 -1
  108. package/js/translations/ru-RU.json +1 -1
  109. package/js/translations/vi-VN.json +1 -1
  110. package/js/types/Joanie.ts +22 -2
  111. package/js/utils/ProductHelper/index.spec.ts +1 -1
  112. package/js/utils/StorybookHelper/index.tsx +3 -6
  113. package/js/utils/cunningham-tokens.ts +1111 -142
  114. package/js/utils/download.ts +3 -1
  115. package/js/utils/errors/handle.spec.ts +3 -3
  116. package/js/utils/react-query/useSessionMutation/index.ts +8 -3
  117. package/js/utils/test/factories/joanie.ts +16 -2
  118. package/js/widgets/Dashboard/components/DashboardAvatar/index.stories.tsx +1 -1
  119. package/js/widgets/Dashboard/components/DashboardBox/index.stories.tsx +13 -5
  120. package/js/widgets/Dashboard/components/DashboardBreadcrumbs/index.stories.tsx +1 -1
  121. package/js/widgets/Dashboard/components/DashboardBreadcrumbs/index.tsx +3 -2
  122. package/js/widgets/Dashboard/components/DashboardCard/index.spec.tsx +13 -2
  123. package/js/widgets/Dashboard/components/DashboardCard/index.stories.tsx +12 -4
  124. package/js/widgets/Dashboard/components/DashboardCard/index.tsx +1 -1
  125. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderAgreementInfo.tsx +72 -0
  126. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderPaymentModal/BatchOrderPaymentManager.tsx +1 -1
  127. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderPaymentModal/index.tsx +2 -1
  128. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderSeatInfo.spec.tsx +114 -0
  129. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderSeatInfo.tsx +133 -0
  130. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/DashboardBatchOrderSubItems.tsx +17 -1
  131. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/batchOrderSeatInfoMessages.ts +24 -0
  132. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/index.tsx +16 -3
  133. package/js/widgets/Dashboard/components/DashboardItem/Certificate/index.stories.tsx +1 -1
  134. package/js/widgets/Dashboard/components/DashboardItem/Contract/index.stories.tsx +1 -1
  135. package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.stories.tsx +1 -1
  136. package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.tsx +8 -4
  137. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.stories.tsx +1 -1
  138. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +1 -1
  139. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderReadonly.stories.tsx +1 -1
  140. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderWritable.stories.tsx +1 -1
  141. package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +1 -1
  142. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/_styles.scss +2 -2
  143. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +2 -1
  144. package/js/widgets/Dashboard/components/DashboardItem/Order/OrganizationBlock/index.tsx +6 -3
  145. package/js/widgets/Dashboard/components/DashboardItem/_styles.scss +6 -2
  146. package/js/widgets/Dashboard/components/DashboardItem/index.stories.tsx +1 -1
  147. package/js/widgets/Dashboard/components/DashboardItem/index.tsx +2 -1
  148. package/js/widgets/Dashboard/components/DashboardListAvatar/index.stories.tsx +1 -1
  149. package/js/widgets/Dashboard/components/DashboardSidebar/index.stories.tsx +1 -1
  150. package/js/widgets/Dashboard/components/LearnerDashboardSidebar/index.stories.tsx +1 -1
  151. package/js/widgets/Dashboard/components/ProtectedOutlet/AuthenticatedOutlet.spec.tsx +1 -1
  152. package/js/widgets/Dashboard/components/ProtectedOutlet/ProtectedOutlet.spec.tsx +1 -1
  153. package/js/widgets/Dashboard/components/SearchBar/index.tsx +2 -1
  154. package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.stories.tsx +1 -1
  155. package/js/widgets/Dashboard/components/TeacherDashboardProfileSidebar/index.stories.tsx +1 -1
  156. package/js/widgets/Dashboard/hooks/useRouteInfo/index.spec.tsx +2 -2
  157. package/js/widgets/Dashboard/index.spec.tsx +1 -1
  158. package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +2 -2
  159. package/js/widgets/Search/components/SearchFilterValueParent/index.stories.tsx +1 -1
  160. package/js/widgets/Search/components/SearchFiltersPane/index.tsx +2 -1
  161. package/js/widgets/Slider/index.stories.tsx +1 -1
  162. package/js/widgets/Slider/index.tsx +7 -6
  163. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCertificateItem/index.stories.tsx +1 -1
  164. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.stories.tsx +1 -1
  165. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +1 -1
  166. package/js/widgets/SyllabusCourseRunsList/components/CourseWishButton/index.tsx +4 -2
  167. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.stories.tsx +1 -1
  168. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRunCompacted/index.stories.tsx +41 -0
  169. package/js/widgets/UserLogin/index.stories.tsx +1 -1
  170. package/package.json +76 -81
  171. package/scss/components/_subheader.scss +1 -1
  172. package/scss/components/templates/richie/slider/_slider.scss +1 -1
  173. package/scss/objects/_course_glimpses.scss +1 -0
  174. package/scss/objects/_dashboard.scss +77 -0
  175. package/scss/trumps/_bootstrap.scss +1 -0
  176. package/scss/vendors/css/cunningham-tokens.css +1259 -154
  177. package/scss/vendors/cunningham-tokens.scss +1479 -150
  178. package/tsconfig.json +1 -1
  179. package/webpack.config.js +8 -0
@@ -5,11 +5,13 @@ import { handle } from './errors/handle';
5
5
  *
6
6
  * @param downloadFunction, an api promise that return a File
7
7
  * @param newWindow, does it open in a new window or not
8
+ * @param filename, optional filename override; if provided, takes precedence over file.name
8
9
  * @returns boolean, true for success
9
10
  */
10
11
  export const browserDownloadFromBlob = async (
11
12
  downloadFunction: () => Promise<File>,
12
13
  newWindow: boolean = false,
14
+ filename?: string,
13
15
  ) => {
14
16
  try {
15
17
  const file = await downloadFunction();
@@ -24,7 +26,7 @@ export const browserDownloadFromBlob = async (
24
26
 
25
27
  const $link = document.createElement('a');
26
28
  $link.href = url;
27
- $link.download = file.name;
29
+ $link.download = filename || file.name;
28
30
 
29
31
  const revokeObject = () => {
30
32
  // eslint-disable-next-line compat/compat
@@ -19,12 +19,12 @@ jest.mock('@sentry/browser', () => ({
19
19
 
20
20
  describe('handle', () => {
21
21
  it('should initialize sentry', () => {
22
- expect(Sentry.init).toBeCalledTimes(1);
23
- expect(Sentry.setTag).toBeCalledTimes(1);
22
+ expect(Sentry.init).toHaveBeenCalledTimes(1);
23
+ expect(Sentry.setTag).toHaveBeenCalledTimes(1);
24
24
  });
25
25
 
26
26
  it('should report error to sentry', () => {
27
27
  handle(new Error('An error for test'));
28
- expect(Sentry.captureException).toBeCalledTimes(1);
28
+ expect(Sentry.captureException).toHaveBeenCalledTimes(1);
29
29
  });
30
30
  });
@@ -1,4 +1,8 @@
1
- import type { UseMutationOptions, UseMutationResult } from '@tanstack/react-query';
1
+ import type {
2
+ MutationFunctionContext,
3
+ UseMutationOptions,
4
+ UseMutationResult,
5
+ } from '@tanstack/react-query';
2
6
  import { useMutation, useQueryClient } from '@tanstack/react-query';
3
7
  import { HttpError, HttpStatusCode } from 'utils/errors/HttpError';
4
8
 
@@ -18,13 +22,14 @@ export function useSessionMutation<TData = unknown, TVariables = void, TContext
18
22
  error: HttpError,
19
23
  variables: TVariables,
20
24
  context: TContext | undefined,
25
+ mutationContext: MutationFunctionContext,
21
26
  ) => {
22
- if (error.code === HttpStatusCode.UNAUTHORIZED) {
27
+ if (error && error.code === HttpStatusCode.UNAUTHORIZED) {
23
28
  await queryClient.invalidateQueries({ queryKey: ['user'], exact: true });
24
29
  }
25
30
 
26
31
  if (options?.onError) {
27
- return options.onError(error, variables, context);
32
+ return options.onError(error, variables, context, mutationContext);
28
33
  }
29
34
  };
30
35
 
@@ -46,6 +46,7 @@ import {
46
46
  BatchOrderQuote,
47
47
  Relation,
48
48
  Agreement,
49
+ BatchOrderSeat,
49
50
  } from 'types/Joanie';
50
51
  import { Payment, PaymentMethod, PaymentProviders } from 'components/PaymentInterfaces/types';
51
52
  import { CourseStateFactory } from 'utils/test/factories/richie';
@@ -176,7 +177,7 @@ export const OrganizationFactory = factory((): Organization => {
176
177
  contact_phone: faker.phone.number(),
177
178
  address: AddressFactory().one(),
178
179
  abilities: {
179
- can_submit_for_signature_batch_order: faker.datatype.boolean(),
180
+ can_manage_batch_order_agreement: faker.datatype.boolean(),
180
181
  confirm_bank_transfer: faker.datatype.boolean(),
181
182
  confirm_quote: faker.datatype.boolean(),
182
183
  delete: faker.datatype.boolean(),
@@ -214,12 +215,15 @@ export const BatchOrderQuoteFactory = factory((): BatchOrderQuote => {
214
215
  relation: RelationFactory().one(),
215
216
  payment_method: faker.helpers.arrayElement(Object.values(PaymentMethod)),
216
217
  contract_submitted: faker.datatype.boolean(),
217
- nb_seats: faker.number.int({ min: 1, max: 100 }),
218
+ nb_seats: faker.number.int({ min: 10, max: 100 }),
219
+ seats_owned: faker.number.int({ min: 0, max: 10 }),
220
+ seats_to_own: faker.number.int({ min: 90, max: 100 }),
218
221
  available_actions: {
219
222
  confirm_quote: false,
220
223
  confirm_purchase_order: false,
221
224
  confirm_bank_transfer: false,
222
225
  submit_for_signature: false,
226
+ download_quote: false,
223
227
  next_action: null,
224
228
  },
225
229
  };
@@ -551,6 +555,8 @@ export const BatchOrderReadFactory = factory((): BatchOrderRead => {
551
555
  funding_entity: faker.company.name(),
552
556
  funding_amount: faker.number.int({ min: 100, max: 10000 }),
553
557
  offering: OfferingBatchOrderFactory().one(),
558
+ seats_owned: faker.number.int({ min: 1, max: 200 }),
559
+ seats_to_own: faker.number.int({ min: 1, max: 200 }),
554
560
  };
555
561
  });
556
562
 
@@ -678,3 +684,11 @@ export const SaleTunnelContextFactory = factory(
678
684
  setPaymentMode: noop,
679
685
  }),
680
686
  );
687
+
688
+ export const BatchOrderSeatFactory = factory((): BatchOrderSeat => {
689
+ return {
690
+ id: faker.string.uuid(),
691
+ owner_name: null,
692
+ voucher: faker.string.alphanumeric(10),
693
+ };
694
+ });
@@ -1,4 +1,4 @@
1
- import { Meta, StoryObj } from '@storybook/react';
1
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
2
2
  import { UserFactory } from 'utils/test/factories/richie';
3
3
  import { OrganizationFactory } from 'utils/test/factories/joanie';
4
4
  import { DashboardAvatar, DashboardAvatarVariantEnum } from '.';
@@ -1,4 +1,4 @@
1
- import { Meta, StoryObj } from '@storybook/react';
1
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
2
2
  import { Button } from '@openfun/cunningham-react';
3
3
  import { DashboardBox } from './index';
4
4
 
@@ -25,8 +25,12 @@ export const Default: Story = {
25
25
  ),
26
26
  footer: (
27
27
  <>
28
- <Button color="primary">Delete</Button>
29
- <Button color="primary">Update</Button>
28
+ <Button color="brand" variant="primary">
29
+ Delete
30
+ </Button>
31
+ <Button color="brand" variant="primary">
32
+ Update
33
+ </Button>
30
34
  </>
31
35
  ),
32
36
  },
@@ -43,8 +47,12 @@ export const NoHeader: Story = {
43
47
  ),
44
48
  footer: (
45
49
  <>
46
- <Button color="primary">Delete</Button>
47
- <Button color="primary">Update</Button>
50
+ <Button color="brand" variant="primary">
51
+ Delete
52
+ </Button>
53
+ <Button color="brand" variant="primary">
54
+ Update
55
+ </Button>
48
56
  </>
49
57
  ),
50
58
  },
@@ -1,4 +1,4 @@
1
- import { Meta, StoryObj } from '@storybook/react';
1
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
2
2
  import { createMemoryRouter, Outlet, RouteObject, RouterProvider } from 'react-router';
3
3
  import { defineMessages } from 'react-intl';
4
4
  import { DashboardBreadcrumbsProvider } from 'widgets/Dashboard/contexts/DashboardBreadcrumbsContext';
@@ -60,7 +60,8 @@ export const DashboardBreadcrumbs = () => {
60
60
  <RouterButton
61
61
  href={backPath}
62
62
  size="nano"
63
- color="tertiary-text"
63
+ color="brand"
64
+ variant="tertiary"
64
65
  icon={<span className="material-icons">chevron_left</span>}
65
66
  >
66
67
  <FormattedMessage {...messages.back} />
@@ -69,7 +70,7 @@ export const DashboardBreadcrumbs = () => {
69
70
 
70
71
  {breadcrumbs.map((breadcrumb) => (
71
72
  <li key={breadcrumb.pathname}>
72
- <RouterButton href={breadcrumb.pathname} size="nano" color="tertiary-text">
73
+ <RouterButton href={breadcrumb.pathname} size="nano" color="brand" variant="tertiary">
73
74
  {breadcrumb.name}
74
75
  </RouterButton>
75
76
  </li>
@@ -5,7 +5,14 @@ import { DashboardCard } from '.';
5
5
  describe('<DashboardCard/>', () => {
6
6
  it('opens and closes', async () => {
7
7
  render(
8
- <DashboardCard header="My header" footer={<Button color="primary">Update</Button>}>
8
+ <DashboardCard
9
+ header="My header"
10
+ footer={
11
+ <Button color="brand" variant="primary">
12
+ Update
13
+ </Button>
14
+ }
15
+ >
9
16
  Content here
10
17
  </DashboardCard>,
11
18
  );
@@ -23,7 +30,11 @@ describe('<DashboardCard/>', () => {
23
30
  <DashboardCard
24
31
  defaultExpanded={false}
25
32
  header="My header"
26
- footer={<Button color="primary">Update</Button>}
33
+ footer={
34
+ <Button color="brand" variant="primary">
35
+ Update
36
+ </Button>
37
+ }
27
38
  >
28
39
  Content here
29
40
  </DashboardCard>,
@@ -1,4 +1,4 @@
1
- import { Meta, StoryObj } from '@storybook/react';
1
+ import { Meta, StoryObj } from '@storybook/react-webpack5';
2
2
  import { Button } from '@openfun/cunningham-react';
3
3
  import { DashboardBox } from '../DashboardBox';
4
4
  import { DashboardCard } from './index';
@@ -26,7 +26,11 @@ export const Default: Story = {
26
26
  </div>
27
27
  </div>
28
28
  ),
29
- footer: <Button color="primary">Update</Button>,
29
+ footer: (
30
+ <Button color="brand" variant="primary">
31
+ Update
32
+ </Button>
33
+ ),
30
34
  },
31
35
  };
32
36
 
@@ -39,8 +43,12 @@ export const WithBoxes: Story = {
39
43
  header={<>Address used by default</>}
40
44
  footer={
41
45
  <>
42
- <Button color="primary">Remove</Button>
43
- <Button color="primary">Edit</Button>
46
+ <Button color="brand" variant="primary">
47
+ Remove
48
+ </Button>
49
+ <Button color="brand" variant="primary">
50
+ Edit
51
+ </Button>
44
52
  </>
45
53
  }
46
54
  >
@@ -64,7 +64,7 @@ export const DashboardCard = ({
64
64
  <header className="dashboard-card__header">
65
65
  <div>{header}</div>
66
66
  {expandable && (
67
- <Button onClick={toggle} color="tertiary" size="small">
67
+ <Button onClick={toggle} color="brand" variant="tertiary" size="small">
68
68
  <Icon
69
69
  name={IconTypeEnum.CHEVRON_DOWN_OUTLINE}
70
70
  data-testid="dashboard-card__header__toggle"
@@ -0,0 +1,72 @@
1
+ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
2
+ import { BatchOrderRead } from 'types/Joanie';
3
+ import DownloadAgreementButton from 'components/DownloadAgreementButton';
4
+ import { DashboardSubItem } from 'widgets/Dashboard/components/DashboardItem/DashboardSubItem';
5
+ import { useOrganizationAgreement } from 'hooks/useOrganizationAgreements.tsx';
6
+ import useDateFormat from 'hooks/useDateFormat';
7
+
8
+ const messages = defineMessages({
9
+ title: {
10
+ id: 'batchOrder.agreement.title',
11
+ description: 'Step label for the agreement document in the batch order detail',
12
+ defaultMessage: 'Agreement',
13
+ },
14
+ organizationSignedOn: {
15
+ id: 'batchOrder.agreement.organizationSignedOn',
16
+ description: 'Label displayed once the organization has counter-signed the agreement',
17
+ defaultMessage: 'Signed by the organization on {date}.',
18
+ },
19
+ waitingOrganization: {
20
+ id: 'batchOrder.agreement.waitingOrganization',
21
+ description:
22
+ 'Label displayed when the agreement is waiting for the organization counter-signature',
23
+ defaultMessage: 'Waiting for the organization to counter-sign the agreement.',
24
+ },
25
+ });
26
+
27
+ interface BatchOrderAgreementInfoProps {
28
+ batchOrder: BatchOrderRead;
29
+ }
30
+
31
+ export const BatchOrderAgreementInfo = ({ batchOrder }: BatchOrderAgreementInfoProps) => {
32
+ const intl = useIntl();
33
+ const formatDate = useDateFormat();
34
+ const {
35
+ item: agreement,
36
+ states: { isFetched, error },
37
+ } = useOrganizationAgreement(batchOrder.contract_id!, {
38
+ organization_id: batchOrder.organization.id,
39
+ });
40
+
41
+ if (!isFetched || error || !agreement) {
42
+ return null;
43
+ }
44
+
45
+ const signedOn = agreement.organization_signed_on;
46
+
47
+ return (
48
+ <DashboardSubItem
49
+ title={intl.formatMessage(messages.title)}
50
+ footer={
51
+ <div className="content">
52
+ {signedOn ? (
53
+ <>
54
+ <p>
55
+ <FormattedMessage
56
+ {...messages.organizationSignedOn}
57
+ values={{ date: formatDate(signedOn) }}
58
+ />
59
+ </p>
60
+ <DownloadAgreementButton
61
+ organizationId={batchOrder.organization.id}
62
+ agreementId={batchOrder.contract_id!}
63
+ />
64
+ </>
65
+ ) : (
66
+ <FormattedMessage {...messages.waitingOrganization} />
67
+ )}
68
+ </div>
69
+ }
70
+ />
71
+ );
72
+ };
@@ -48,7 +48,7 @@ export const BatchOrderPaymentManager = ({ batchOrder }: BatchPaymentManagerProp
48
48
  footer={
49
49
  <div className="content">
50
50
  <FormattedMessage {...messages.batchOrderPayment} />
51
- <Button size="small" color="primary" onClick={retryModal.open}>
51
+ <Button size="small" color="brand" variant="primary" onClick={retryModal.open}>
52
52
  <FormattedMessage
53
53
  {...messages.paymentNeededButton}
54
54
  values={{
@@ -172,7 +172,8 @@ export const BatchOrderPaymentModal = ({ batchOrder, ...props }: Props) => {
172
172
  hideCloseButton={state === ComponentStates.LOADING}
173
173
  actions={
174
174
  <Button
175
- color="primary"
175
+ color="brand"
176
+ variant="primary"
176
177
  size="small"
177
178
  fullWidth={true}
178
179
  onClick={pay}
@@ -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,133 @@
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
+ {remainingCount > 0 && (
109
+ <Button
110
+ className="enrollment-load-more"
111
+ color="brand"
112
+ variant="secondary"
113
+ size="small"
114
+ onClick={() => setPage((p) => p + 1)}
115
+ disabled={states.fetching}
116
+ >
117
+ {intl.formatMessage(batchOrderSeatInfoMessages.loadMore, {
118
+ count: remainingCount,
119
+ })}
120
+ </Button>
121
+ )}
122
+ </>
123
+ )}
124
+ </div>
125
+ <DownloadBatchOrderSeatsButton
126
+ batchOrderId={batchOrder.id}
127
+ productTitle={batchOrder.offering?.product.title ?? ''}
128
+ />
129
+ </div>
130
+ }
131
+ />
132
+ );
133
+ };
@@ -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: {
@@ -144,6 +146,12 @@ const DashboardItemField = ({
144
146
  export const DashboardBatchOrderSubItems = ({ batchOrder }: { batchOrder: BatchOrderRead }) => {
145
147
  const intl = useIntl();
146
148
 
149
+ const displaySeatsInfo =
150
+ batchOrder.state === BatchOrderState.COMPLETED &&
151
+ !!batchOrder.nb_seats &&
152
+ batchOrder.seats_owned !== undefined &&
153
+ batchOrder.seats_to_own !== undefined;
154
+
147
155
  const items = [
148
156
  <DashboardSubItem
149
157
  key="company"
@@ -312,5 +320,13 @@ export const DashboardBatchOrderSubItems = ({ batchOrder }: { batchOrder: BatchO
312
320
  );
313
321
  }
314
322
 
323
+ if (batchOrder.contract_id) {
324
+ items.push(<BatchOrderAgreementInfo key="agreement" batchOrder={batchOrder} />);
325
+ }
326
+
327
+ if (displaySeatsInfo) {
328
+ items.push(<BatchOrderSeatInfo key="enrollment-management" batchOrder={batchOrder} />);
329
+ }
330
+
315
331
  return <DashboardSubItemsList subItems={items} />;
316
332
  };
@@ -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
+ });