richie-education 2.25.0-b2.dev49 → 2.25.0-b2.dev58

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/.nvmrc CHANGED
@@ -1 +1 @@
1
- 18.19.0
1
+ 20.11.0
@@ -12,7 +12,7 @@ import { createTestQueryClient } from 'utils/test/createTestQueryClient';
12
12
  import { ContractFactory, OrganizationFactory } from 'utils/test/factories/joanie';
13
13
  import { expectNoSpinner } from 'utils/test/expectSpinner';
14
14
  import { expectBannerError } from 'utils/test/expectBanner';
15
- import { HttpError } from 'utils/errors/HttpError';
15
+ import { HttpStatusCode } from 'utils/errors/HttpError';
16
16
  import TeacherDashboardContracts from '.';
17
17
 
18
18
  jest.mock('utils/context', () => ({
@@ -82,12 +82,12 @@ describe('pages/TeacherDashboardContracts', () => {
82
82
  fetchMock.get(`https://joanie.test/api/v1.0/organizations/`, [organization]);
83
83
  // TeacherDashboardContracts request a paginated list of contracts to display
84
84
  fetchMock.get(
85
- `https://joanie.test/api/v1.0/organizations/${organization.id}/contracts/?signature_state=signed&course_product_relation_id=2&page=1&page_size=25`,
85
+ `https://joanie.test/api/v1.0/organizations/${organization.id}/contracts/?course_product_relation_id=2&signature_state=signed&page=1&page_size=25`,
86
86
  { results: contracts, count: 0, previous: null, next: null },
87
87
  );
88
88
  // useTeacherContractsToSign request all contract to sign, without pagination
89
89
  fetchMock.get(
90
- `https://joanie.test/api/v1.0/organizations/${organization.id}/contracts/?signature_state=half_signed&course_product_relation_id=2`,
90
+ `https://joanie.test/api/v1.0/organizations/${organization.id}/contracts/?course_product_relation_id=2&signature_state=half_signed`,
91
91
  { results: [], count: 0, previous: null, next: null },
92
92
  );
93
93
 
@@ -244,6 +244,10 @@ describe('pages/TeacherDashboardContracts', () => {
244
244
  abilities: { sign: true },
245
245
  }).many(3);
246
246
 
247
+ fetchMock.get(
248
+ `https://joanie.test/api/v1.0/organizations/1/contracts/?signature_state=signed&page=1&page_size=25`,
249
+ { results: [], count: 0, previous: null, next: null },
250
+ );
247
251
  fetchMock.get(
248
252
  `https://joanie.test/api/v1.0/organizations/1/contracts/?signature_state=half_signed&page=1&page_size=25`,
249
253
  { results: contracts, count: 3, previous: null, next: null },
@@ -309,15 +313,11 @@ describe('pages/TeacherDashboardContracts', () => {
309
313
  it('should render an error banner if an error occured during contracts fetching', async () => {
310
314
  fetchMock.get(
311
315
  `https://joanie.test/api/v1.0/organizations/1/contracts/?signature_state=signed&page=1&page_size=25`,
312
- () => {
313
- throw new HttpError(404, 'Not found');
314
- },
316
+ HttpStatusCode.NOT_FOUND,
315
317
  );
316
318
  fetchMock.get(
317
319
  `https://joanie.test/api/v1.0/organizations/1/contracts/?signature_state=half_signed`,
318
- () => {
319
- throw new HttpError(404, 'Not found');
320
- },
320
+ HttpStatusCode.NOT_FOUND,
321
321
  );
322
322
 
323
323
  render(
@@ -87,10 +87,7 @@ describe('TeacherDashboardContractsLayout/ContractActionsBar', () => {
87
87
 
88
88
  render(
89
89
  <Wrapper>
90
- <ContractActionsBar
91
- courseProductRelationId={faker.string.uuid()}
92
- organizationId={faker.string.uuid()}
93
- />
90
+ <ContractActionsBar organizationId={faker.string.uuid()} />
94
91
  </Wrapper>,
95
92
  );
96
93
 
@@ -99,26 +96,47 @@ describe('TeacherDashboardContractsLayout/ContractActionsBar', () => {
99
96
  expect(screen.getByRole('button', { name: /Request contracts archive/ })).toBeInTheDocument();
100
97
  });
101
98
 
102
- it("shouldn't only display sign button", () => {
103
- mockHasContractToDownload = false;
104
- mockCanSignContracts = true;
105
- mockContractsToSignCount = 1;
106
-
107
- render(
108
- <Wrapper>
109
- <ContractActionsBar
110
- courseProductRelationId={faker.string.uuid()}
111
- organizationId={faker.string.uuid()}
112
- />
113
- </Wrapper>,
114
- );
115
-
116
- expect(screen.getByTestId('teacher-contracts-list-actionsBar')).toBeInTheDocument();
117
- expect(screen.getByRole('button', { name: /Sign all pending contracts/ })).toBeInTheDocument();
118
- expect(
119
- screen.queryByRole('button', { name: /Request contracts archive/ }),
120
- ).not.toBeInTheDocument();
121
- });
99
+ it.each([
100
+ {
101
+ label: "doesn't have contract to download",
102
+ hasContractToDownload: false,
103
+ courseProductRelationId: undefined,
104
+ },
105
+ {
106
+ label: 'has contract to download and courseProductRelationId',
107
+ hasContractToDownload: true,
108
+ courseProductRelationId: faker.string.uuid(),
109
+ },
110
+ {
111
+ label: "doesn't have contract to download and courseProductRelationId",
112
+ hasContractToDownload: false,
113
+ courseProductRelationId: faker.string.uuid(),
114
+ },
115
+ ])(
116
+ "shouldn't only display sign button when $label",
117
+ ({ hasContractToDownload, courseProductRelationId }) => {
118
+ mockHasContractToDownload = hasContractToDownload;
119
+ mockCanSignContracts = true;
120
+ mockContractsToSignCount = 1;
121
+
122
+ render(
123
+ <Wrapper>
124
+ <ContractActionsBar
125
+ courseProductRelationId={courseProductRelationId}
126
+ organizationId={faker.string.uuid()}
127
+ />
128
+ </Wrapper>,
129
+ );
130
+
131
+ expect(screen.getByTestId('teacher-contracts-list-actionsBar')).toBeInTheDocument();
132
+ expect(
133
+ screen.getByRole('button', { name: /Sign all pending contracts/ }),
134
+ ).toBeInTheDocument();
135
+ expect(
136
+ screen.queryByRole('button', { name: /Request contracts archive/ }),
137
+ ).not.toBeInTheDocument();
138
+ },
139
+ );
122
140
 
123
141
  it("shouldn't only display download button", () => {
124
142
  mockHasContractToDownload = true;
@@ -127,10 +145,7 @@ describe('TeacherDashboardContractsLayout/ContractActionsBar', () => {
127
145
 
128
146
  render(
129
147
  <Wrapper>
130
- <ContractActionsBar
131
- courseProductRelationId={faker.string.uuid()}
132
- organizationId={faker.string.uuid()}
133
- />
148
+ <ContractActionsBar organizationId={faker.string.uuid()} />
134
149
  </Wrapper>,
135
150
  );
136
151
 
@@ -141,15 +156,25 @@ describe('TeacherDashboardContractsLayout/ContractActionsBar', () => {
141
156
  ).not.toBeInTheDocument();
142
157
  });
143
158
 
144
- it('should return nothing when no actions are available', () => {
145
- mockHasContractToDownload = false;
159
+ it.each([
160
+ {
161
+ label: 'only download is available but we have a courseProductRelationId',
162
+ hasContractToDownload: true,
163
+ courseProductRelationId: faker.string.uuid(),
164
+ },
165
+ {
166
+ label: 'no actions are available',
167
+ hasContractToDownload: false,
168
+ },
169
+ ])('should return nothing when $label', ({ hasContractToDownload, courseProductRelationId }) => {
170
+ mockHasContractToDownload = hasContractToDownload;
146
171
  mockCanSignContracts = false;
147
172
  mockContractsToSignCount = 0;
148
173
 
149
174
  render(
150
175
  <Wrapper>
151
176
  <ContractActionsBar
152
- courseProductRelationId={faker.string.uuid()}
177
+ courseProductRelationId={courseProductRelationId}
153
178
  organizationId={faker.string.uuid()}
154
179
  />
155
180
  </Wrapper>,
@@ -15,9 +15,10 @@ const ContractActionsBar = ({ organizationId, courseProductRelationId }: Contrac
15
15
  organizationId,
16
16
  courseProductRelationId,
17
17
  });
18
- const hasContractToDownload = useHasContractToDownload(organizationId);
18
+ const hasContractToDownload = useHasContractToDownload(organizationId, courseProductRelationId);
19
19
 
20
- const nbAvailableActions = [canSignContracts, hasContractToDownload].filter((val) => val).length;
20
+ const canDownloadContracts = hasContractToDownload && !courseProductRelationId;
21
+ const nbAvailableActions = [canSignContracts, canDownloadContracts].filter((val) => val).length;
21
22
  return (
22
23
  nbAvailableActions > 0 && (
23
24
  <div
@@ -35,7 +36,7 @@ const ContractActionsBar = ({ organizationId, courseProductRelationId }: Contrac
35
36
  />
36
37
  </div>
37
38
  )}
38
- {hasContractToDownload && <BulkDownloadContractButton organizationId={organizationId} />}
39
+ {canDownloadContracts && <BulkDownloadContractButton organizationId={organizationId} />}
39
40
  </div>
40
41
  )
41
42
  );
@@ -0,0 +1,134 @@
1
+ import { QueryClientProvider } from '@tanstack/react-query';
2
+ import { PropsWithChildren } from 'react';
3
+ import { IntlProvider } from 'react-intl';
4
+ import { MemoryRouter, Route, Routes } from 'react-router-dom';
5
+ import { renderHook, waitFor } from '@testing-library/react';
6
+ import fetchMock from 'fetch-mock';
7
+ import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
8
+ import JoanieSessionProvider from 'contexts/SessionContext/JoanieSessionProvider';
9
+ import { createTestQueryClient } from 'utils/test/createTestQueryClient';
10
+ import { OrganizationFactory } from 'utils/test/factories/joanie';
11
+ import { Organization } from 'types/Joanie';
12
+ import useDefaultOrganizationId from '.';
13
+
14
+ jest.mock('utils/context', () => ({
15
+ __esModule: true,
16
+ default: mockRichieContextFactory({
17
+ authentication: { backend: 'fonzie', endpoint: 'https://demo.test' },
18
+ joanie_backend: { endpoint: 'https://joanie.test' },
19
+ }).one(),
20
+ }));
21
+
22
+ interface WrapperProps {
23
+ routePath: string;
24
+ initialEntry: string;
25
+ }
26
+
27
+ describe('useDefaultOrganizationId', () => {
28
+ const organizations: {
29
+ routeOrganization: Organization;
30
+ queryOrganization: Organization;
31
+ userOrganizationList: Organization[];
32
+ } = {
33
+ routeOrganization: OrganizationFactory().one(),
34
+ queryOrganization: OrganizationFactory().one(),
35
+ userOrganizationList: OrganizationFactory().many(2),
36
+ };
37
+
38
+ const Wrapper = ({ children, routePath, initialEntry }: PropsWithChildren<WrapperProps>) => {
39
+ return (
40
+ <IntlProvider locale="en">
41
+ <QueryClientProvider client={createTestQueryClient({ user: true })}>
42
+ <JoanieSessionProvider>
43
+ <MemoryRouter initialEntries={[initialEntry]}>
44
+ <Routes>
45
+ <Route path={routePath} element={children} />
46
+ </Routes>
47
+ </MemoryRouter>
48
+ </JoanieSessionProvider>
49
+ </QueryClientProvider>
50
+ </IntlProvider>
51
+ );
52
+ };
53
+
54
+ beforeEach(() => {
55
+ // Joanie provider's calls
56
+ fetchMock.get('https://joanie.test/api/v1.0/orders/', []);
57
+ fetchMock.get('https://joanie.test/api/v1.0/credit-cards/', []);
58
+ fetchMock.get('https://joanie.test/api/v1.0/addresses/', []);
59
+ });
60
+
61
+ afterEach(() => {
62
+ fetchMock.restore();
63
+ });
64
+
65
+ it.each([
66
+ {
67
+ testLabel: 'route organization before query',
68
+ routeOrganization: organizations.routeOrganization,
69
+ queryOrganization: organizations.queryOrganization,
70
+ userOrganizationList: organizations.userOrganizationList,
71
+ expectedOrganizationId: organizations.routeOrganization.id,
72
+ },
73
+ {
74
+ testLabel: 'query organization before first element of list',
75
+ routeOrganization: undefined,
76
+ queryOrganization: organizations.queryOrganization,
77
+ userOrganizationList: organizations.userOrganizationList,
78
+ expectedOrganizationId: organizations.queryOrganization.id,
79
+ },
80
+ {
81
+ testLabel: 'first element of list when nothing else is found',
82
+ routeOrganization: undefined,
83
+ queryOrganization: undefined,
84
+ userOrganizationList: organizations.userOrganizationList,
85
+ expectedOrganizationId: organizations.userOrganizationList[0].id,
86
+ },
87
+ {
88
+ testLabel: 'undefined when user have no organization in his list',
89
+ routeOrganization: undefined,
90
+ queryOrganization: undefined,
91
+ userOrganizationList: [],
92
+ expectedOrganizationId: undefined,
93
+ },
94
+ ])(
95
+ 'should return $testLabel',
96
+ async ({
97
+ routeOrganization,
98
+ queryOrganization,
99
+ userOrganizationList,
100
+ expectedOrganizationId,
101
+ }) => {
102
+ let routePath = '/';
103
+ if (routeOrganization) {
104
+ routePath += ':organizationId/';
105
+ }
106
+ let initialEntry = '/';
107
+ if (routeOrganization) {
108
+ initialEntry += `${routeOrganization.id}/`;
109
+ }
110
+ if (queryOrganization) {
111
+ initialEntry += `?organization_id=${queryOrganization.id}`;
112
+ }
113
+
114
+ fetchMock.get(
115
+ 'https://joanie.test/api/v1.0/organizations/',
116
+ [...userOrganizationList, routeOrganization, queryOrganization].filter(
117
+ (organization) => organization !== undefined,
118
+ ),
119
+ );
120
+ const { result } = renderHook(useDefaultOrganizationId, {
121
+ wrapper: ({ children }) => (
122
+ <Wrapper routePath={routePath} initialEntry={initialEntry}>
123
+ {children}
124
+ </Wrapper>
125
+ ),
126
+ });
127
+
128
+ // when looking in organization list defaultOrganization will be updated when organizations are fetched.
129
+ await waitFor(() => {
130
+ expect(result.current).toBe(expectedOrganizationId);
131
+ });
132
+ },
133
+ );
134
+ });
@@ -0,0 +1,28 @@
1
+ import { useParams, useSearchParams } from 'react-router-dom';
2
+ import { useOrganizations } from 'hooks/useOrganizations';
3
+ import { Organization } from 'types/Joanie';
4
+
5
+ /**
6
+ * return organization id with this priority:
7
+ * * route param
8
+ * * query param
9
+ * * first organization of user's organizations
10
+ */
11
+ const useDefaultOrganizationId = () => {
12
+ const { organizationId: routeOrganizationId } = useParams<{
13
+ organizationId?: Organization['id'];
14
+ }>();
15
+ const [searchParams] = useSearchParams();
16
+ const queryOrganizationId = searchParams.get('organization_id') || undefined;
17
+ const { items: organizations } = useOrganizations(undefined, {
18
+ enabled: !routeOrganizationId && !queryOrganizationId,
19
+ });
20
+
21
+ return (
22
+ routeOrganizationId ||
23
+ queryOrganizationId ||
24
+ (organizations.length > 0 ? organizations[0].id : undefined)
25
+ );
26
+ };
27
+
28
+ export default useDefaultOrganizationId;
@@ -1,13 +1,17 @@
1
1
  import { useOrganizationContracts } from 'hooks/useContracts';
2
2
  import { PER_PAGE } from 'settings';
3
- import { ContractState, Organization } from 'types/Joanie';
3
+ import { ContractState, CourseProductRelation, Organization } from 'types/Joanie';
4
4
 
5
- const useHasContractToDownload = (organizationId: Organization['id']) => {
5
+ const useHasContractToDownload = (
6
+ organizationId: Organization['id'],
7
+ courseProductRelationId?: CourseProductRelation['id'],
8
+ ) => {
6
9
  const {
7
10
  items: contracts,
8
11
  states: { isFetched },
9
12
  } = useOrganizationContracts({
10
13
  organization_id: organizationId,
14
+ course_product_relation_id: courseProductRelationId,
11
15
  signature_state: ContractState.SIGNED,
12
16
  page: 1,
13
17
  page_size: PER_PAGE.teacherContractList,
@@ -0,0 +1,193 @@
1
+ import fetchMock from 'fetch-mock';
2
+ import { PropsWithChildren } from 'react';
3
+ import { IntlProvider } from 'react-intl';
4
+ import { QueryClientProvider } from '@tanstack/react-query';
5
+ import { renderHook, waitFor } from '@testing-library/react';
6
+ import { MemoryRouter, Route, Routes } from 'react-router-dom';
7
+ import { act } from 'react-dom/test-utils';
8
+ import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
9
+ import { createTestQueryClient } from 'utils/test/createTestQueryClient';
10
+ import JoanieSessionProvider from 'contexts/SessionContext/JoanieSessionProvider';
11
+ import { CourseProductRelationFactory, OrganizationFactory } from 'utils/test/factories/joanie';
12
+ import { ContractState } from 'types/Joanie';
13
+ import useTeacherContractFilters from '.';
14
+
15
+ jest.mock('utils/context', () => ({
16
+ __esModule: true,
17
+ default: mockRichieContextFactory({
18
+ authentication: { backend: 'fonzie', endpoint: 'https://auth.test' },
19
+ joanie_backend: { endpoint: 'https://joanie.test' },
20
+ }).one(),
21
+ }));
22
+
23
+ interface WrapperProps {
24
+ routePath: string;
25
+ initialEntry: string;
26
+ }
27
+
28
+ describe('useTeacherContractFilters', () => {
29
+ const Wrapper = ({ children, routePath, initialEntry }: PropsWithChildren<WrapperProps>) => {
30
+ return (
31
+ <IntlProvider locale="en">
32
+ <QueryClientProvider client={createTestQueryClient({ user: true })}>
33
+ <JoanieSessionProvider>
34
+ <MemoryRouter initialEntries={[initialEntry]}>
35
+ <Routes>
36
+ <Route path={routePath} element={children} />
37
+ </Routes>
38
+ </MemoryRouter>
39
+ </JoanieSessionProvider>
40
+ </QueryClientProvider>
41
+ </IntlProvider>
42
+ );
43
+ };
44
+ beforeEach(() => {
45
+ // Joanie provider's calls
46
+ fetchMock.get('https://joanie.test/api/v1.0/orders/', []);
47
+ fetchMock.get('https://joanie.test/api/v1.0/credit-cards/', []);
48
+ fetchMock.get('https://joanie.test/api/v1.0/addresses/', []);
49
+ });
50
+
51
+ afterEach(() => {
52
+ fetchMock.restore();
53
+ });
54
+
55
+ it('should return default filter when called in a route without parameters', async () => {
56
+ const defaultOrganization = OrganizationFactory({ id: 'default' }).one();
57
+ // fetching user's organizations to initialize default organizationId.
58
+ fetchMock.get('https://joanie.test/api/v1.0/organizations/', [defaultOrganization]);
59
+ const { result } = renderHook(useTeacherContractFilters, {
60
+ wrapper: ({ children }) => (
61
+ <Wrapper routePath="/" initialEntry="/">
62
+ {children}
63
+ </Wrapper>
64
+ ),
65
+ });
66
+
67
+ await waitFor(() => {
68
+ expect(result.current.initialFilters).toStrictEqual({
69
+ contract_ids: [],
70
+ organization_id: defaultOrganization.id,
71
+ course_product_relation_id: undefined,
72
+ signature_state: ContractState.SIGNED,
73
+ });
74
+ expect(result.current.filters).toStrictEqual({
75
+ contract_ids: [],
76
+ organization_id: defaultOrganization.id,
77
+ course_product_relation_id: undefined,
78
+ signature_state: ContractState.SIGNED,
79
+ });
80
+ });
81
+ });
82
+
83
+ it('should use route parameters values when given', async () => {
84
+ const defaultOrganization = OrganizationFactory({ id: 'default' }).one();
85
+ const filteredOrganization = OrganizationFactory({ id: 'filtered' }).one();
86
+ const routeOrganization = OrganizationFactory({ id: 'route' }).one();
87
+ const routeCourseProductRelation = CourseProductRelationFactory().one();
88
+ // fetching user's organizations to initialize default organizationId.
89
+ fetchMock.get('https://joanie.test/api/v1.0/organizations/', [
90
+ defaultOrganization,
91
+ filteredOrganization,
92
+ ]);
93
+ const { result } = renderHook(useTeacherContractFilters, {
94
+ wrapper: ({ children }) => (
95
+ <Wrapper
96
+ routePath="/:organizationId/:courseProductRelationId"
97
+ initialEntry={`/${routeOrganization.id}/${routeCourseProductRelation.id}?organization_id=${filteredOrganization.id}&signature_state=${ContractState?.UNSIGNED}&contract_ids=1&contract_ids=2`}
98
+ >
99
+ {children}
100
+ </Wrapper>
101
+ ),
102
+ });
103
+
104
+ await waitFor(() => {
105
+ expect(result.current.initialFilters).toStrictEqual({
106
+ contract_ids: ['1', '2'],
107
+ organization_id: routeOrganization.id,
108
+ course_product_relation_id: routeCourseProductRelation.id,
109
+ signature_state: ContractState.UNSIGNED,
110
+ });
111
+ expect(result.current.filters).toStrictEqual({
112
+ contract_ids: ['1', '2'],
113
+ organization_id: routeOrganization.id,
114
+ course_product_relation_id: routeCourseProductRelation.id,
115
+ signature_state: ContractState.UNSIGNED,
116
+ });
117
+ });
118
+ });
119
+
120
+ it("should use organizationId from query parameters when it's not in route params", async () => {
121
+ const defaultOrganization = OrganizationFactory({ id: 'default' }).one();
122
+ const filteredOrganization = OrganizationFactory({ id: 'filtered' }).one();
123
+ const routeCourseProductRelation = CourseProductRelationFactory({ id: 'route' }).one();
124
+ // fetching user's organizations to initialize default organizationId.
125
+ fetchMock.get('https://joanie.test/api/v1.0/organizations/', [
126
+ defaultOrganization,
127
+ filteredOrganization,
128
+ ]);
129
+ const { result } = renderHook(useTeacherContractFilters, {
130
+ wrapper: ({ children }) => (
131
+ <Wrapper
132
+ routePath="/:courseProductRelationId"
133
+ initialEntry={`/${routeCourseProductRelation.id}/?organization_id=${filteredOrganization.id}&signature_state=${ContractState?.UNSIGNED}&contract_ids=1&contract_ids=2`}
134
+ >
135
+ {children}
136
+ </Wrapper>
137
+ ),
138
+ });
139
+
140
+ await waitFor(() => {
141
+ expect(result.current.initialFilters).toStrictEqual({
142
+ contract_ids: ['1', '2'],
143
+ organization_id: filteredOrganization.id,
144
+ course_product_relation_id: routeCourseProductRelation.id,
145
+ signature_state: ContractState.UNSIGNED,
146
+ });
147
+ expect(result.current.filters).toStrictEqual({
148
+ contract_ids: ['1', '2'],
149
+ organization_id: filteredOrganization.id,
150
+ course_product_relation_id: routeCourseProductRelation.id,
151
+ signature_state: ContractState.UNSIGNED,
152
+ });
153
+ });
154
+ });
155
+
156
+ it('setFilters should update filter state', async () => {
157
+ const defaultOrganization = OrganizationFactory({ id: 'default' }).one();
158
+ const routeOrganization = OrganizationFactory({ id: 'route' }).one();
159
+ const routeCourseProductRelation = CourseProductRelationFactory().one();
160
+ // fetching user's organizations to initialize default organizationId.
161
+ fetchMock.get('https://joanie.test/api/v1.0/organizations/', [defaultOrganization]);
162
+ const { result } = renderHook(useTeacherContractFilters, {
163
+ wrapper: ({ children }) => (
164
+ <Wrapper routePath="/" initialEntry="/">
165
+ {children}
166
+ </Wrapper>
167
+ ),
168
+ });
169
+
170
+ const expectedInitialFilters = {
171
+ contract_ids: [],
172
+ organization_id: defaultOrganization.id,
173
+ course_product_relation_id: undefined,
174
+ signature_state: ContractState.SIGNED,
175
+ };
176
+ await waitFor(() => {
177
+ expect(result.current.initialFilters).toStrictEqual(expectedInitialFilters);
178
+ });
179
+
180
+ const newFilters = {
181
+ contract_ids: ['1', '2'],
182
+ organization_id: routeOrganization.id,
183
+ course_product_relation_id: routeCourseProductRelation.id,
184
+ signature_state: ContractState.UNSIGNED,
185
+ };
186
+ act(() => {
187
+ result.current.setFilters(newFilters);
188
+ });
189
+
190
+ expect(result.current.filters).toStrictEqual(newFilters);
191
+ expect(result.current.initialFilters).toStrictEqual(expectedInitialFilters);
192
+ });
193
+ });
@@ -0,0 +1,44 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import { useParams, useSearchParams } from 'react-router-dom';
3
+ import { ContractResourceQuery, ContractState } from 'types/Joanie';
4
+ import useDefaultOrganizationId from '../useDefaultOrganizationId';
5
+
6
+ export type TeacherDashboardContractsParams = {
7
+ organizationId?: string;
8
+ courseProductRelationId?: string;
9
+ };
10
+
11
+ const useTeacherContractFilters = () => {
12
+ const { courseProductRelationId } = useParams<TeacherDashboardContractsParams>();
13
+ const [searchParams] = useSearchParams();
14
+ const searchFilters: ContractResourceQuery = useMemo(() => {
15
+ return {
16
+ organization_id: searchParams.get('organization_id') || undefined,
17
+ course_product_relation_id: searchParams.get('course_product_relation_id') || undefined,
18
+ contract_ids: searchParams.getAll('contract_ids') || undefined,
19
+ signature_state:
20
+ (searchParams.get('signature_state') as ContractState) || ContractState.SIGNED,
21
+ };
22
+ }, Array.from(searchParams.entries()));
23
+
24
+ // default orgnizationId between (ordered by priority): route, query, first user's organization.
25
+ const defaultOrganizationId = useDefaultOrganizationId();
26
+
27
+ const initialFilters = useMemo(() => {
28
+ return {
29
+ ...searchFilters,
30
+ organization_id: defaultOrganizationId,
31
+ course_product_relation_id: courseProductRelationId,
32
+ };
33
+ }, [defaultOrganizationId]);
34
+ const [filters, setFilters] = useState<ContractResourceQuery>(initialFilters);
35
+
36
+ // update current filter with initial value when it's ready
37
+ useEffect(() => {
38
+ setFilters(initialFilters);
39
+ }, [initialFilters]);
40
+
41
+ return { initialFilters, filters, setFilters };
42
+ };
43
+
44
+ export default useTeacherContractFilters;
@@ -78,7 +78,7 @@ describe('utils/search/getSuggestionsSection', () => {
78
78
  expect(mockHandle).toHaveBeenCalledWith(
79
79
  new Error(
80
80
  'Failed to decode JSON in getSuggestionSection FetchError: invalid json response body at ' +
81
- '/api/v1.0/courses/autocomplete/?query=some%20search reason: Unexpected token o in JSON at position 1',
81
+ '/api/v1.0/courses/autocomplete/?query=some%20search reason: Unexpected token \'o\', "not json" is not valid JSON',
82
82
  ),
83
83
  );
84
84
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "2.25.0-b2.dev49",
3
+ "version": "2.25.0-b2.dev58",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {
@@ -147,6 +147,6 @@
147
147
  "workerDirectory": "../richie/static/richie/js"
148
148
  },
149
149
  "volta": {
150
- "node": "18.19.0"
150
+ "node": "20.11.0"
151
151
  }
152
152
  }
@@ -131,18 +131,12 @@
131
131
  }
132
132
 
133
133
  &__logo {
134
- position: relative;
134
+ height: 100%;
135
135
  width: 100%;
136
- padding-bottom: 56.25%; // Aspect ratio 16/9
137
136
 
138
137
  & > img {
139
- position: absolute;
140
- top: 0;
141
- right: 0;
142
- bottom: 0;
143
- left: 0;
144
- width: 100%;
145
138
  height: 100%;
139
+ width: 100%;
146
140
  object-fit: contain;
147
141
  object-position: center;
148
142
  }
@@ -1,28 +0,0 @@
1
- import { useMemo, useState } from 'react';
2
- import { useParams, useSearchParams } from 'react-router-dom';
3
- import { ContractResourceQuery, ContractState } from 'types/Joanie';
4
-
5
- export type TeacherDashboardContractsParams = {
6
- organizationId?: string;
7
- courseProductRelationId?: string;
8
- };
9
-
10
- const useTeacherContractFilters = () => {
11
- const { organizationId, courseProductRelationId } = useParams<TeacherDashboardContractsParams>();
12
- const [searchParams] = useSearchParams();
13
-
14
- const initialFilters = useMemo(
15
- () => ({
16
- signature_state:
17
- (searchParams.get('signature_state') as ContractState) || ContractState.SIGNED,
18
- organization_id: organizationId,
19
- course_product_relation_id: courseProductRelationId,
20
- }),
21
- [],
22
- );
23
- const [filters, setFilters] = useState<ContractResourceQuery>(initialFilters);
24
-
25
- return { initialFilters, filters, setFilters };
26
- };
27
-
28
- export default useTeacherContractFilters;