feat: SkillsBuilder career interests selection
This commit is contained in:
@@ -2,7 +2,7 @@ import {
|
||||
SET_GOAL,
|
||||
SET_CURRENT_JOB_TITLE,
|
||||
ADD_CAREER_INTEREST,
|
||||
REMOVE_CAREER_INTEREEST,
|
||||
REMOVE_CAREER_INTEREST,
|
||||
} from './constants';
|
||||
|
||||
export const setGoal = (payload) => ({
|
||||
@@ -21,6 +21,6 @@ export const addCareerInterest = (payload) => ({
|
||||
});
|
||||
|
||||
export const removeCareerInterest = (payload) => ({
|
||||
type: REMOVE_CAREER_INTEREEST,
|
||||
type: REMOVE_CAREER_INTEREST,
|
||||
payload,
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
export const SET_GOAL = 'SET_GOAL';
|
||||
export const SET_CURRENT_JOB_TITLE = 'SET_CURRENT_JOB_TITLE';
|
||||
export const ADD_CAREER_INTEREST = 'ADD_CAREER_INTEREST';
|
||||
export const REMOVE_CAREER_INTEREEST = 'REMOVE_CAREER_INTEREEST';
|
||||
export const REMOVE_CAREER_INTEREST = 'REMOVE_CAREER_INTEREST';
|
||||
|
||||
// Stepper keys
|
||||
export const STEP1 = 'select-your-preferences';
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
SET_GOAL,
|
||||
SET_CURRENT_JOB_TITLE,
|
||||
ADD_CAREER_INTEREST,
|
||||
REMOVE_CAREER_INTEREEST,
|
||||
REMOVE_CAREER_INTEREST,
|
||||
} from './constants';
|
||||
|
||||
export function skillsReducer(state, action) {
|
||||
@@ -22,7 +22,7 @@ export function skillsReducer(state, action) {
|
||||
...state,
|
||||
careerInterests: [...state.careerInterests, action.payload],
|
||||
};
|
||||
case REMOVE_CAREER_INTEREEST:
|
||||
case REMOVE_CAREER_INTEREST:
|
||||
return {
|
||||
...state,
|
||||
careerInterests: state.careerInterests.filter(interest => interest !== action.payload),
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
SET_GOAL,
|
||||
SET_CURRENT_JOB_TITLE,
|
||||
ADD_CAREER_INTEREST,
|
||||
REMOVE_CAREER_INTEREEST,
|
||||
REMOVE_CAREER_INTEREST,
|
||||
} from '../constants';
|
||||
|
||||
describe('skillsReducer', () => {
|
||||
@@ -48,7 +48,7 @@ describe('skillsReducer', () => {
|
||||
};
|
||||
const returnedState = skillsReducer(
|
||||
testStateWithInterest,
|
||||
{ type: REMOVE_CAREER_INTEREEST, payload: newCareerInterestPayload },
|
||||
{ type: REMOVE_CAREER_INTEREST, payload: newCareerInterestPayload },
|
||||
);
|
||||
const finalState = {
|
||||
...testStateWithInterest,
|
||||
|
||||
@@ -33,7 +33,7 @@ const SkillsBuilderModal = () => {
|
||||
<ModalDialog
|
||||
title="Skills Builder"
|
||||
size="fullscreen"
|
||||
className="skills-builder-modal"
|
||||
className="skills-builder-modal bg-light-200"
|
||||
isOpen
|
||||
onClose={onCloseHandle}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
IconButton, Icon,
|
||||
} from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Close } from '@edx/paragon/icons';
|
||||
import { SkillsBuilderContext } from '../../skills-builder-context';
|
||||
import { removeCareerInterest } from '../../data/actions';
|
||||
import messages from './messages';
|
||||
|
||||
const CareerInterestCard = ({ interest }) => {
|
||||
const intl = useIntl();
|
||||
const { dispatch } = useContext(SkillsBuilderContext);
|
||||
|
||||
return (
|
||||
<div className="d-flex justify-content-between align-items-center pb-2 pr-2 pl-4 rounded shadow-sm">
|
||||
<p className="pt-4">
|
||||
{interest}
|
||||
</p>
|
||||
<IconButton
|
||||
iconAs={Icon}
|
||||
src={Close}
|
||||
alt={`${intl.formatMessage(messages.removeCareerInterestButtonAltText)} ${interest}`}
|
||||
onClick={() => dispatch(removeCareerInterest(interest))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CareerInterestCard.propTypes = {
|
||||
interest: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CareerInterestCard;
|
||||
@@ -1,16 +1,46 @@
|
||||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Stack, Row, Col } from '@edx/paragon';
|
||||
import { InstantSearch } from 'react-instantsearch-hooks-web';
|
||||
import JobTitleInstantSearch from './JobTitleInstantSearch';
|
||||
import CareerInterestCard from './CareerInterestCard';
|
||||
import { addCareerInterest } from '../../data/actions';
|
||||
import { SkillsBuilderContext } from '../../skills-builder-context';
|
||||
import messages from './messages';
|
||||
|
||||
const CareerInterestSelect = () => (
|
||||
<div>
|
||||
<h4>
|
||||
<FormattedMessage {...messages.careerInterestPrompt} />
|
||||
</h4>
|
||||
<p>
|
||||
JobTitleAutosuggest component can be reused here
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
const CareerInterestSelect = () => {
|
||||
const { state, dispatch, algolia } = useContext(SkillsBuilderContext);
|
||||
const { careerInterests } = state;
|
||||
const { searchClient } = algolia;
|
||||
|
||||
const handleCareerInterestSelect = (value) => {
|
||||
// By checking for a value to exist, we avoid adding a null value to the careerInterests array
|
||||
// The 'onSelected' function is fired during every 'onChange' event
|
||||
// A null value was being passed to this function whenever the search box received input, resulting in empty cards
|
||||
if (value && careerInterests.length < 3) {
|
||||
dispatch(addCareerInterest(value));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={2}>
|
||||
<h4>
|
||||
<FormattedMessage {...messages.careerInterestPrompt} />
|
||||
</h4>
|
||||
<InstantSearch searchClient={searchClient} indexName={getConfig().ALGOLIA_JOBS_INDEX_NAME}>
|
||||
<JobTitleInstantSearch onSelected={handleCareerInterestSelect} />
|
||||
</InstantSearch>
|
||||
<Row>
|
||||
{careerInterests.map((interest, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Col key={index} xs={12} sm={4} className="mb-4">
|
||||
<CareerInterestCard interest={interest} />
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default CareerInterestSelect;
|
||||
|
||||
@@ -21,6 +21,7 @@ const GoalDropdown = () => {
|
||||
as="select"
|
||||
value={currentGoal}
|
||||
onChange={(e) => dispatch(setGoal(e.target.value))}
|
||||
data-testid="goal-select-dropdown"
|
||||
>
|
||||
<option value="">{intl.formatMessage(messages.selectLearningGoal)}</option>
|
||||
<option>{intl.formatMessage(messages.learningGoalStartCareer)}</option>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Form,
|
||||
@@ -13,9 +13,12 @@ const JobTitleInstantSearch = (props) => {
|
||||
|
||||
const handleAutosuggestChange = (value) => {
|
||||
setJobInput(value);
|
||||
refine(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refine(jobInput);
|
||||
}, [jobInput, refine]);
|
||||
|
||||
return (
|
||||
<Form.Autosuggest
|
||||
value={jobInput}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
Form, Stack,
|
||||
} from '@edx/paragon';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { InstantSearch } from 'react-instantsearch-hooks-web';
|
||||
import { setCurrentJobTitle } from '../../data/actions';
|
||||
@@ -14,6 +14,10 @@ const JobTitleSelect = () => {
|
||||
const { dispatch, algolia } = useContext(SkillsBuilderContext);
|
||||
const { searchClient } = algolia;
|
||||
|
||||
const handleCurrentJobTitleSelect = (value) => {
|
||||
dispatch(setCurrentJobTitle(value));
|
||||
};
|
||||
|
||||
// Below implementation sets the job title to "student" or "looking_for_work" — this overwrites any previous selection
|
||||
// This will need to be revisited when we decide what to do with this data
|
||||
const handleCheckboxChange = (e) => dispatch(setCurrentJobTitle(e.target.value));
|
||||
@@ -24,7 +28,7 @@ const JobTitleSelect = () => {
|
||||
<FormattedMessage {...messages.jobTitlePrompt} />
|
||||
</h4>
|
||||
<InstantSearch searchClient={searchClient} indexName={getConfig().ALGOLIA_JOBS_INDEX_NAME}>
|
||||
<JobTitleInstantSearch onSelected={(value) => dispatch(setCurrentJobTitle(value))} />
|
||||
<JobTitleInstantSearch onSelected={handleCurrentJobTitleSelect} />
|
||||
</InstantSearch>
|
||||
<Form.Group>
|
||||
<Form.CheckboxSet
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
} from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { SkillsBuilderContext } from '../../skills-builder-context';
|
||||
|
||||
import GoalSelect from './GoalSelect';
|
||||
import JobTitleSelect from './JobTitleSelect';
|
||||
import CareerInterestSelect from './CareerInterestSelect';
|
||||
@@ -15,21 +14,23 @@ const SelectPreferences = () => {
|
||||
const { currentGoal, currentJobTitle } = state;
|
||||
|
||||
return (
|
||||
<Stack gap={5}>
|
||||
<p className="lead">
|
||||
<>
|
||||
<p className="lead mb-5">
|
||||
<FormattedMessage {...messages.skillsBuilderDescription} />
|
||||
</p>
|
||||
<Stack gap={4}>
|
||||
|
||||
<GoalSelect />
|
||||
<GoalSelect />
|
||||
|
||||
{currentGoal && (
|
||||
<JobTitleSelect />
|
||||
)}
|
||||
{currentGoal && (
|
||||
<JobTitleSelect />
|
||||
)}
|
||||
|
||||
{currentJobTitle && (
|
||||
<CareerInterestSelect />
|
||||
)}
|
||||
</Stack>
|
||||
{currentJobTitle && (
|
||||
<CareerInterestSelect />
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -61,6 +61,10 @@ const messages = defineMessages({
|
||||
defaultMessage: 'What careers are you interested in?',
|
||||
description: 'Prompts the user to select careers they are interested in pursuing.',
|
||||
},
|
||||
removeCareerInterestButtonAltText: {
|
||||
id: 'career.interest.remove.button.alt.text',
|
||||
defaultMessage: 'Remove career interest: ',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -71,14 +71,27 @@ describe('skills-builder', () => {
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(screen.getByText('Next, search and select your current job title')).toBeTruthy();
|
||||
const expectedGoal = {
|
||||
payload: 'I want to advance my career',
|
||||
type: 'SET_GOAL',
|
||||
};
|
||||
const expectedJobTitle = {
|
||||
payload: 'student',
|
||||
type: 'SET_CURRENT_JOB_TITLE',
|
||||
};
|
||||
|
||||
const goalSelect = screen.getByTestId('goal-select-dropdown');
|
||||
fireEvent.change(goalSelect, { target: { value: 'I want to advance my career' } });
|
||||
|
||||
const checkbox = screen.getByRole('checkbox', { name: 'I\'m a student' });
|
||||
fireEvent.click(checkbox);
|
||||
expect(dispatchMock).toHaveBeenCalled();
|
||||
|
||||
expect(screen.getByText('Next, search and select your current job title')).toBeTruthy();
|
||||
expect(dispatchMock).toHaveBeenCalledWith(expectedGoal);
|
||||
expect(dispatchMock).toHaveBeenCalledWith(expectedJobTitle);
|
||||
});
|
||||
|
||||
it('should render the third prompt if a goal is selected', () => {
|
||||
it('should render the third prompt if a current job title is selected', () => {
|
||||
render(
|
||||
SkillsBuilderWrapperWithContext(
|
||||
{
|
||||
@@ -93,4 +106,47 @@ describe('skills-builder', () => {
|
||||
);
|
||||
expect(screen.getByText('What careers are you interested in?')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render a <CareerInterestCard> for each career interest', () => {
|
||||
render(
|
||||
SkillsBuilderWrapperWithContext(
|
||||
{
|
||||
...contextValue,
|
||||
state: {
|
||||
...contextValue.state,
|
||||
currentGoal: 'I want to start my career',
|
||||
currentJobTitle: 'Goblin Lackey',
|
||||
careerInterests: ['Prospector', 'Mirror Breaker', 'Bombardment'],
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(screen.getByText('Prospector')).toBeTruthy();
|
||||
expect(screen.getByText('Mirror Breaker')).toBeTruthy();
|
||||
expect(screen.getByText('Bombardment')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should remove a <CareerInterestCard> when the corresponding close button is selected', () => {
|
||||
render(
|
||||
SkillsBuilderWrapperWithContext(
|
||||
{
|
||||
...contextValue,
|
||||
state: {
|
||||
...contextValue.state,
|
||||
currentGoal: 'I want to start my career',
|
||||
currentJobTitle: 'Goblin Lackey',
|
||||
careerInterests: ['Prospector', 'Mirror Breaker', 'Bombardment'],
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
const expected = {
|
||||
payload: 'Prospector',
|
||||
type: 'REMOVE_CAREER_INTEREST',
|
||||
};
|
||||
|
||||
fireEvent.click(screen.getByLabelText('Remove career interest: Prospector'));
|
||||
expect(dispatchMock).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user