richie-education 2.14.1 → 2.16.0
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/.storybook/main.js +17 -0
- package/.storybook/preview-body.html +1 -0
- package/.storybook/preview-head.html +1 -0
- package/.storybook/preview.tsx +38 -0
- package/jest/resolver.js +34 -0
- package/jest/setup.ts +30 -0
- package/jest.config.js +6 -3
- package/js/components/AddressesManagement/AddressForm.spec.tsx +191 -0
- package/js/components/AddressesManagement/AddressForm.tsx +32 -4
- package/js/components/AddressesManagement/_styles.scss +1 -79
- package/js/components/AddressesManagement/index.spec.tsx +42 -182
- package/js/components/AddressesManagement/index.tsx +3 -4
- package/js/components/AddressesManagement/validationSchema.spec.ts +147 -0
- package/js/components/AddressesManagement/validationSchema.ts +51 -0
- package/js/components/Banner/index.stories.tsx +32 -0
- package/js/components/CourseProductItem/index.spec.tsx +7 -65
- package/js/components/CourseProductItem/index.tsx +4 -3
- package/js/components/CourseRunEnrollment/index.spec.tsx +1 -1
- package/js/components/DashBoard/index.spec.tsx +38 -0
- package/js/components/DashBoard/index.tsx +5 -0
- package/js/components/Form/CheckboxField.stories.tsx +13 -0
- package/js/components/Form/Field.stories.tsx +16 -0
- package/js/components/Form/Inputs.tsx +3 -3
- package/js/components/Form/RadioField.stories.tsx +18 -0
- package/js/components/Form/SelectField.stories.tsx +26 -0
- package/js/components/Form/TextAreaField.stories.tsx +13 -0
- package/js/components/Form/TextField.stories.tsx +13 -0
- package/js/components/Icon/index.stories.tsx +11 -0
- package/js/components/LtiConsumer/index.spec.tsx +244 -11
- package/js/components/LtiConsumer/index.tsx +46 -13
- package/js/components/Modal/index.stories.tsx +42 -0
- package/js/components/PaymentButton/index.tsx +1 -1
- package/js/components/RegisteredAddress/_styles.scss +83 -0
- package/js/components/RegisteredAddress/index.tsx +3 -3
- package/js/components/Root/index.tsx +3 -0
- package/js/components/SaleTunnel/_styles.scss +30 -0
- package/js/components/SaleTunnel/index.spec.tsx +8 -0
- package/js/components/SaleTunnel/index.tsx +4 -2
- package/js/components/SaleTunnelStepPayment/index.spec.tsx +3 -3
- package/js/components/SaleTunnelStepPayment/index.tsx +8 -4
- package/js/components/SaleTunnelStepResume/index.tsx +1 -1
- package/js/components/SaleTunnelStepValidation/index.spec.tsx +1 -1
- package/js/components/SaleTunnelStepValidation/index.tsx +1 -1
- package/js/components/Search/FiltersPaneCloseButton.tsx +49 -0
- package/js/components/Search/index.spec.tsx +57 -25
- package/js/components/Search/index.tsx +60 -49
- package/js/components/SearchFiltersPane/index.tsx +4 -1
- package/js/components/Spinner/index.stories.tsx +26 -0
- package/js/data/CourseCodeProvider/index.spec.tsx +5 -0
- package/js/data/JoanieApiProvider/index.spec.tsx +5 -0
- package/js/data/SessionProvider/BaseSessionProvider.tsx +1 -1
- package/js/data/SessionProvider/JoanieSessionProvider.tsx +1 -1
- package/js/data/SessionProvider/index.spec.tsx +2 -2
- package/js/settings.ts +1 -0
- package/js/types/Joanie.ts +2 -2
- package/js/types/api.ts +1 -1
- package/js/types/globals.d.ts +8 -0
- package/js/utils/api/authentication.ts +3 -3
- package/js/utils/api/lms/{base.spec.ts → dummy.spec.ts} +49 -12
- package/js/utils/api/lms/{base.ts → dummy.ts} +27 -3
- package/js/utils/api/lms/index.spec.ts +1 -1
- package/js/utils/api/lms/index.ts +4 -4
- package/js/utils/test/factories.ts +11 -12
- package/package.json +61 -48
- package/scss/_main.scss +13 -101
- package/scss/colors/_theme.scss +8 -0
- package/scss/components/_header.scss +20 -2
- package/scss/components/_index.scss +52 -0
- package/scss/components/templates/courses/cms/_blogpost_detail.scss +18 -1
- package/scss/components/templates/search/_search.scss +17 -0
- package/scss/generic/_index.scss +6 -0
- package/scss/objects/_blogpost_glimpses.scss +19 -4
- package/scss/objects/_course_glimpses.scss +0 -1
- package/scss/objects/_form.scss +26 -21
- package/scss/objects/_index.scss +17 -0
- package/scss/settings/_bootstrap.scss +18 -0
- package/scss/settings/_variables.scss +24 -33
- package/scss/tools/_buttons.scss +1 -0
- package/scss/tools/_colors.scss +17 -0
- package/scss/tools/_index.scss +14 -0
- package/scss/tools/_rem.scss +6 -1
- package/stories/Introduction.stories.mdx +13 -0
- package/js/components/CourseProductItem/PurchasedProductMenu.tsx +0 -135
- package/js/testSetup.ts +0 -13
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
"stories": [
|
|
3
|
+
"../stories/**/*.stories.mdx",
|
|
4
|
+
"../stories/**/*.stories.@(js|jsx|ts|tsx)",
|
|
5
|
+
"../js/**/*.stories.@(js|jsx|ts|tsx)",
|
|
6
|
+
],
|
|
7
|
+
"addons": [
|
|
8
|
+
"@storybook/addon-links",
|
|
9
|
+
"@storybook/addon-essentials",
|
|
10
|
+
"@storybook/addon-interactions"
|
|
11
|
+
],
|
|
12
|
+
"framework": "@storybook/react",
|
|
13
|
+
"core": {
|
|
14
|
+
"builder": "@storybook/builder-webpack5"
|
|
15
|
+
},
|
|
16
|
+
staticDirs: ['../../richie/static', '../../richie/apps/core/templates/richie'],
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div id="modal-exclude" style="min-height: 0;"></div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<link rel="stylesheet" type="text/css" href="/richie/css/main.css">
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { useAsyncEffect } from "../js/utils/useAsyncEffect";
|
|
3
|
+
|
|
4
|
+
export const parameters = {
|
|
5
|
+
actions: {argTypesRegex: "^on[A-Z].*"},
|
|
6
|
+
controls: {
|
|
7
|
+
matchers: {
|
|
8
|
+
color: /(background|color)$/i,
|
|
9
|
+
date: /Date$/,
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
const IconsWrapper = props => {
|
|
16
|
+
const [symbols, setSymbols] = useState('');
|
|
17
|
+
|
|
18
|
+
useAsyncEffect(async () => {
|
|
19
|
+
const response = await fetch('/icons.html');
|
|
20
|
+
const body = await response.text();
|
|
21
|
+
setSymbols(body);
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div>
|
|
26
|
+
<div dangerouslySetInnerHTML={{__html: symbols}}/>
|
|
27
|
+
{props.children}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const decorators = [
|
|
33
|
+
(Story) => (
|
|
34
|
+
<IconsWrapper>
|
|
35
|
+
<Story/>
|
|
36
|
+
</IconsWrapper>
|
|
37
|
+
),
|
|
38
|
+
];
|
package/jest/resolver.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
/*
|
|
4
|
+
Read this issue for further information
|
|
5
|
+
https://github.com/microsoft/accessibility-insights-web/pull/5421#issuecomment-1109168149
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
module.exports = (path, options) => {
|
|
9
|
+
// Call the defaultResolver, so we leverage its cache, error handling, etc.
|
|
10
|
+
return options.defaultResolver(path, {
|
|
11
|
+
...options,
|
|
12
|
+
// Use packageFilter to process parsed `package.json` before the resolution (see https://www.npmjs.com/package/resolve#resolveid-opts-cb)
|
|
13
|
+
packageFilter: (pkg) => {
|
|
14
|
+
// This is a workaround for https://github.com/uuidjs/uuid/pull/616
|
|
15
|
+
//
|
|
16
|
+
// jest-environment-jsdom 28+ tries to use browser exports instead of default exports,
|
|
17
|
+
// but uuid only offers an ESM browser export and not a CommonJS one. Jest does not yet
|
|
18
|
+
// support ESM modules natively, so this causes a Jest error related to trying to parse
|
|
19
|
+
// "export" syntax.
|
|
20
|
+
//
|
|
21
|
+
// This workaround prevents Jest from considering uuid's module-based exports at all;
|
|
22
|
+
// it falls back to uuid's CommonJS+node "main" property.
|
|
23
|
+
//
|
|
24
|
+
// Once we're able to migrate our Jest config to ESM and a browser crypto
|
|
25
|
+
// implementation is available for the browser+ESM version of uuid to use (eg, via
|
|
26
|
+
// https://github.com/jsdom/jsdom/pull/3352 or a similar polyfill), this can go away.
|
|
27
|
+
if (pkg.name === 'uuid') {
|
|
28
|
+
delete pkg.exports;
|
|
29
|
+
delete pkg.module;
|
|
30
|
+
}
|
|
31
|
+
return pkg;
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
};
|
package/jest/setup.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Extend jest matchers with jest-dom's
|
|
2
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
3
|
+
|
|
4
|
+
import { setLogger } from 'react-query';
|
|
5
|
+
import { noop } from 'utils';
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* A little trick to prevent so package to be reset when using `jest.resetModules()`.
|
|
9
|
+
* https://github.com/facebook/jest/issues/8987#issuecomment-584898030
|
|
10
|
+
*/
|
|
11
|
+
const RESET_MODULE_EXCEPTIONS = ['react', 'react-intl'];
|
|
12
|
+
|
|
13
|
+
const mockActualRegistry: Record<PropertyKey, any> = {};
|
|
14
|
+
|
|
15
|
+
RESET_MODULE_EXCEPTIONS.forEach((moduleName) => {
|
|
16
|
+
jest.doMock(moduleName, () => {
|
|
17
|
+
if (!mockActualRegistry[moduleName]) {
|
|
18
|
+
mockActualRegistry[moduleName] = jest.requireActual(moduleName);
|
|
19
|
+
}
|
|
20
|
+
return mockActualRegistry[moduleName];
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/* Prevent log error during tests */
|
|
25
|
+
setLogger({
|
|
26
|
+
// eslint-disable-next-line no-console
|
|
27
|
+
log: console.log,
|
|
28
|
+
warn: console.warn,
|
|
29
|
+
error: noop,
|
|
30
|
+
});
|
package/jest.config.js
CHANGED
|
@@ -6,12 +6,15 @@ module.exports = {
|
|
|
6
6
|
moduleNameMapper: {
|
|
7
7
|
'\\.(css)$': '<rootDir>/front/__mocks__/styleMock.js',
|
|
8
8
|
},
|
|
9
|
-
setupFilesAfterEnv: ['
|
|
9
|
+
setupFilesAfterEnv: ['<rootDir>/jest/setup.ts'],
|
|
10
10
|
testMatch: [`${__dirname}/js/**/*.spec.+(ts|tsx|js)`],
|
|
11
|
-
testURL: 'https://localhost',
|
|
12
11
|
coverageDirectory: '.coverage',
|
|
13
12
|
testEnvironment: 'jsdom',
|
|
14
|
-
|
|
13
|
+
testEnvironmentOptions: {
|
|
14
|
+
url: 'https://localhost',
|
|
15
|
+
},
|
|
16
|
+
resolver: '<rootDir>/jest/resolver.js',
|
|
17
|
+
transformIgnorePatterns: ['node_modules/(?!(lodash-es|@hookform/resolvers)/)'],
|
|
15
18
|
globals: {
|
|
16
19
|
RICHIE_VERSION: 'test',
|
|
17
20
|
},
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { act } from '@testing-library/react-hooks';
|
|
2
|
+
import { fireEvent, getByText, render, screen } from '@testing-library/react';
|
|
3
|
+
import * as mockFactories from 'utils/test/factories';
|
|
4
|
+
import { AddressFactory } from 'utils/test/factories';
|
|
5
|
+
import countries from 'i18n-iso-countries';
|
|
6
|
+
import { IntlProvider } from 'react-intl';
|
|
7
|
+
import { Address } from 'types/Joanie';
|
|
8
|
+
import { ErrorKeys } from './validationSchema';
|
|
9
|
+
import AddressForm from './AddressForm';
|
|
10
|
+
|
|
11
|
+
jest.mock('hooks/useAddresses', () => ({
|
|
12
|
+
useAddresses: () => ({
|
|
13
|
+
states: {
|
|
14
|
+
creating: false,
|
|
15
|
+
updating: false,
|
|
16
|
+
},
|
|
17
|
+
}),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
describe('AddressForm', () => {
|
|
21
|
+
const handleReset = jest.fn();
|
|
22
|
+
const onSubmit = jest.fn();
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
jest.resetAllMocks();
|
|
26
|
+
jest.resetModules();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('renders a button with label "Use this address" when no address is provided', () => {
|
|
30
|
+
render(
|
|
31
|
+
<IntlProvider locale="en">
|
|
32
|
+
<AddressForm handleReset={handleReset} onSubmit={onSubmit} />
|
|
33
|
+
</IntlProvider>,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
screen.getByRole('button', { name: 'Use this address' });
|
|
37
|
+
expect(screen.queryByRole('button', { name: 'Cancel' })).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('renders a button with label "Use this address" and a cancel button when no address is provided', async () => {
|
|
41
|
+
const address: Address = AddressFactory.generate();
|
|
42
|
+
render(
|
|
43
|
+
<IntlProvider locale="en">
|
|
44
|
+
<AddressForm handleReset={handleReset} onSubmit={onSubmit} address={address} />
|
|
45
|
+
</IntlProvider>,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
screen.getByRole('button', { name: 'Update this address' });
|
|
49
|
+
|
|
50
|
+
const $button = screen.getByRole('button', { name: 'Cancel' });
|
|
51
|
+
await act(async () => {
|
|
52
|
+
fireEvent.click($button);
|
|
53
|
+
});
|
|
54
|
+
expect(handleReset).toHaveBeenCalledTimes(1);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('renders an error message when a value in the form is invalid', async () => {
|
|
58
|
+
render(
|
|
59
|
+
<IntlProvider locale="en">
|
|
60
|
+
<AddressForm handleReset={handleReset} onSubmit={onSubmit} />
|
|
61
|
+
</IntlProvider>,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
screen.getByRole('form');
|
|
65
|
+
const $titleInput = screen.getByRole('textbox', { name: 'Address title' });
|
|
66
|
+
const $firstnameInput = screen.getByRole('textbox', { name: "Recipient's first name" });
|
|
67
|
+
const $lastnameInput = screen.getByRole('textbox', { name: "Recipient's last name" });
|
|
68
|
+
const $addressInput = screen.getByRole('textbox', { name: 'Address' });
|
|
69
|
+
const $cityInput = screen.getByRole('textbox', { name: 'City' });
|
|
70
|
+
const $postcodeInput = screen.getByRole('textbox', { name: 'Postcode' });
|
|
71
|
+
const $countryInput = screen.getByRole('combobox', { name: 'Country' });
|
|
72
|
+
const $submitButton = screen.getByRole('button', {
|
|
73
|
+
name: 'Use this address',
|
|
74
|
+
}) as HTMLButtonElement;
|
|
75
|
+
|
|
76
|
+
// - Until form is not fulfill, submit button should be disabled
|
|
77
|
+
expect($submitButton.disabled).toBe(true);
|
|
78
|
+
|
|
79
|
+
// - User fulfills address fields
|
|
80
|
+
const address = mockFactories.AddressFactory.generate();
|
|
81
|
+
|
|
82
|
+
await act(async () => {
|
|
83
|
+
fireEvent.input($titleInput, { target: { value: address.title } });
|
|
84
|
+
fireEvent.change($firstnameInput, { target: { value: address.first_name } });
|
|
85
|
+
fireEvent.change($lastnameInput, { target: { value: address.last_name } });
|
|
86
|
+
fireEvent.change($addressInput, { target: { value: address.address } });
|
|
87
|
+
fireEvent.change($cityInput, { target: { value: address.city } });
|
|
88
|
+
fireEvent.change($postcodeInput, { target: { value: address.postcode } });
|
|
89
|
+
fireEvent.change($countryInput, { target: { value: address.country } });
|
|
90
|
+
// - As form validation is triggered on blur, we need to trigger this event in
|
|
91
|
+
// order to update form state.
|
|
92
|
+
fireEvent.blur($countryInput);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Once the form has been fulfilled properly, submit button should be enabled.
|
|
96
|
+
expect($submitButton.disabled).toBe(false);
|
|
97
|
+
|
|
98
|
+
// Before submitting, we change field values to corrupt the form data
|
|
99
|
+
await act(async () => {
|
|
100
|
+
fireEvent.input($titleInput, { target: { value: 'a' } });
|
|
101
|
+
fireEvent.input($firstnameInput, { target: { value: '' } });
|
|
102
|
+
fireEvent.change($countryInput, { target: { value: '-' } });
|
|
103
|
+
fireEvent.click($submitButton);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
107
|
+
|
|
108
|
+
// Error messages should have been displayed.
|
|
109
|
+
// Title field should have a message saying that the value is too short.
|
|
110
|
+
getByText($titleInput.closest('.form-field')!, 'The minimum length is 2 chars.');
|
|
111
|
+
// Firstname field should have a message saying that the value is required.
|
|
112
|
+
getByText($firstnameInput.closest('.form-field')!, 'This field is required.');
|
|
113
|
+
// Country field should have a message saying that the value is not valid.
|
|
114
|
+
getByText(
|
|
115
|
+
$countryInput.closest('.form-field')!,
|
|
116
|
+
`You must select a value within: ${Object.keys(countries.getAlpha2Codes()).join(', ')}.`,
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('renders default error message when error message does not exist', async () => {
|
|
121
|
+
jest.doMock('./validationSchema', () => ({
|
|
122
|
+
__esModule: true,
|
|
123
|
+
...jest.requireActual('./validationSchema'),
|
|
124
|
+
errorMessages: {
|
|
125
|
+
[ErrorKeys.MIXED_INVALID]: {
|
|
126
|
+
id: 'components.AddressesManagement.validationSchema.mixedInvalid',
|
|
127
|
+
defaultMessage: 'This field is invalid.',
|
|
128
|
+
description: 'Error message displayed when a field value is invalid.',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
}));
|
|
132
|
+
|
|
133
|
+
// Import locally to get module with mocked error messages.
|
|
134
|
+
const Form = jest.requireActual('./AddressForm').default;
|
|
135
|
+
|
|
136
|
+
render(
|
|
137
|
+
<IntlProvider locale="en">
|
|
138
|
+
<Form handleReset={handleReset} onSubmit={onSubmit} />
|
|
139
|
+
</IntlProvider>,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
screen.getByRole('form');
|
|
143
|
+
const $titleInput = screen.getByRole('textbox', { name: 'Address title' });
|
|
144
|
+
const $firstnameInput = screen.getByRole('textbox', { name: "Recipient's first name" });
|
|
145
|
+
const $lastnameInput = screen.getByRole('textbox', { name: "Recipient's last name" });
|
|
146
|
+
const $addressInput = screen.getByRole('textbox', { name: 'Address' });
|
|
147
|
+
const $cityInput = screen.getByRole('textbox', { name: 'City' });
|
|
148
|
+
const $postcodeInput = screen.getByRole('textbox', { name: 'Postcode' });
|
|
149
|
+
const $countryInput = screen.getByRole('combobox', { name: 'Country' });
|
|
150
|
+
const $submitButton = screen.getByRole('button', {
|
|
151
|
+
name: 'Use this address',
|
|
152
|
+
}) as HTMLButtonElement;
|
|
153
|
+
|
|
154
|
+
// - Until form is not fulfill, submit button should be disabled
|
|
155
|
+
expect($submitButton.disabled).toBe(true);
|
|
156
|
+
|
|
157
|
+
// - User fulfills address fields
|
|
158
|
+
const address = mockFactories.AddressFactory.generate();
|
|
159
|
+
|
|
160
|
+
await act(async () => {
|
|
161
|
+
fireEvent.input($titleInput, { target: { value: address.title } });
|
|
162
|
+
fireEvent.change($firstnameInput, { target: { value: address.first_name } });
|
|
163
|
+
fireEvent.change($lastnameInput, { target: { value: address.last_name } });
|
|
164
|
+
fireEvent.change($addressInput, { target: { value: address.address } });
|
|
165
|
+
fireEvent.change($cityInput, { target: { value: address.city } });
|
|
166
|
+
fireEvent.change($postcodeInput, { target: { value: address.postcode } });
|
|
167
|
+
fireEvent.change($countryInput, { target: { value: address.country } });
|
|
168
|
+
// - As form validation is triggered on blur, we need to trigger this event in
|
|
169
|
+
// order to update form state.
|
|
170
|
+
fireEvent.blur($countryInput);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Once the form has been fulfilled properly, submit button should be enabled.
|
|
174
|
+
expect($submitButton.disabled).toBe(false);
|
|
175
|
+
|
|
176
|
+
// Before submitting, we change field values to corrupt the form data
|
|
177
|
+
await act(async () => {
|
|
178
|
+
fireEvent.input($titleInput, { target: { value: 'a' } });
|
|
179
|
+
fireEvent.input($firstnameInput, { target: { value: '' } });
|
|
180
|
+
fireEvent.change($countryInput, { target: { value: '-' } });
|
|
181
|
+
fireEvent.click($submitButton);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
185
|
+
|
|
186
|
+
// Default error messages should have been displayed.
|
|
187
|
+
getByText($titleInput.closest('.form-field')!, 'This field is invalid.');
|
|
188
|
+
getByText($firstnameInput.closest('.form-field')!, 'This field is invalid.');
|
|
189
|
+
getByText($countryInput.closest('.form-field')!, `This field is invalid.`);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -7,7 +7,8 @@ import { messages } from 'components/AddressesManagement/index';
|
|
|
7
7
|
import { CheckboxField, SelectField, TextField } from 'components/Form';
|
|
8
8
|
import { useAddresses } from 'hooks/useAddresses';
|
|
9
9
|
import type { Address } from 'types/Joanie';
|
|
10
|
-
import
|
|
10
|
+
import { Maybe } from 'types/utils';
|
|
11
|
+
import validationSchema, { ErrorKeys, errorMessages } from './validationSchema';
|
|
11
12
|
|
|
12
13
|
export type AddressFormValues = Omit<Address, 'id' | 'is_main'> & { save: boolean };
|
|
13
14
|
|
|
@@ -41,6 +42,25 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
41
42
|
const [languageCode] = intl.locale.split('-');
|
|
42
43
|
const countryList = countries.getNames(languageCode);
|
|
43
44
|
|
|
45
|
+
const getLocalizedErrorMessage = (
|
|
46
|
+
error: Maybe<
|
|
47
|
+
| string
|
|
48
|
+
| {
|
|
49
|
+
key: ErrorKeys;
|
|
50
|
+
values: Record<PropertyKey, string | number | Array<string | number>>;
|
|
51
|
+
}
|
|
52
|
+
>,
|
|
53
|
+
) => {
|
|
54
|
+
if (!error) return undefined;
|
|
55
|
+
|
|
56
|
+
if (typeof error === 'string' || errorMessages[error.key] === undefined) {
|
|
57
|
+
// If the error has not been translated we return a default error message.
|
|
58
|
+
return intl.formatMessage(errorMessages[ErrorKeys.MIXED_INVALID]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return intl.formatMessage(errorMessages[error.key], error.values);
|
|
62
|
+
};
|
|
63
|
+
|
|
44
64
|
/**
|
|
45
65
|
* Prevent form to be submitted and clear `editedAddress` state.
|
|
46
66
|
*/
|
|
@@ -62,6 +82,7 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
62
82
|
id="title"
|
|
63
83
|
label={intl.formatMessage(messages.titleInputLabel)}
|
|
64
84
|
error={!!formState.errors.title}
|
|
85
|
+
message={getLocalizedErrorMessage(formState.errors.title?.message)}
|
|
65
86
|
{...register('title')}
|
|
66
87
|
/>
|
|
67
88
|
<div className="form-group">
|
|
@@ -71,6 +92,7 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
71
92
|
id="first_name"
|
|
72
93
|
label={intl.formatMessage(messages.first_nameInputLabel)}
|
|
73
94
|
error={!!formState.errors.first_name}
|
|
95
|
+
message={getLocalizedErrorMessage(formState.errors.first_name?.message)}
|
|
74
96
|
{...register('first_name')}
|
|
75
97
|
/>
|
|
76
98
|
<TextField
|
|
@@ -79,6 +101,7 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
79
101
|
id="last_name"
|
|
80
102
|
label={intl.formatMessage(messages.last_nameInputLabel)}
|
|
81
103
|
error={!!formState.errors.last_name}
|
|
104
|
+
message={getLocalizedErrorMessage(formState.errors.last_name?.message)}
|
|
82
105
|
{...register('last_name')}
|
|
83
106
|
/>
|
|
84
107
|
</div>
|
|
@@ -88,6 +111,7 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
88
111
|
id="address"
|
|
89
112
|
label={intl.formatMessage(messages.addressInputLabel)}
|
|
90
113
|
error={!!formState.errors.address}
|
|
114
|
+
message={getLocalizedErrorMessage(formState.errors.address?.message)}
|
|
91
115
|
{...register('address')}
|
|
92
116
|
/>
|
|
93
117
|
<div className="form-group">
|
|
@@ -97,6 +121,7 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
97
121
|
id="postcode"
|
|
98
122
|
label={intl.formatMessage(messages.postcodeInputLabel)}
|
|
99
123
|
error={!!formState.errors.postcode}
|
|
124
|
+
message={getLocalizedErrorMessage(formState.errors.postcode?.message)}
|
|
100
125
|
{...register('postcode')}
|
|
101
126
|
/>
|
|
102
127
|
<TextField
|
|
@@ -105,6 +130,7 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
105
130
|
id="city"
|
|
106
131
|
label={intl.formatMessage(messages.cityInputLabel)}
|
|
107
132
|
error={!!formState.errors.city}
|
|
133
|
+
message={getLocalizedErrorMessage(formState.errors.city?.message)}
|
|
108
134
|
{...register('city')}
|
|
109
135
|
/>
|
|
110
136
|
</div>
|
|
@@ -114,6 +140,7 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
114
140
|
id="country"
|
|
115
141
|
label={intl.formatMessage(messages.countryInputLabel)}
|
|
116
142
|
error={!!formState.errors.country}
|
|
143
|
+
message={getLocalizedErrorMessage(formState.errors.country?.message)}
|
|
117
144
|
{...register('country', { value: address?.country, required: true })}
|
|
118
145
|
>
|
|
119
146
|
<option disabled value="-">
|
|
@@ -132,6 +159,7 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
132
159
|
id="save"
|
|
133
160
|
label={intl.formatMessage(messages.saveInputLabel)}
|
|
134
161
|
error={!!formState.errors?.save}
|
|
162
|
+
message={getLocalizedErrorMessage(formState.errors.save?.message)}
|
|
135
163
|
{...register('save')}
|
|
136
164
|
/>
|
|
137
165
|
) : null}
|
|
@@ -139,14 +167,14 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
139
167
|
{address ? (
|
|
140
168
|
<Fragment>
|
|
141
169
|
<button
|
|
142
|
-
className="button button--
|
|
170
|
+
className="button button-sale--tertiary"
|
|
143
171
|
onClick={handleCancel}
|
|
144
172
|
title={intl.formatMessage(messages.cancelTitleButton)}
|
|
145
173
|
>
|
|
146
174
|
<FormattedMessage {...messages.cancelButton} />
|
|
147
175
|
</button>
|
|
148
176
|
<button
|
|
149
|
-
className="button button--primary"
|
|
177
|
+
className="button button-sale--primary"
|
|
150
178
|
disabled={!formState.isValid || addresses.states.updating}
|
|
151
179
|
type="submit"
|
|
152
180
|
>
|
|
@@ -155,7 +183,7 @@ const AddressForm = ({ handleReset, onSubmit, address }: Props) => {
|
|
|
155
183
|
</Fragment>
|
|
156
184
|
) : (
|
|
157
185
|
<button
|
|
158
|
-
className="button button--primary"
|
|
186
|
+
className="button button-sale--primary"
|
|
159
187
|
disabled={!formState.isValid || addresses.states.creating || addresses.states.updating}
|
|
160
188
|
type="submit"
|
|
161
189
|
>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
& > .button {
|
|
11
|
-
.button__icon {
|
|
11
|
+
.icon.button__icon {
|
|
12
12
|
display: inline-block;
|
|
13
13
|
fill: none;
|
|
14
14
|
height: 1.2em;
|
|
@@ -60,82 +60,4 @@
|
|
|
60
60
|
list-style: none;
|
|
61
61
|
padding: 0;
|
|
62
62
|
}
|
|
63
|
-
.registered-addresses-item {
|
|
64
|
-
@include shadowed-box($padding: 1rem);
|
|
65
|
-
align-items: center;
|
|
66
|
-
display: flex;
|
|
67
|
-
|
|
68
|
-
&:not(:last-child) {
|
|
69
|
-
margin-bottom: 1rem;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
@include media-breakpoint-down(md) {
|
|
73
|
-
align-items: start;
|
|
74
|
-
flex-direction: column;
|
|
75
|
-
|
|
76
|
-
&__actions {
|
|
77
|
-
align-self: end;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
& > * {
|
|
82
|
-
margin: 0;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
.address-main-indicator {
|
|
86
|
-
$tick-size: 1.125rem;
|
|
87
|
-
background-color: r-theme-val(form, input-unchecked-background);
|
|
88
|
-
border-radius: 100vw;
|
|
89
|
-
border: 2px solid r-theme-val(form, input-unchecked-border);
|
|
90
|
-
box-sizing: border-box;
|
|
91
|
-
display: block;
|
|
92
|
-
height: $tick-size;
|
|
93
|
-
position: relative;
|
|
94
|
-
width: $tick-size;
|
|
95
|
-
|
|
96
|
-
&:before {
|
|
97
|
-
background: transparent;
|
|
98
|
-
border-radius: 100vw;
|
|
99
|
-
content: '';
|
|
100
|
-
display: block;
|
|
101
|
-
height: 60%;
|
|
102
|
-
left: 50%;
|
|
103
|
-
position: absolute;
|
|
104
|
-
top: 50%;
|
|
105
|
-
transform-origin: center center;
|
|
106
|
-
transform: translate(-50%, -50%) scale(calc(100 / 60));
|
|
107
|
-
transition: transform 400ms $r-ease-out;
|
|
108
|
-
width: 60%;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
&--is-main {
|
|
112
|
-
background-color: r-theme-val(form, input-checked-background);
|
|
113
|
-
border-color: r-theme-val(form, input-checked-border);
|
|
114
|
-
color: r-theme-val(form, input-checked-color);
|
|
115
|
-
|
|
116
|
-
&:before {
|
|
117
|
-
background-color: r-theme-val(form, input-checked-color);
|
|
118
|
-
transform: translate(-50%, -50%) scale(1);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
&__title {
|
|
124
|
-
font-size: 0.8rem;
|
|
125
|
-
margin-right: 0.8rem;
|
|
126
|
-
}
|
|
127
|
-
&__address {
|
|
128
|
-
flex: 1;
|
|
129
|
-
line-height: 1em;
|
|
130
|
-
}
|
|
131
|
-
&__actions {
|
|
132
|
-
& > .button {
|
|
133
|
-
min-width: 100px;
|
|
134
|
-
|
|
135
|
-
&:disabled {
|
|
136
|
-
visibility: hidden;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
63
|
}
|