Merge pull request #971 from openedx/attiya/VAN-1790
feat: add work experience field
This commit is contained in:
@@ -45,6 +45,7 @@ import {
|
||||
GENDER_OPTIONS,
|
||||
COUNTRY_WITH_STATES,
|
||||
COPPA_COMPLIANCE_YEAR,
|
||||
WORK_EXPERIENCE_OPTIONS,
|
||||
getStatesList,
|
||||
} from './data/constants';
|
||||
import { fetchSiteLanguages } from './site-language';
|
||||
@@ -142,6 +143,10 @@ class AccountSettingsPage extends React.Component {
|
||||
value: key,
|
||||
label: this.props.intl.formatMessage(messages[`account.settings.field.gender.options.${key || 'empty'}`]),
|
||||
})),
|
||||
workExperienceOptions: WORK_EXPERIENCE_OPTIONS.map(key => ({
|
||||
value: key,
|
||||
label: key === '' ? this.props.intl.formatMessage(messages['account.settings.field.work.experience.options.empty']) : key,
|
||||
})),
|
||||
}));
|
||||
|
||||
handleEditableFieldChange = (name, value) => {
|
||||
@@ -149,7 +154,17 @@ class AccountSettingsPage extends React.Component {
|
||||
};
|
||||
|
||||
handleSubmit = (formId, values) => {
|
||||
this.props.saveSettings(formId, values);
|
||||
const { formValues } = this.props;
|
||||
let extendedProfileObject = {};
|
||||
|
||||
if ('extended_profile' in formValues && formValues.extended_profile.some((field) => field.field_name === formId)) {
|
||||
extendedProfileObject = {
|
||||
extended_profile: formValues.extended_profile.map(field => (field.field_name === formId
|
||||
? { ...field, field_value: values }
|
||||
: field)),
|
||||
};
|
||||
}
|
||||
this.props.saveSettings(formId, values, extendedProfileObject);
|
||||
};
|
||||
|
||||
handleSubmitProfileName = (formId, values) => {
|
||||
@@ -469,13 +484,15 @@ class AccountSettingsPage extends React.Component {
|
||||
yearOfBirthOptions,
|
||||
educationLevelOptions,
|
||||
genderOptions,
|
||||
workExperienceOptions,
|
||||
} = this.getLocalizedOptions(this.context.locale, this.props.formValues.country);
|
||||
|
||||
// Show State field only if the country is US (could include Canada later)
|
||||
const showState = this.props.formValues.country === COUNTRY_WITH_STATES;
|
||||
|
||||
const { verifiedName } = this.props;
|
||||
|
||||
const hasWorkExperience = !!this.props.formValues?.extended_profile?.find(field => field.field_name === 'work_experience');
|
||||
|
||||
const timeZoneOptions = this.getLocalizedTimeZoneOptions(
|
||||
this.props.timeZoneOptions,
|
||||
this.props.countryTimeZoneOptions,
|
||||
@@ -679,6 +696,18 @@ class AccountSettingsPage extends React.Component {
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.gender.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
{hasWorkExperience
|
||||
&& (
|
||||
<EditableSelectField
|
||||
name="work_experience"
|
||||
type="select"
|
||||
value={this.props.formValues?.extended_profile?.find(field => field.field_name === 'work_experience')?.field_value}
|
||||
options={workExperienceOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.work.experience'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.work.experience.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
)}
|
||||
<EditableSelectField
|
||||
name="language_proficiencies"
|
||||
type="select"
|
||||
@@ -850,6 +879,7 @@ AccountSettingsPage.propTypes = {
|
||||
country: PropTypes.string,
|
||||
level_of_education: PropTypes.string,
|
||||
gender: PropTypes.string,
|
||||
extended_profile: PropTypes.string,
|
||||
language_proficiencies: PropTypes.string,
|
||||
pending_name_change: PropTypes.string,
|
||||
phone_number: PropTypes.string,
|
||||
|
||||
@@ -570,6 +570,21 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Notifications',
|
||||
description: 'Label for Notifications',
|
||||
},
|
||||
'account.settings.field.work.experience': {
|
||||
id: 'account.settings.work.experience',
|
||||
defaultMessage: 'Work Experience',
|
||||
description: 'Label for account settings Work experience field.',
|
||||
},
|
||||
'account.settings.field.work.experience.empty': {
|
||||
id: 'account.settings.field.work.experience.empty',
|
||||
defaultMessage: 'Add work experience',
|
||||
description: 'Placeholder for empty account settings work experience field.',
|
||||
},
|
||||
'account.settings.field.work.experience.options.empty': {
|
||||
id: 'account.settings.field.work.experience.options.empty',
|
||||
defaultMessage: 'Select work experience',
|
||||
description: 'Placeholder for the work experience levels dropdown.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -151,7 +151,7 @@ describe('NameChange', () => {
|
||||
const submitButton = screen.getByText('Choose name');
|
||||
fireEvent.click(submitButton);
|
||||
expect(mockDispatch).toHaveBeenCalledWith({
|
||||
payload: { formId, commitValues: false },
|
||||
payload: { formId, commitValues: false, extendedProfile: {} },
|
||||
type: 'ACCOUNT_SETTINGS__SAVE_SETTINGS',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,9 +77,9 @@ export const beginNameChange = (formId) => ({
|
||||
});
|
||||
// SAVE SETTINGS ACTIONS
|
||||
|
||||
export const saveSettings = (formId, commitValues) => ({
|
||||
export const saveSettings = (formId, commitValues, extendedProfile = {}) => ({
|
||||
type: SAVE_SETTINGS.BASE,
|
||||
payload: { formId, commitValues },
|
||||
payload: { formId, commitValues, extendedProfile },
|
||||
});
|
||||
|
||||
export const saveSettingsBegin = () => ({
|
||||
|
||||
@@ -34,6 +34,21 @@ export const GENDER_OPTIONS = [
|
||||
'm',
|
||||
'o',
|
||||
];
|
||||
export const WORK_EXPERIENCE_OPTIONS = [
|
||||
'',
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3',
|
||||
'4',
|
||||
'5',
|
||||
'6',
|
||||
'7',
|
||||
'8',
|
||||
'9',
|
||||
'10+',
|
||||
|
||||
];
|
||||
|
||||
export const COUNTRY_WITH_STATES = 'US';
|
||||
|
||||
|
||||
@@ -83,8 +83,8 @@ export function* handleSaveSettings(action) {
|
||||
yield put(saveSettingsBegin());
|
||||
|
||||
const { username, userId } = getAuthenticatedUser();
|
||||
const { commitValues, formId } = action.payload;
|
||||
const commitData = { [formId]: commitValues };
|
||||
const { commitValues, formId, extendedProfile } = action.payload;
|
||||
const commitData = Object.keys(extendedProfile).length > 0 ? extendedProfile : { [formId]: commitValues };
|
||||
let savedValues = null;
|
||||
if (formId === 'siteLanguage') {
|
||||
const previousSiteLanguage = getLocale();
|
||||
|
||||
@@ -156,7 +156,7 @@ function chooseFormValue(draft, committed) {
|
||||
return draft !== undefined ? draft : committed;
|
||||
}
|
||||
|
||||
const formValuesSelector = createSelector(
|
||||
export const formValuesSelector = createSelector(
|
||||
valuesSelector,
|
||||
draftsSelector,
|
||||
(values, drafts) => {
|
||||
@@ -164,6 +164,20 @@ const formValuesSelector = createSelector(
|
||||
Object.entries(values).forEach(([name, value]) => {
|
||||
if (typeof value === 'boolean') {
|
||||
formValues[name] = chooseFormValue(drafts[name], value);
|
||||
} else if (typeof value === 'object' && name === 'extended_profile' && value !== null) {
|
||||
const extendedProfile = value.slice();
|
||||
const draftsKeys = Object.keys(drafts);
|
||||
|
||||
if (draftsKeys.length !== 0) {
|
||||
const draftFieldName = draftsKeys[0];
|
||||
const index = extendedProfile.findIndex((profile) => profile.field_name === draftFieldName);
|
||||
|
||||
if (index !== -1) {
|
||||
extendedProfile[index] = { field_name: draftFieldName, field_value: drafts[draftFieldName] };
|
||||
}
|
||||
}
|
||||
|
||||
formValues.extended_profile = [...extendedProfile];
|
||||
} else {
|
||||
formValues[name] = chooseFormValue(drafts[name], value) || '';
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { profileDataManagerSelector } from './selectors';
|
||||
import { profileDataManagerSelector, formValuesSelector } from './selectors';
|
||||
|
||||
const testValue = 'test VALUE';
|
||||
|
||||
@@ -13,4 +13,60 @@ describe('profileDataManagerSelector', () => {
|
||||
|
||||
expect(result).toEqual(state.accountSettings.profileDataManager);
|
||||
});
|
||||
|
||||
it('should correctly select form values', () => {
|
||||
const state = {
|
||||
accountSettings: {
|
||||
values: {
|
||||
name: 'John Doe',
|
||||
age: 25,
|
||||
},
|
||||
drafts: {
|
||||
age: 26,
|
||||
|
||||
},
|
||||
verifiedNameHistory: 'test',
|
||||
confirmationValues: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = formValuesSelector(state);
|
||||
|
||||
const expected = {
|
||||
name: 'John Doe',
|
||||
age: 26,
|
||||
verified_name: '',
|
||||
useVerifiedNameForCerts: false,
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should correctly select form values with extended_profile', () => {
|
||||
// Mock data with extended_profile field in both values and drafts
|
||||
const state = {
|
||||
accountSettings: {
|
||||
values: {
|
||||
extended_profile: [
|
||||
{ field_name: 'test_field', field_value: '5' },
|
||||
],
|
||||
},
|
||||
drafts: { test_field: '6' },
|
||||
verifiedNameHistory: 'test',
|
||||
confirmationValues: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = formValuesSelector(state);
|
||||
|
||||
const expected = {
|
||||
verified_name: '',
|
||||
useVerifiedNameForCerts: false,
|
||||
extended_profile: [ // Draft value should override the committed value
|
||||
{ field_name: 'test_field', field_value: '6' }, // Value from the committed values
|
||||
],
|
||||
};
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
101
src/account-settings/test/AccountSettingsPage.test.jsx
Normal file
101
src/account-settings/test/AccountSettingsPage.test.jsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import {
|
||||
render, screen, fireEvent,
|
||||
} from '@testing-library/react';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import AccountSettingsPage from '../AccountSettingsPage';
|
||||
import mockData from './mockData';
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
sendTrackingLogEvent: jest.fn(),
|
||||
getCountryList: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useDispatch: () => mockDispatch,
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
|
||||
const IntlAccountSettingsPage = injectIntl(AccountSettingsPage);
|
||||
|
||||
const middlewares = [thunk];
|
||||
const mockStore = configureStore(middlewares);
|
||||
|
||||
describe('AccountSettingsPage', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
const appContext = { locale: 'en', authenticatedUser: { userId: 3, roles: [] } };
|
||||
const reduxWrapper = children => (
|
||||
<AppContext.Provider value={appContext}>
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>
|
||||
{children}
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore(mockData);
|
||||
props = {
|
||||
loaded: true,
|
||||
siteLanguage: {},
|
||||
formValues: {
|
||||
username: 'test_username',
|
||||
accomplishments_shared: false,
|
||||
name: 'test_name',
|
||||
email: 'test_email@test.com',
|
||||
id: 534,
|
||||
extended_profile: [
|
||||
{
|
||||
field_name: 'work_experience',
|
||||
field_value: '',
|
||||
},
|
||||
],
|
||||
|
||||
},
|
||||
fetchSettings: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
it('renders AccountSettingsPage correctly with editing enabled', async () => {
|
||||
const { getByText, rerender, getByLabelText } = render(reduxWrapper(<IntlAccountSettingsPage {...props} />));
|
||||
|
||||
const workExperienceText = getByText('Work Experience');
|
||||
const workExperienceEditButton = workExperienceText.parentElement.querySelector('button');
|
||||
|
||||
expect(workExperienceEditButton).toBeInTheDocument();
|
||||
|
||||
store = mockStore({
|
||||
...mockData,
|
||||
accountSettings: {
|
||||
...mockData.accountSettings,
|
||||
openFormId: 'work_experience',
|
||||
},
|
||||
});
|
||||
rerender(reduxWrapper(<IntlAccountSettingsPage {...props} />));
|
||||
|
||||
const submitButton = screen.getByText('Save');
|
||||
expect(submitButton).toBeInTheDocument();
|
||||
|
||||
const workExperienceSelect = getByLabelText('Work Experience');
|
||||
|
||||
// Use fireEvent.change to simulate changing the selected value
|
||||
fireEvent.change(workExperienceSelect, { target: { value: '4' } });
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
});
|
||||
});
|
||||
112
src/account-settings/test/mockData.js
Normal file
112
src/account-settings/test/mockData.js
Normal file
@@ -0,0 +1,112 @@
|
||||
const mockData = {
|
||||
accountSettings: {
|
||||
loading: false,
|
||||
loaded: true,
|
||||
loadingError: null,
|
||||
data: null,
|
||||
values: {
|
||||
username: 'test_username',
|
||||
country: 'AD',
|
||||
accomplishments_shared: false,
|
||||
name: 'test_name',
|
||||
email: 'test_email@test.com',
|
||||
id: 533,
|
||||
verified_name: null,
|
||||
extended_profile: [
|
||||
{
|
||||
field_name: 'work_experience',
|
||||
field_value: '',
|
||||
},
|
||||
],
|
||||
gender: null,
|
||||
|
||||
'pref-lang': 'en',
|
||||
shouldDisplayDemographicsSection: false,
|
||||
demographicsOptions: false,
|
||||
},
|
||||
errors: {},
|
||||
confirmationValues: {},
|
||||
drafts: {},
|
||||
saveState: null,
|
||||
timeZones: [
|
||||
{
|
||||
time_zone: 'Africa/Abidjan',
|
||||
description: 'Africa/Abidjan (GMT, UTC+0000)',
|
||||
},
|
||||
],
|
||||
countryTimeZones: [
|
||||
{
|
||||
time_zone: 'Europe/Andorra',
|
||||
description: 'Europe/Andorra (CET, UTC+0100)',
|
||||
},
|
||||
],
|
||||
previousSiteLanguage: null,
|
||||
deleteAccount: {
|
||||
status: null,
|
||||
errorType: null,
|
||||
},
|
||||
siteLanguage: {
|
||||
loading: false,
|
||||
loaded: true,
|
||||
loadingError: null,
|
||||
siteLanguageList: [
|
||||
{
|
||||
code: 'en',
|
||||
name: 'English',
|
||||
released: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
resetPassword: {
|
||||
status: null,
|
||||
},
|
||||
nameChange: {
|
||||
saveState: null,
|
||||
errors: {},
|
||||
},
|
||||
thirdPartyAuth: {
|
||||
providers: [
|
||||
{
|
||||
id: 'oa2-google-oauth2',
|
||||
name: 'Google',
|
||||
connected: false,
|
||||
accepts_logins: true,
|
||||
connectUrl: 'http://localhost:18000/auth/login/google-oauth2/?auth_entry=account_settings&next=%2Faccount%2Fsettings',
|
||||
disconnectUrl: 'http://localhost:18000/auth/disconnect/google-oauth2/?',
|
||||
},
|
||||
],
|
||||
disconnectionStatuses: {},
|
||||
errors: {},
|
||||
},
|
||||
verifiedName: null,
|
||||
mostRecentVerifiedName: {},
|
||||
verifiedNameHistory: {
|
||||
use_verified_name_for_certs: false,
|
||||
results: [],
|
||||
},
|
||||
profileDataManager: null,
|
||||
},
|
||||
notificationPreferences: {
|
||||
showPreferences: false,
|
||||
courses: {
|
||||
status: 'success',
|
||||
courses: [],
|
||||
pagination: {
|
||||
count: 0,
|
||||
currentPage: 1,
|
||||
hasMore: false,
|
||||
totalPages: 1,
|
||||
},
|
||||
},
|
||||
preferences: {
|
||||
status: 'idle',
|
||||
updatePreferenceStatus: 'idle',
|
||||
selectedCourse: null,
|
||||
preferences: [],
|
||||
apps: [],
|
||||
nonEditable: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default mockData;
|
||||
Reference in New Issue
Block a user