Skip to content

Commit

Permalink
fix: productize clickable skills (#68)
Browse files Browse the repository at this point in the history
  • Loading branch information
cdeery authored Oct 5, 2023
1 parent 4db5869 commit 919838c
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { SkillsBuilderContext } from '../../skills-builder-context';
import { VisibilityFlagsContext } from '../../visibility-flags-context';
import { extractProductKeys } from '../../utils/extractProductKeys';

const RecommendationStack = ({ selectedRecommendations, productTypeNames }) => {
const RecommendationStack = ({ selectedRecommendations, productTypeNames, skillFilter }) => {
const { state, dispatch } = useContext(SkillsBuilderContext);
const { expandedList } = state;
const { state: visibilityFlagsState } = useContext(VisibilityFlagsContext);
Expand Down Expand Up @@ -90,10 +90,21 @@ const RecommendationStack = ({ selectedRecommendations, productTypeNames }) => {
});
};

const filterBySkill = (currentRecommendations) => {
if (skillFilter) {
const filteredRecommendations = currentRecommendations.filter((recommendation) => {
const filteredSkills = recommendation.skills.filter((currSkill) => currSkill.skill === skillFilter);
return filteredSkills.length > 0;
});
return filteredRecommendations;
}
return currentRecommendations;
};

return (
productTypeNames.map(productTypeName => {
// the recommendations object has a key for each productTypeName
const productTypeRecommendations = recommendations[productTypeName];
const productTypeRecommendations = filterBySkill(recommendations[productTypeName]);
const numberResults = productTypeRecommendations?.length;
const isExpanded = expandedList.includes(productTypeName);

Expand All @@ -104,7 +115,7 @@ const RecommendationStack = ({ selectedRecommendations, productTypeNames }) => {
<Stack gap={2.5} key={productTypeName}>
<ProductTypeBanner
productTypeName={productTypeName}
jobName={jobName}
jobName={skillFilter || jobName}
numberResults={numberResults}
handleShowAllButtonClick={handleShowAllButtonClick}
isExpanded={isExpanded}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,109 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import {
Chip, Card,
Chip, Card, Button,
} from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { VisibilityFlagsContext } from '../../visibility-flags-context';
import messages from './messages';

const RelatedSkillsSingleBoxSet = ({ jobSkillsList }) => {
const RelatedSkillsSingleBoxSet = ({
jobSkillsList, handleSelectSkill, matchedSkills, selectedSkill,
}) => {
const { formatMessage } = useIntl();
const { state: visibilityFlagsState } = useContext(VisibilityFlagsContext);
const { showSkillsBox, showSkillsList } = visibilityFlagsState;
const {
showSkillsBox,
showSkillsList,
sortSkillsByUniquePostings,
filterSkillsWithResults,
showAllSkills,
isClickableSkills,
isClickableSkillsDevMode,
} = visibilityFlagsState;

const { name, skills } = jobSkillsList[0];

// just a no-op for now.
// eslint-disable-next-line no-unused-vars
const handleSkillClick = (skill) => {
// console.log(skill.name);
// Toggle selection if already selected
if (skill.name === selectedSkill) {
handleSelectSkill('');
} else {
handleSelectSkill(skill.name);
}
// TODO send segment event
};

const renderTopFiveSkills = () => {
const topFiveSkills = skills.sort((a, b) => b.significance - a.significance).slice(0, 5);
// Display the skills as buttons that will set the display to
// related products for that skill
const renderSkillButtons = (displayedSkills) => (
displayedSkills.map(skill => {
const text = isClickableSkillsDevMode
? `${skill.name} (${skill.significance}) [${skill.unique_postings}]`
: skill.name;
return (
<Button
onClick={() => handleSkillClick(skill)}
// Change the appearance of the skillsButton depending on its state.
// This is not very friendly to screen readers, but it is only for experiments
variant={skill.name === selectedSkill ? 'primary' : 'light'}
size="sm"
className="mb-2 mr-2"
key={skill.external_id}
>
{text}
</Button>
);
})
);

// Display the skills as non-interactive chips
const renderSkillChips = (displayedSkills) => (
displayedSkills.map(skill => (
<span key={skill.external_id}>
<Chip
className="chip-max-width"
>
{skill.name}
</Chip>
</span>
))
);

// Get the intersection of skills associated with the job and the products
const getFilteredRelatedSkills = (() => {
// find the intersection of the skills lists. These are job skills with a
// related product entry
const filteredSkillNames = skills.filter((skill) => matchedSkills.indexOf(skill.name) !== -1);
return filteredSkillNames;
});

// Get the set of skills to display, depending on the settings
const getSortedAndFilteredSkills = (() => {
// use either the cross referenced skills, or all the job skills
const filteredSkills = filterSkillsWithResults ? getFilteredRelatedSkills() : skills;
// Sort the skills according to the settings
let skillsToDisplay = [];
if (sortSkillsByUniquePostings) {
skillsToDisplay = filteredSkills.sort((a, b) => b.unique_postings - a.unique_postings);
} else {
skillsToDisplay = filteredSkills.sort((a, b) => b.significance - a.significance);
}
// Either show 5 skills, or all of them
if (showAllSkills) {
return skillsToDisplay;
}
return skillsToDisplay.slice(0, 5);
});

const renderTopSkills = () => {
const displayedSkills = getSortedAndFilteredSkills();
return (
topFiveSkills.map(skill => (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<span key={skill.external_id} onClick={() => handleSkillClick(skill)}>
<Chip
className="chip-max-width"
>
{skill.name}
</Chip>
</span>
))
isClickableSkills ? (
renderSkillButtons(displayedSkills)
) : (
renderSkillChips(displayedSkills)
)
);
};

Expand All @@ -47,7 +118,7 @@ const RelatedSkillsSingleBoxSet = ({ jobSkillsList }) => {
&& (
<Card.Section>
<p className="heading-label x-small">{formatMessage(messages.relatedSkillsHeading)}</p>
{renderTopFiveSkills(skills)}
{renderTopSkills(skills)}
</Card.Section>
)}
</Card>
Expand All @@ -60,6 +131,9 @@ RelatedSkillsSingleBoxSet.propTypes = {
name: PropTypes.string.isRequired,
skills: PropTypes.arrayOf(PropTypes.shape({})),
})).isRequired,
handleSelectSkill: PropTypes.func.isRequired,
selectedSkill: PropTypes.string.isRequired,
matchedSkills: PropTypes.arrayOf(PropTypes.string).isRequired,
};

export default RelatedSkillsSingleBoxSet;
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const ViewResults = () => {
const [selectedRecommendations, setSelectedRecommendations] = useState({});
const [isLoading, setIsLoading] = useState(true);
const [fetchError, setFetchError] = useState(false);
// String that is set to only show products related to a specific skill
const [skillFilter, setSkillFilter] = useState('');

const productTypes = useRef(useProductTypes());
const { state: visibilityFlagsState } = useContext(VisibilityFlagsContext);
Expand All @@ -46,6 +48,7 @@ const ViewResults = () => {
setJobSkillsList(jobInfo);
setSelectedJobTitle(results[0]?.name);
setProductRecommendations(results);
setSkillFilter('');
sendTrackEvent('edx.skills_builder.recommendation.shown', {
app_name: 'skills_builder',
category: 'skills_builder',
Expand Down Expand Up @@ -98,6 +101,10 @@ const ViewResults = () => {
const { value } = e.target;
// check if the clicked target is different than the currently selected job title box
if (selectedJobTitle !== value) {
// clear the skill filter
if (skillFilter) {
setSkillFilter('');
}
// set the expanded list to an empty array so each grid will render un-expanded
dispatch(setExpandedList([]));
setSelectedJobTitle(value);
Expand Down Expand Up @@ -159,6 +166,9 @@ const ViewResults = () => {
) : (
<RelatedSkillsSingleBoxSet
jobSkillsList={jobSkillsList}
matchedSkills={selectedRecommendations?.matchedSkills}
handleSelectSkill={setSkillFilter}
selectedSkill={skillFilter}
/>
)
)}
Expand All @@ -168,6 +178,7 @@ const ViewResults = () => {
<RecommendationStack
selectedRecommendations={selectedRecommendations}
productTypeNames={productTypes.current}
skillFilter={skillFilter}
/>
)}
</Stack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export async function getRecommendations(jobSearchIndex, productSearchIndex, car
id: job.id,
name: job.name,
recommendations: {},
matchedSkills: [],
};

// get recommendations for each product type based on the skills for the current job
Expand All @@ -22,6 +23,23 @@ export async function getRecommendations(jobSearchIndex, productSearchIndex, car

// add a new key to the recommendations object and set the value to the response
data.recommendations[productType] = response;

// Get the list of skills for this job that intersect with the skills for the products returned
const productSkillsList = {};
response.forEach(product => {
const skills = product?.skills;
if (skills) {
skills.forEach((skill) => {
const jobSkill = skill?.skill;
if (jobSkill) {
// Upsert an entry with the skill name and update the number
// of times the skill is found in the results
productSkillsList[jobSkill] = (productSkillsList[jobSkill] || 0) + 1;
}
});
}
});
data.matchedSkills = formattedSkills.filter((skillName) => skillName in productSkillsList);
}));

return data;
Expand Down
10 changes: 10 additions & 0 deletions src/skills-builder/visibility-flags-context/data/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ export const DEFAULT_VISIBILITY_FLAGS = {
showSkillsList: true,
showSmallHeader: true,
showCategorizinator: false,
sortSkillsByUniquePostings: false,
filterSkillsWithResults: false,
showAllSkills: false,
isClickableSkills: false,
isClickableSkillsDevMode: false,
};

// Show a single question, and go right to the recommendations
Expand All @@ -31,4 +36,9 @@ export const ONE_QUESTION_VISIBILITY_FLAGS = {
showSkillsList: true,
showSmallHeader: true,
showCategorizinator: true,
sortSkillsByUniquePostings: false,
filterSkillsWithResults: false,
showAllSkills: true,
isClickableSkills: true,
isClickableSkillsDevMode: false,
};

0 comments on commit 919838c

Please sign in to comment.