Skip to content

Commit b73a1bf

Browse files
feat: improve job edit flow career chart (#769)
1 parent 1251ceb commit b73a1bf

File tree

12 files changed

+596
-168
lines changed

12 files changed

+596
-168
lines changed

src/components/my-career/AddJobRole.jsx

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,31 @@
1-
import React, { useContext, useEffect } from 'react';
1+
import React, { useEffect, useState } from 'react';
2+
import PropTypes from 'prop-types';
23
import { useHistory, useLocation } from 'react-router-dom';
34
import {
4-
Alert, Row, breakpoints, MediaQuery, Hyperlink, Icon, useToggle,
5+
Alert, Row, breakpoints, MediaQuery, Icon, useToggle, TransitionReplace, Button,
56
} from '@edx/paragon';
6-
import { AppContext } from '@edx/frontend-platform/react';
77
import { MainContent, Sidebar } from '../layout';
88
import { DashboardSidebar } from '../dashboard/sidebar';
99
import { CourseEnrollmentsContextProvider } from '../dashboard/main-content/course-enrollments';
1010
import CourseEnrollmentFailedAlert, { ENROLLMENT_SOURCE } from '../course/CourseEnrollmentFailedAlert';
1111
import { LICENSE_ACTIVATION_MESSAGE } from '../dashboard/data/constants';
12-
12+
import SearchJobRole from './SearchJobRole';
1313
import SkillsQuizImage from '../../assets/images/skills-quiz/skills-quiz.png';
1414

15-
const AddJobRole = () => {
15+
const addIcon = () => (
16+
<Icon
17+
id="add-job-role-icon"
18+
className="fa fa-plus add-job-icon"
19+
screenReaderText="Add Role"
20+
/>
21+
);
22+
23+
const AddJobRole = ({ submitClickHandler }) => {
1624
const { state } = useLocation();
1725
const history = useHistory();
26+
const [isEditable, setIsEditable] = useState(false);
1827
const [isActivationAlertOpen, , closeActivationAlert] = useToggle(!!state?.activationSuccess);
28+
1929
useEffect(() => {
2030
if (state?.activationSuccess) {
2131
const updatedLocationState = { ...state };
@@ -24,9 +34,18 @@ const AddJobRole = () => {
2434
}
2535
}, [history, state]);
2636

27-
const {
28-
enterpriseConfig: { slug },
29-
} = useContext(AppContext);
37+
const addRoleClickHandler = () => {
38+
setIsEditable(true);
39+
};
40+
41+
const onSaveRole = (resp) => {
42+
submitClickHandler(resp);
43+
setIsEditable(false);
44+
};
45+
46+
const onCancelRole = () => {
47+
setIsEditable(false);
48+
};
3049

3150
return (
3251
<>
@@ -50,20 +69,30 @@ const AddJobRole = () => {
5069
<h2>Visualize your career.</h2>
5170
<div className="row job-role-details">
5271
<div className="col-lg-6 col-sm-12">
53-
<p>
54-
Take one minute to pick a job title that best describes your
55-
current or desired role. We&apos;ll tell you what skills you
56-
should be looking for when enrolling in courses, and track
57-
your skill growth as you complete courses.
58-
</p>
59-
<Hyperlink destination={`/${slug}/skills-quiz`}>
60-
<Icon
61-
id="add-job-role-icon"
62-
className="fa fa-plus add-job-icon"
63-
screenReaderText="Add Role"
64-
/>
65-
Add Role
66-
</Hyperlink>
72+
<TransitionReplace className="mb-3">
73+
{!isEditable ? (
74+
<div key="add-job-button">
75+
<p>
76+
Take one minute to pick a job title that best describes your
77+
current or desired role. We&apos;ll tell you what skills you
78+
should be looking for when enrolling in courses, and track
79+
your skill growth as you complete courses.
80+
</p>
81+
<Button
82+
style={{ paddingLeft: 0 }}
83+
variant="link"
84+
iconBefore={addIcon}
85+
onClick={addRoleClickHandler}
86+
>
87+
Add Role
88+
</Button>
89+
</div>
90+
) : (
91+
<div key="add-job-dropdown">
92+
<SearchJobRole onSave={onSaveRole} onCancel={onCancelRole} />
93+
</div>
94+
)}
95+
</TransitionReplace>
6796
</div>
6897
<div className="col-lg-6 col-sm-12">
6998
<img
@@ -88,4 +117,8 @@ const AddJobRole = () => {
88117
);
89118
};
90119

120+
AddJobRole.propTypes = {
121+
submitClickHandler: PropTypes.func.isRequired,
122+
};
123+
91124
export default AddJobRole;

src/components/my-career/MyCareerTab.jsx

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,54 @@
1-
import React, { useContext } from 'react';
1+
import React, {
2+
useContext, useState, useEffect,
3+
} from 'react';
24

35
import { AppContext, ErrorPage } from '@edx/frontend-platform/react';
46
import { SearchData } from '@edx/frontend-enterprise-catalog-search';
5-
import { useLearnerSkillQuiz } from './data/hooks';
7+
import { useLearnerProfileData } from './data/hooks';
68
import { LoadingSpinner } from '../loading-spinner';
79
import AddJobRole from './AddJobRole';
810
import VisualizeCareer from './VisualizeCareer';
9-
import { getSkillQuiz } from './data/utils';
10-
import { SEARCH_FACET_FILTERS } from '../search/constants';
11+
import { extractCurrentJobID } from './data/utils';
1112

1213
const MyCareerTab = () => {
1314
const { authenticatedUser } = useContext(AppContext);
1415
const { username } = authenticatedUser;
1516

16-
const [learnerSkillQuiz, learnerSkillQuizFetchError] = useLearnerSkillQuiz(
17+
const [learnerProfileData, learnerProfileDataFetchError, isLoadingData] = useLearnerProfileData(
1718
username,
1819
);
20+
const [learnerProfileState, setLearnerProfileState] = useState();
1921

20-
if (learnerSkillQuizFetchError) {
21-
return <ErrorPage status={learnerSkillQuizFetchError.status} />;
22+
useEffect(() => {
23+
if (learnerProfileData) {
24+
setLearnerProfileState(learnerProfileData);
25+
}
26+
}, [learnerProfileData]);
27+
28+
if (learnerProfileDataFetchError) {
29+
return <ErrorPage status={learnerProfileDataFetchError.status} />;
2230
}
2331

24-
if (!learnerSkillQuiz) {
32+
if (isLoadingData && !learnerProfileState) {
2533
return (
2634
<div className="py-5">
2735
<LoadingSpinner screenReaderText="loading my career data" />
2836
</div>
2937
);
3038
}
3139

32-
const skillQuiz = getSkillQuiz(learnerSkillQuiz);
33-
34-
return (!skillQuiz) ? (
35-
<AddJobRole />
36-
) : (
37-
<SearchData searchFacetFilters={SEARCH_FACET_FILTERS}>
38-
<VisualizeCareer jobId={skillQuiz.currentJob} />
39-
</SearchData>
40+
const learnerCurrentJobID = extractCurrentJobID(learnerProfileState);
41+
42+
return (
43+
<div>
44+
<SearchData>
45+
{ !learnerCurrentJobID ? (
46+
<AddJobRole submitClickHandler={setLearnerProfileState} />
47+
) : (
48+
<VisualizeCareer jobId={learnerCurrentJobID} submitClickHandler={setLearnerProfileState} />
49+
)}
50+
</SearchData>
51+
</div>
4052
);
4153
};
4254

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import React, { useContext, useState } from 'react';
2+
import PropTypes from 'prop-types';
3+
import { getConfig } from '@edx/frontend-platform/config';
4+
import { logError } from '@edx/frontend-platform/logging';
5+
import { InstantSearch } from 'react-instantsearch-dom';
6+
import { SearchContext, deleteRefinementAction } from '@edx/frontend-enterprise-catalog-search';
7+
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
8+
import FacetListRefinement from '@edx/frontend-enterprise-catalog-search/FacetListRefinement';
9+
import { camelCaseObject } from '@edx/frontend-platform/utils';
10+
import {
11+
Button, Form, StatefulButton,
12+
} from '@edx/paragon';
13+
import { CURRENT_JOB_FACET } from '../skills-quiz/constants';
14+
import { patchProfile, fetchJobDetailsFromAlgolia } from './data/service';
15+
import { CURRENT_JOB_PROFILE_FIELD_NAME, SAVE_BUTTON_LABELS } from './data/constants';
16+
import { useAlgoliaSearch } from '../../utils/hooks';
17+
18+
const SearchJobRole = (props) => {
19+
const config = getConfig();
20+
const [searchClient, searchIndex] = useAlgoliaSearch(config, config.ALGOLIA_INDEX_NAME_JOBS);
21+
const { username } = getAuthenticatedUser();
22+
const { refinements, dispatch } = useContext(SearchContext);
23+
const { current_job: currentJob } = refinements;
24+
const [loadingRequest, setLoadingRequest] = useState(false);
25+
const [requestComplete, setRequestComplete] = useState(false);
26+
const [requestError, setRequestError] = useState(false);
27+
28+
const {
29+
title, attribute, typeaheadOptions, facetValueType, customAttribute,
30+
} = CURRENT_JOB_FACET;
31+
32+
const getDisabledStates = () => {
33+
if (currentJob) {
34+
return ['pending', 'complete'];
35+
}
36+
return ['pending', 'complete', 'default'];
37+
};
38+
39+
const getButtonState = () => {
40+
if (requestError) {
41+
return 'error';
42+
}
43+
if (requestComplete) {
44+
return 'complete';
45+
}
46+
if (loadingRequest) {
47+
return 'pending';
48+
}
49+
return 'default';
50+
};
51+
52+
const handleSubmit = async () => {
53+
let resp = {};
54+
setLoadingRequest(true);
55+
const { id: currentJobID } = await fetchJobDetailsFromAlgolia(searchIndex, currentJob);
56+
const params = {
57+
extended_profile: [
58+
{ field_name: CURRENT_JOB_PROFILE_FIELD_NAME, field_value: currentJobID },
59+
],
60+
};
61+
62+
try {
63+
resp = await patchProfile(username, params);
64+
} catch (error) {
65+
setLoadingRequest(false);
66+
setRequestError(true);
67+
logError(new Error(error));
68+
}
69+
setRequestComplete(true);
70+
setLoadingRequest(false);
71+
if (currentJob) {
72+
dispatch(deleteRefinementAction(customAttribute));
73+
}
74+
props.onSave(camelCaseObject(resp));
75+
};
76+
77+
const handleCancelButtonClick = () => {
78+
if (currentJob) {
79+
dispatch(deleteRefinementAction(customAttribute));
80+
}
81+
props.onCancel();
82+
};
83+
84+
return (
85+
<div>
86+
<form>
87+
<Form.Group>
88+
<InstantSearch
89+
indexName={config.ALGOLIA_INDEX_NAME_JOBS}
90+
searchClient={searchClient}
91+
>
92+
<p> Search for your job role </p>
93+
<FacetListRefinement
94+
id="current-job-dropdown"
95+
key={attribute}
96+
title={refinements[customAttribute]?.length > 0 ? refinements[customAttribute][0] : title}
97+
attribute={attribute}
98+
limit={300}
99+
refinements={refinements}
100+
facetValueType={facetValueType}
101+
typeaheadOptions={typeaheadOptions}
102+
searchable={!!typeaheadOptions}
103+
doRefinement={false}
104+
customAttribute={customAttribute}
105+
showBadge={false}
106+
variant="default"
107+
/>
108+
</InstantSearch>
109+
</Form.Group>
110+
<p>
111+
<StatefulButton
112+
type="submit"
113+
className="mr-2"
114+
labels={{
115+
default: SAVE_BUTTON_LABELS.DEFAULT,
116+
pending: SAVE_BUTTON_LABELS.PENDING,
117+
complete: SAVE_BUTTON_LABELS.COMPLETE,
118+
error: SAVE_BUTTON_LABELS.ERROR,
119+
}}
120+
state={getButtonState()}
121+
onClick={(e) => {
122+
e.preventDefault();
123+
handleSubmit();
124+
}}
125+
disabledStates={getDisabledStates()}
126+
data-testid="save-button"
127+
/>
128+
<Button
129+
variant="outline-primary"
130+
onClick={handleCancelButtonClick}
131+
className="cancel-btn"
132+
data-testid="cancel-button"
133+
>
134+
Cancel
135+
</Button>
136+
</p>
137+
</form>
138+
</div>
139+
);
140+
};
141+
142+
SearchJobRole.propTypes = {
143+
onSave: PropTypes.func.isRequired,
144+
onCancel: PropTypes.func.isRequired,
145+
};
146+
147+
export default SearchJobRole;

0 commit comments

Comments
 (0)