From ddff5364ce463bbc34499b11f5422ffef671613c Mon Sep 17 00:00:00 2001 From: Maxwell Frank Date: Mon, 27 Feb 2023 20:03:25 +0000 Subject: [PATCH] feat: SkillsBuilder career interests selection --- src/skills-builder/data/actions.js | 4 +- src/skills-builder/data/constants.js | 2 +- src/skills-builder/data/reducer.js | 4 +- src/skills-builder/data/test/reducer.test.js | 4 +- .../SkillsBuilderModal.jsx | 2 +- .../select-preferences/CareerInterestCard.jsx | 35 +++++++++++ .../CareerInterestSelect.jsx | 52 ++++++++++++---- .../select-preferences/GoalSelect.jsx | 1 + .../JobTitleInstantSearch.jsx | 7 ++- .../select-preferences/JobTitleSelect.jsx | 8 ++- .../select-preferences/SelectPreferences.jsx | 23 +++---- .../select-preferences/messages.js | 4 ++ .../test/SkillsBuilder.test.jsx | 62 ++++++++++++++++++- 13 files changed, 171 insertions(+), 37 deletions(-) create mode 100644 src/skills-builder/skills-builder-modal/select-preferences/CareerInterestCard.jsx diff --git a/src/skills-builder/data/actions.js b/src/skills-builder/data/actions.js index b4df8c9..70839e5 100644 --- a/src/skills-builder/data/actions.js +++ b/src/skills-builder/data/actions.js @@ -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, }); diff --git a/src/skills-builder/data/constants.js b/src/skills-builder/data/constants.js index 65847df..9bfe1ff 100644 --- a/src/skills-builder/data/constants.js +++ b/src/skills-builder/data/constants.js @@ -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'; diff --git a/src/skills-builder/data/reducer.js b/src/skills-builder/data/reducer.js index 6f6af67..e8876e0 100644 --- a/src/skills-builder/data/reducer.js +++ b/src/skills-builder/data/reducer.js @@ -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), diff --git a/src/skills-builder/data/test/reducer.test.js b/src/skills-builder/data/test/reducer.test.js index 0cfbb3c..07b11dd 100644 --- a/src/skills-builder/data/test/reducer.test.js +++ b/src/skills-builder/data/test/reducer.test.js @@ -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, diff --git a/src/skills-builder/skills-builder-modal/SkillsBuilderModal.jsx b/src/skills-builder/skills-builder-modal/SkillsBuilderModal.jsx index 470618e..fb7df32 100644 --- a/src/skills-builder/skills-builder-modal/SkillsBuilderModal.jsx +++ b/src/skills-builder/skills-builder-modal/SkillsBuilderModal.jsx @@ -33,7 +33,7 @@ const SkillsBuilderModal = () => { diff --git a/src/skills-builder/skills-builder-modal/select-preferences/CareerInterestCard.jsx b/src/skills-builder/skills-builder-modal/select-preferences/CareerInterestCard.jsx new file mode 100644 index 0000000..62b7a37 --- /dev/null +++ b/src/skills-builder/skills-builder-modal/select-preferences/CareerInterestCard.jsx @@ -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 ( +
+

+ {interest} +

+ dispatch(removeCareerInterest(interest))} + /> +
+ ); +}; + +CareerInterestCard.propTypes = { + interest: PropTypes.string.isRequired, +}; + +export default CareerInterestCard; diff --git a/src/skills-builder/skills-builder-modal/select-preferences/CareerInterestSelect.jsx b/src/skills-builder/skills-builder-modal/select-preferences/CareerInterestSelect.jsx index 68f314f..892fb37 100644 --- a/src/skills-builder/skills-builder-modal/select-preferences/CareerInterestSelect.jsx +++ b/src/skills-builder/skills-builder-modal/select-preferences/CareerInterestSelect.jsx @@ -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 = () => ( -
-

- -

-

- JobTitleAutosuggest component can be reused here -

-
-); +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 ( + +

+ +

+ + + + + {careerInterests.map((interest, index) => ( + // eslint-disable-next-line react/no-array-index-key + + + + ))} + +
+ ); +}; export default CareerInterestSelect; diff --git a/src/skills-builder/skills-builder-modal/select-preferences/GoalSelect.jsx b/src/skills-builder/skills-builder-modal/select-preferences/GoalSelect.jsx index 2c2f5f6..30b1dc0 100644 --- a/src/skills-builder/skills-builder-modal/select-preferences/GoalSelect.jsx +++ b/src/skills-builder/skills-builder-modal/select-preferences/GoalSelect.jsx @@ -21,6 +21,7 @@ const GoalDropdown = () => { as="select" value={currentGoal} onChange={(e) => dispatch(setGoal(e.target.value))} + data-testid="goal-select-dropdown" > diff --git a/src/skills-builder/skills-builder-modal/select-preferences/JobTitleInstantSearch.jsx b/src/skills-builder/skills-builder-modal/select-preferences/JobTitleInstantSearch.jsx index e147d5d..5b60665 100644 --- a/src/skills-builder/skills-builder-modal/select-preferences/JobTitleInstantSearch.jsx +++ b/src/skills-builder/skills-builder-modal/select-preferences/JobTitleInstantSearch.jsx @@ -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 ( { 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 = () => { - dispatch(setCurrentJobTitle(value))} /> + { const { currentGoal, currentJobTitle } = state; return ( - -

+ <> +

+ - + - {currentGoal && ( - - )} + {currentGoal && ( + + )} - {currentJobTitle && ( - - )} - + {currentJobTitle && ( + + )} +
+ ); }; diff --git a/src/skills-builder/skills-builder-modal/select-preferences/messages.js b/src/skills-builder/skills-builder-modal/select-preferences/messages.js index 76e124a..53e1551 100644 --- a/src/skills-builder/skills-builder-modal/select-preferences/messages.js +++ b/src/skills-builder/skills-builder-modal/select-preferences/messages.js @@ -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; diff --git a/src/skills-builder/test/SkillsBuilder.test.jsx b/src/skills-builder/test/SkillsBuilder.test.jsx index 0925b15..b9b8b7b 100644 --- a/src/skills-builder/test/SkillsBuilder.test.jsx +++ b/src/skills-builder/test/SkillsBuilder.test.jsx @@ -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 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 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); + }); });