richie-education 2.25.0-b2.dev83 → 2.25.0-b2.dev92

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 (28) hide show
  1. package/js/components/CourseGlimpseList/index.spec.tsx +1 -1
  2. package/js/components/CourseGlimpseList/index.tsx +1 -1
  3. package/js/components/PaymentButton/index.spec.tsx +2 -62
  4. package/js/components/PaymentButton/index.tsx +4 -7
  5. package/js/pages/TeacherDashboardContractsLayout/components/ContractFiltersBar/index.tsx +10 -49
  6. package/js/types/Joanie.ts +1 -0
  7. package/js/utils/OrderHelper/index.ts +32 -0
  8. package/js/widgets/Dashboard/components/DashboardAvatar/_styles.scss +4 -3
  9. package/js/widgets/Dashboard/components/DashboardAvatar/index.tsx +1 -1
  10. package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.tsx +5 -2
  11. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +2 -2
  12. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +9 -6
  13. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.spec.tsx +2 -5
  14. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +5 -5
  15. package/js/widgets/Dashboard/components/DashboardListAvatar/_styles.scss +8 -0
  16. package/js/widgets/Dashboard/components/DashboardListAvatar/index.tsx +11 -0
  17. package/js/widgets/Dashboard/components/DashboardOrderLoader/index.tsx +5 -2
  18. package/js/widgets/Dashboard/components/DashboardSidebar/_styles.scss +2 -0
  19. package/js/widgets/Dashboard/components/FilterOrganization/index.tsx +58 -0
  20. package/js/widgets/Dashboard/components/FiltersBar/index.tsx +9 -0
  21. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/EnrollableCourseRunList.tsx +4 -2
  22. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +4 -2
  23. package/package.json +1 -1
  24. package/scss/components/_index.scss +1 -0
  25. package/scss/objects/_course_glimpses.scss +0 -7
  26. package/scss/objects/_index.scss +1 -0
  27. package/scss/objects/_list.scss +8 -0
  28. 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('.course-glimpse-list__count')).toHaveAttribute(
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={{
@@ -900,7 +900,7 @@ describe.each([
900
900
  });
901
901
  });
902
902
 
903
- it('should show an error if product has a contract definition and the terms are not accepted', async () => {
903
+ it('should show an error if user does not accept the terms', async () => {
904
904
  const product: Joanie.Product = ProductFactory().one();
905
905
  const billingAddress: Joanie.Address = AddressFactory().one();
906
906
 
@@ -945,7 +945,7 @@ describe.each([
945
945
  expect(screen.getByText('You must accept the terms.')).toBeInTheDocument();
946
946
  });
947
947
 
948
- it('should be able to preview the contract if product has a contract definition', async () => {
948
+ it('should show a link to the platform terms and conditions', async () => {
949
949
  const product: Joanie.Product = ProductFactory().one();
950
950
  const billingAddress: Joanie.Address = AddressFactory().one();
951
951
 
@@ -977,66 +977,6 @@ describe.each([
977
977
  expect($terms).toHaveAttribute('href', '/en/about/terms-and-conditions/');
978
978
  });
979
979
 
980
- it('should not show terms checkbox if the product does not have a contract definition', async () => {
981
- const product: Joanie.Product = ProductFactory().one();
982
- product.contract_definition = undefined;
983
- const billingAddress: Joanie.Address = AddressFactory().one();
984
-
985
- const { payment_info: paymentInfo, ...order } = OrderWithPaymentFactory().one();
986
-
987
- const fetchOrderQueryParams =
988
- product.type === ProductType.CREDENTIAL
989
- ? {
990
- course_code: TEST_COURSE_CODE,
991
- product_id: product.id,
992
- state: ['pending', 'validated', 'submitted'],
993
- }
994
- : {
995
- enrollment_id: TEST_ENROLLMENT_ID,
996
- product_id: product.id,
997
- state: ['pending', 'validated', 'submitted'],
998
- };
999
-
1000
- fetchMock
1001
- .get(
1002
- `https://joanie.test/api/v1.0/orders/?${queryString.stringify(fetchOrderQueryParams)}`,
1003
- [],
1004
- )
1005
- .post('https://joanie.test/api/v1.0/orders/', order)
1006
- .patch(`https://joanie.test/api/v1.0/orders/${order.id}/submit/`, {
1007
- paymentInfo,
1008
- });
1009
-
1010
- render(
1011
- <Wrapper client={createTestQueryClient({ user: true })} product={product}>
1012
- <PaymentButton billingAddress={billingAddress} onSuccess={noop} />
1013
- </Wrapper>,
1014
- );
1015
-
1016
- const $button = screen.getByRole('button', {
1017
- name: `Pay ${formatPrice(product.price, product.price_currency)}`,
1018
- }) as HTMLButtonElement;
1019
-
1020
- // - As all information are provided, payment button should not be disabled.
1021
- expect($button.disabled).toBe(false);
1022
-
1023
- // - The terms checbkox is not rendered.
1024
- expect(
1025
- screen.queryByLabelText('By checking this box, you accept the General Terms of Sale'),
1026
- ).not.toBeInTheDocument();
1027
-
1028
- // - User clicks on pay button
1029
- await act(async () => {
1030
- fireEvent.click($button);
1031
- });
1032
-
1033
- // - No errors.
1034
- expect(screen.queryByText('You must accept the terms.')).not.toBeInTheDocument();
1035
-
1036
- // - Payment interface should be displayed.
1037
- screen.getByText('Payment interface component');
1038
- });
1039
-
1040
980
  if (productType === ProductType.CREDENTIAL) {
1041
981
  it('should create an order with an order group', async () => {
1042
982
  const product: Joanie.Product = ProductFactory().one();
@@ -114,12 +114,7 @@ const PaymentButton = ({ billingAddress, creditCard, onSuccess }: PaymentButtonP
114
114
  });
115
115
 
116
116
  const isReadyToPay = useMemo(() => {
117
- return (
118
- (course || enrollment) &&
119
- product &&
120
- billingAddress &&
121
- (termsAccepted || !product.contract_definition)
122
- );
117
+ return (course || enrollment) && product && billingAddress && termsAccepted;
123
118
  }, [product, course, enrollment, billingAddress, termsAccepted]);
124
119
 
125
120
  /**
@@ -201,10 +196,12 @@ const PaymentButton = ({ billingAddress, creditCard, onSuccess }: PaymentButtonP
201
196
  ? {
202
197
  product_id: product.id,
203
198
  enrollment_id: enrollment!.id,
199
+ has_consent_to_terms: termsAccepted,
204
200
  }
205
201
  : {
206
202
  product_id: product.id,
207
203
  course_code: course!.code,
204
+ has_consent_to_terms: termsAccepted,
208
205
  ...(orderGroup ? { order_group_id: orderGroup.id } : {}),
209
206
  };
210
207
 
@@ -284,7 +281,7 @@ const PaymentButton = ({ billingAddress, creditCard, onSuccess }: PaymentButtonP
284
281
 
285
282
  return (
286
283
  <div className="payment-button" data-testid={order && 'payment-button-order-loaded'}>
287
- {product.contract_definition && renderTermsCheckbox()}
284
+ {renderTermsCheckbox()}
288
285
  <Button
289
286
  disabled={state === ComponentStates.LOADING}
290
287
  onClick={createOrder}
@@ -1,10 +1,9 @@
1
1
  import { Select, SelectProps } from '@openfun/cunningham-react';
2
2
  import { defineMessages, useIntl } from 'react-intl';
3
- import { useEffect } from 'react';
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 { Spinner } from 'components/Spinner';
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
- <div className="dashboard__page__actions-row dashboard__page__actions-row--end">
40
+ <FiltersBar>
47
41
  {!hideFilterOrganization && (
48
- <OrganizationContractFilter
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
- </div>
53
+ </FiltersBar>
60
54
  );
61
55
  };
62
56
 
63
- const OrganizationContractFilter = ({ defaultValue, onChange }: FilterProps) => {
64
- const intl = useIntl();
65
- const {
66
- items: organizations,
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 }: FilterProps) => {
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)
@@ -376,6 +376,7 @@ export interface AddressCreationPayload extends Omit<Address, 'id' | 'is_main'>
376
376
  interface AbstractOrderProductCreationPayload {
377
377
  product_id: Product['id'];
378
378
  order_group_id?: OrderGroup['id'];
379
+ has_consent_to_terms: boolean;
379
380
  }
380
381
 
381
382
  interface OrderCertificateCreationPayload extends AbstractOrderProductCreationPayload {
@@ -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: 100px;
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: 2.5rem;
18
+ font-size: 1em;
18
19
  font-family: $r-font-family-montserrat;
19
20
  font-weight: $font-weight-boldest;
20
21
  line-height: 1;
@@ -8,7 +8,7 @@ export enum DashboardAvatarVariantEnum {
8
8
  SQUARE = 'square',
9
9
  }
10
10
 
11
- interface DashboardAvatarProps {
11
+ export interface DashboardAvatarProps {
12
12
  title: string;
13
13
  image?: Nullable<JoanieFile>;
14
14
  variant?: DashboardAvatarVariantEnum;
@@ -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 ? orderNeedsSignature(order, product) : false;
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],
@@ -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 { orderNeedsSignature } from 'widgets/Dashboard/components/DashboardItem/utils/order';
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.contract_definition!}
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 order={order} product={product} />
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.contract_definition!}
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.contract_definition!}
179
+ contract_definition={product?.contract_definition!}
177
180
  contract={order.contract}
178
181
  writable={writable}
179
182
  mode="compact"
@@ -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 product = ProductFactory({
78
- contract_definition: ContractDefinitionFactory().one(),
79
- }).one();
76
+ const contractDefinition = ContractDefinitionFactory().one();
80
77
 
81
78
  render(
82
79
  <Wrapper>
83
- <OrderStateMessage order={order} product={product} />
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, Product } from 'types/Joanie';
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 { orderNeedsSignature } from 'widgets/Dashboard/components/DashboardItem/utils/order';
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
- product?: Product;
56
+ contractDefinition?: ContractDefinition;
57
57
  }
58
58
 
59
- const OrderStateMessage = ({ order, product }: OrderStateMessageProps) => {
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, product)) {
75
+ if (OrderHelper.orderNeedsSignature(order, contractDefinition)) {
76
76
  return <FormattedMessage {...messages.statusWaitingSignature} />;
77
77
  }
78
78
 
@@ -0,0 +1,8 @@
1
+ .dashboard-list-avatar {
2
+ &__container {
3
+ position: relative;
4
+ display: flex;
5
+ justify-content: center;
6
+ font-size: rem-calc(12px);
7
+ }
8
+ }
@@ -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 { orderNeedsSignature } from 'widgets/Dashboard/components/DashboardItem/utils/order';
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(order, courseProduct?.product);
53
+ const needsSignature = OrderHelper.orderNeedsSignature(
54
+ order,
55
+ courseProduct?.product.contract_definition,
56
+ );
54
57
 
55
58
  return (
56
59
  <>
@@ -30,6 +30,8 @@
30
30
  &__avatar {
31
31
  position: absolute;
32
32
  top: calc($avatar-size / -2);
33
+ // avatar's parent font-size define the avatar size.
34
+ font-size: rem-calc(40px);
33
35
  }
34
36
 
35
37
  h3 {
@@ -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;
@@ -0,0 +1,9 @@
1
+ import { PropsWithChildren } from 'react';
2
+
3
+ const FiltersBar = ({ children }: PropsWithChildren) => {
4
+ return (
5
+ <div className="dashboard__page__actions-row dashboard__page__actions-row--end">{children}</div>
6
+ );
7
+ };
8
+
9
+ export default FiltersBar;
@@ -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 { orderNeedsSignature } from 'widgets/Dashboard/components/DashboardItem/utils/order';
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 ? orderNeedsSignature(order, product) : false;
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 { orderNeedsSignature } from 'widgets/Dashboard/components/DashboardItem/utils/order';
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 ? orderNeedsSignature(order, product) : false;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "2.25.0-b2.dev83",
3
+ "version": "2.25.0-b2.dev92",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {
@@ -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
  }
@@ -2,6 +2,7 @@
2
2
  @import './banner';
3
3
  @import './breadcrumbs';
4
4
  @import './dashboard';
5
+ @import './list';
5
6
  @import './course_glimpses';
6
7
  @import './blogpost_glimpses';
7
8
  @import './organization_glimpses';
@@ -0,0 +1,8 @@
1
+ .list {
2
+ &__count-description {
3
+ flex-basis: 100%; // Should not wrap as mozaic list elements
4
+ color: r-theme-val(course-glimpse-list, count-color);
5
+ text-align: right;
6
+ align-self: self-end;
7
+ }
8
+ }
@@ -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
- };