Compare commits
1 Commits
ags/localh
...
ags/upgrad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
532cd5001b |
1
.env
1
.env
@@ -25,7 +25,6 @@ FAVICON_URL=''
|
||||
COLLECT_YEAR_OF_BIRTH=true
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
SEARCH_CATALOG_URL=''
|
||||
ENABLE_SKILLS_BUILDER=''
|
||||
ENABLE_SKILLS_BUILDER_PROFILE=''
|
||||
ALGOLIA_APP_ID=''
|
||||
|
||||
@@ -26,7 +26,6 @@ FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
COLLECT_YEAR_OF_BIRTH=true
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
SEARCH_CATALOG_URL='http://localhost:18000/courses'
|
||||
ENABLE_SKILLS_BUILDER='true'
|
||||
ENABLE_SKILLS_BUILDER_PROFILE=''
|
||||
ALGOLIA_APP_ID=''
|
||||
|
||||
@@ -18,7 +18,6 @@ LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
ENABLE_LEARNER_RECORD_MFE=''
|
||||
ENABLE_SKILLS_BUILDER='true'
|
||||
ENABLE_SKILLS_BUILDER_PROFILE=''
|
||||
LEARNER_RECORD_MFE_BASE_URL='http://localhost:1990'
|
||||
COLLECT_YEAR_OF_BIRTH=true
|
||||
|
||||
@@ -16,4 +16,4 @@ jobs:
|
||||
secrets:
|
||||
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
||||
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||
@@ -1,20 +0,0 @@
|
||||
# This workflow runs when a comment is made on the ticket
|
||||
# If the comment starts with "label: " it tries to apply
|
||||
# the label indicated in rest of comment.
|
||||
# If the comment starts with "remove label: ", it tries
|
||||
# to remove the indicated label.
|
||||
# Note: Labels are allowed to have spaces and this script does
|
||||
# not parse spaces (as often a space is legitimate), so the command
|
||||
# "label: really long lots of words label" will apply the
|
||||
# label "really long lots of words label"
|
||||
|
||||
name: Allows for the adding and removing of labels via comment
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
add_remove_labels:
|
||||
uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master
|
||||
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -13,13 +13,13 @@ jobs:
|
||||
- i18n_extract
|
||||
- lint
|
||||
- test
|
||||
node: [16]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: npm install -g npm@8.x.x
|
||||
- run: make requirements
|
||||
- run: make test NPM_TESTS=build
|
||||
- run: make test NPM_TESTS=${{ matrix.npm-test }}
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
|
||||
12
.github/workflows/self-assign-issue.yml
vendored
12
.github/workflows/self-assign-issue.yml
vendored
@@ -1,12 +0,0 @@
|
||||
# This workflow runs when a comment is made on the ticket
|
||||
# If the comment starts with "assign me" it assigns the author to the
|
||||
# ticket (case insensitive)
|
||||
|
||||
name: Assign comment author to ticket if they say "assign me"
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
self_assign_by_comment:
|
||||
uses: openedx/.github/.github/workflows/self-assign-issue.yml@master
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -16,7 +16,4 @@ temp/babel-plugin-react-intl
|
||||
*~
|
||||
/temp
|
||||
/.vscode
|
||||
|
||||
# Open edX
|
||||
module.config.js
|
||||
.env.development-stage
|
||||
/module.config.js
|
||||
|
||||
18
Makefile
18
Makefile
@@ -2,10 +2,12 @@ export TRANSIFEX_RESOURCE = frontend-app-profile
|
||||
transifex_resource = frontend-app-profile
|
||||
transifex_langs = "ar,fr,es_419,zh_CN,pt,it,de,uk,ru,hi,fr_CA"
|
||||
|
||||
intl_imports = ./node_modules/.bin/intl-imports.js
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
|
||||
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
|
||||
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
|
||||
@@ -50,23 +52,9 @@ push_translations:
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||
|
||||
ifeq ($(OPENEDX_ATLAS_PULL),)
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||
else
|
||||
# Experimental: OEP-58 Pulls translations using atlas
|
||||
pull_translations:
|
||||
rm -rf src/i18n/messages
|
||||
mkdir src/i18n/messages
|
||||
cd src/i18n/messages \
|
||||
&& atlas pull --filter=$(transifex_langs) \
|
||||
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
|
||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||
translations/frontend-app-profile/src/i18n/messages:frontend-app-profile
|
||||
|
||||
$(intl_imports) frontend-component-header frontend-component-footer frontend-app-profile
|
||||
endif
|
||||
|
||||
# This target is used by Travis.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
frontend-app-profile
|
||||
====================
|
||||
|
||||
This is a micro-frontend application responsible for the display and updating of user profiles. Please tag **@openedx/2u-aperture** on any PRs or issues.
|
||||
This is a micro-frontend application responsible for the display and updating of user profiles. Please tag **@edx/arch-fed** on any PRs or issues.
|
||||
|
||||
When a user views their own profile, they're given fields to edit their full name, location, primary spoken language, education, social links, and bio. Each field also has a dropdown to select the visibility of that field - i.e., whether it can be viewed by other learners.
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
const config = {
|
||||
// Override default .env.development values
|
||||
ACCESS_TOKEN_COOKIE_NAME: 'stage-edx-jwt-cookie-header-payload',
|
||||
CREDENTIALS_BASE_URL: 'https://credentials.stage.edx.org',
|
||||
LMS_BASE_URL: 'https://courses.stage.edx.org',
|
||||
LOGIN_URL: 'https://courses.stage.edx.org/login',
|
||||
LOGOUT_URL: 'https://courses.stage.edx.org/logout',
|
||||
MARKETING_SITE_BASE_URL: 'https://stage.edx.org',
|
||||
ORDER_HISTORY_URL: 'https://orders.stage.edx.org/orders',
|
||||
ENTERPRISE_LEARNER_PORTAL_HOSTNAME: 'enterprise.stage.edx.org',
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT: 'https://courses.stage.edx.org/login_refresh',
|
||||
// Paragon theme URLs
|
||||
PARAGON_THEME_URLS: {
|
||||
core: 'https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/core.min.css',
|
||||
variants: {
|
||||
light: {
|
||||
url: 'https://cdn.jsdelivr.net/npm/@edx/paragon@$paragonVersion/dist/light.min.css',
|
||||
default: true,
|
||||
dark: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
22525
package-lock.json
generated
22525
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@@ -28,19 +28,18 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
|
||||
"@edx/frontend-component-footer": "12.0.0",
|
||||
"@edx/frontend-component-header": "4.0.0",
|
||||
"@edx/frontend-platform": "4.5.0",
|
||||
"@edx/paragon": "^20.32.0",
|
||||
"@edx/frontend-component-footer": "11.6.3",
|
||||
"@edx/frontend-component-header": "3.6.1",
|
||||
"@edx/frontend-platform": "3.4.0",
|
||||
"@edx/paragon": "^20.20.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"algoliasearch": "4.17.1",
|
||||
"algoliasearch": "4.6.0",
|
||||
"classnames": "2.3.2",
|
||||
"core-js": "3.30.2",
|
||||
"history": "4.10.1",
|
||||
"core-js": "3.27.2",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
"lodash.get": "4.4.2",
|
||||
"lodash.pick": "4.4.0",
|
||||
@@ -56,18 +55,18 @@
|
||||
"redux": "4.2.1",
|
||||
"redux-devtools-extension": "2.13.9",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-saga": "1.2.3",
|
||||
"redux-saga": "1.2.2",
|
||||
"redux-thunk": "2.4.2",
|
||||
"regenerator-runtime": "0.13.11",
|
||||
"reselect": "4.1.8",
|
||||
"reselect": "4.1.7",
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "17.6.3",
|
||||
"@commitlint/config-angular": "17.6.3",
|
||||
"@commitlint/cli": "17.4.4",
|
||||
"@commitlint/config-angular": "17.4.4",
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@edx/frontend-build": "12.8.38",
|
||||
"@edx/reactifex": "2.2.0",
|
||||
"@edx/frontend-build": "12.4.19",
|
||||
"@edx/reactifex": "2.1.1",
|
||||
"@testing-library/react": "11.2.7",
|
||||
"codecov": "3.8.3",
|
||||
"enzyme": "3.11.0",
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { messages as headerMessages } from '@edx/frontend-component-header';
|
||||
import { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||
|
||||
import arMessages from './messages/ar.json';
|
||||
import frMessages from './messages/fr.json';
|
||||
import es419Messages from './messages/es_419.json';
|
||||
@@ -14,7 +11,7 @@ import hiMessages from './messages/hi.json';
|
||||
import frCAMessages from './messages/fr_CA.json';
|
||||
// no need to import en messages-- they are in the defaultMessage field
|
||||
|
||||
const appMessages = {
|
||||
const messages = {
|
||||
ar: arMessages,
|
||||
'es-419': es419Messages,
|
||||
fr: frMessages,
|
||||
@@ -28,8 +25,4 @@ const appMessages = {
|
||||
uk: ukMessages,
|
||||
};
|
||||
|
||||
export default [
|
||||
headerMessages,
|
||||
footerMessages,
|
||||
appMessages,
|
||||
];
|
||||
export default messages;
|
||||
@@ -34,11 +34,11 @@
|
||||
"profile.formcontrols.button.saved": "تم الحفظ",
|
||||
"profile.visibility.who.just.me": "أنا فقط",
|
||||
"profile.visibility.who.everyone": "جميع من على {siteName}",
|
||||
"profile.learningGoal.learningGoal": "هدف التعلم",
|
||||
"profile.learningGoal.options.start_career": "أريد أن أبدأ مسيرتي المهنية",
|
||||
"profile.learningGoal.options.advance_career": "أريد أن ارتقي في مسيرتي المهنية",
|
||||
"profile.learningGoal.options.learn_something_new": "أريد أن أتعلم شيئًا جديدًا",
|
||||
"profile.learningGoal.options.something_else": "شيء آخر",
|
||||
"profile.learningGoal.learningGoal": "Learning Goal",
|
||||
"profile.learningGoal.options.start_career": "I want to start my career",
|
||||
"profile.learningGoal.options.advance_career": "I want to advance my career",
|
||||
"profile.learningGoal.options.learn_something_new": "I want to learn something new",
|
||||
"profile.learningGoal.options.something_else": "Something else",
|
||||
"profile.name.full.name": "الاسم الكامل",
|
||||
"profile.name.details": "هذا هو الاسم الذي يظهر في حسابك وفي شهاداتك",
|
||||
"profile.name.empty": "إضافة الاسم",
|
||||
@@ -54,31 +54,9 @@
|
||||
"profile.viewMyRecords": "عرض سجلّاتي",
|
||||
"profile.loading": "يتم تحميل الملف الشخصي...",
|
||||
"profile.username.description": "معلومات ملفك الشخصي تظهر لك فقط. وحده اسم المستخدم الخاص بك يظهر للآخرين على {siteName}.",
|
||||
"skills.builder.header.title": "باني المهارات",
|
||||
"skills.builder.header.subheading": "دع (المنصة التعليمية أو edX) ان تكون دليلك",
|
||||
"go.back.button": "العودة إلى الخلف",
|
||||
"next.step.button": "الخطوة التالية",
|
||||
"exit.button": "خروج",
|
||||
"select.preferences": "حدد التفضيلاتك",
|
||||
"review.results": "مراجعة النتائج",
|
||||
"skills.builder.description": "ابحث عن الدورات والبرامج المناسبة التي تساعدك في الوصول إلى أهدافك.",
|
||||
"learning.goal.prompt": "أولاً، أخبرنا بما تريد تحقيقه",
|
||||
"select.learning.goal": "اختر هدفًا",
|
||||
"learning.goal.start_career": "أريد أن أبدأ مسيرتي المهنية",
|
||||
"learning.goal.advance_career": "أريد أن ارتقي في مهنتي",
|
||||
"learning.goal.change_career": "اريد تغيير المهنتي",
|
||||
"learning.goal.something.new": "أريد أن أتعلم شيئًا جديدًا",
|
||||
"learning.goal.something.else": "شيء آخر",
|
||||
"job.title.prompt": "بعد ذلك، ابحث وحدد المسمى الوظيفي الحالي الخاص بك",
|
||||
"job.title.input.placeholder.text": "أبحث واختار مسمى وظيفي",
|
||||
"student.checkbox.prompt": "أنا طالب",
|
||||
"currently.looking.checkbox.prompt": "أنا حاليا أبحث عن عمل",
|
||||
"career.interest.prompt": "ما هي المهن التي تثير اهتمامك؟",
|
||||
"career.interest.input.placeholder.text": "حدد ما يصل إلى ثلاث عناوين وظيفية جديدة",
|
||||
"career.interest.remove.button.alt.text": "إزالة الاهتمام الوظيفي:",
|
||||
"matches.found.success.alert": "وجدنا المهارات والدورات التي تناسب تفضيلاتك!",
|
||||
"matches.not.found.danger.alert": "لم نتمكن من استرداد التوصيات في هذا الوقت. الرجاء معاودة المحاولة في وقت لاحق.",
|
||||
"related.skills.heading": "مهارات ذات الصلة",
|
||||
"related.skills.selectable.box.label.text": "مهارات ذات الصلة:",
|
||||
"product.recommendations.header.text": "{productType} recommendations for {jobName}"
|
||||
"skills.builder.header.title": "Skills Builder",
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit"
|
||||
}
|
||||
@@ -58,27 +58,5 @@
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit",
|
||||
"select.preferences": "Select preferences",
|
||||
"review.results": "Review results",
|
||||
"skills.builder.description": "Find the right courses and programs that help you reach your goals.",
|
||||
"learning.goal.prompt": "First, tell us what you want to achieve",
|
||||
"select.learning.goal": "Select a goal",
|
||||
"learning.goal.start_career": "I want to start my career",
|
||||
"learning.goal.advance_career": "I want to advance my career",
|
||||
"learning.goal.change_career": "I want to change careers",
|
||||
"learning.goal.something.new": "I want to learn something new",
|
||||
"learning.goal.something.else": "Something else",
|
||||
"job.title.prompt": "Next, search and select your current job title",
|
||||
"job.title.input.placeholder.text": "Search and select a job title",
|
||||
"student.checkbox.prompt": "I'm a student",
|
||||
"currently.looking.checkbox.prompt": "I'm currently looking for work",
|
||||
"career.interest.prompt": "What careers are you interested in?",
|
||||
"career.interest.input.placeholder.text": "Select up to 3 new job titles",
|
||||
"career.interest.remove.button.alt.text": "Remove career interest: ",
|
||||
"matches.found.success.alert": "We found skills and courses that match your preferences!",
|
||||
"matches.not.found.danger.alert": "We were not able to retrieve recommendations at this time. Please try again later.",
|
||||
"related.skills.heading": "Related Skills",
|
||||
"related.skills.selectable.box.label.text": "Related skills:",
|
||||
"product.recommendations.header.text": "{productType} recommendations for {jobName}"
|
||||
"exit.button": "Exit"
|
||||
}
|
||||
@@ -58,27 +58,5 @@
|
||||
"skills.builder.header.subheading": "Dejanos ser tu guía",
|
||||
"go.back.button": "Volver Atrás",
|
||||
"next.step.button": "Próximo paso",
|
||||
"exit.button": "Salida",
|
||||
"select.preferences": "Seleccionar preferencias",
|
||||
"review.results": "Revisar resultados",
|
||||
"skills.builder.description": "Encontrar los cursos y programas adecuados que lo ayuden a alcanzar sus metas.",
|
||||
"learning.goal.prompt": "Primero, contar qué quieres lograr",
|
||||
"select.learning.goal": "Seleccionar una meta",
|
||||
"learning.goal.start_career": "Quiero empezar mi carrera",
|
||||
"learning.goal.advance_career": "Quiero avanzar en mi carrera",
|
||||
"learning.goal.change_career": "Quiero cambiar de carrera",
|
||||
"learning.goal.something.new": "Quiero aprender algo nuevo",
|
||||
"learning.goal.something.else": "Algo más",
|
||||
"job.title.prompt": "A continuación, busque y seleccione su título de trabajo actual",
|
||||
"job.title.input.placeholder.text": "Search and select a job title",
|
||||
"student.checkbox.prompt": "Soy un estudiante",
|
||||
"currently.looking.checkbox.prompt": "Actualmente estoy buscando trabajo",
|
||||
"career.interest.prompt": "¿Qué carreras te interesan?",
|
||||
"career.interest.input.placeholder.text": "Select up to 3 new job titles",
|
||||
"career.interest.remove.button.alt.text": "Eliminar interés profesional:",
|
||||
"matches.found.success.alert": "We found skills and courses that match your preferences!",
|
||||
"matches.not.found.danger.alert": "We were not able to retrieve recommendations at this time. Please try again later.",
|
||||
"related.skills.heading": "Related Skills",
|
||||
"related.skills.selectable.box.label.text": "Related skills:",
|
||||
"product.recommendations.header.text": "{productType} recommendations for {jobName}"
|
||||
"exit.button": "Exit"
|
||||
}
|
||||
@@ -58,27 +58,5 @@
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit",
|
||||
"select.preferences": "Select preferences",
|
||||
"review.results": "Review results",
|
||||
"skills.builder.description": "Find the right courses and programs that help you reach your goals.",
|
||||
"learning.goal.prompt": "First, tell us what you want to achieve",
|
||||
"select.learning.goal": "Select a goal",
|
||||
"learning.goal.start_career": "I want to start my career",
|
||||
"learning.goal.advance_career": "I want to advance my career",
|
||||
"learning.goal.change_career": "I want to change careers",
|
||||
"learning.goal.something.new": "I want to learn something new",
|
||||
"learning.goal.something.else": "Something else",
|
||||
"job.title.prompt": "Next, search and select your current job title",
|
||||
"job.title.input.placeholder.text": "Search and select a job title",
|
||||
"student.checkbox.prompt": "I'm a student",
|
||||
"currently.looking.checkbox.prompt": "I'm currently looking for work",
|
||||
"career.interest.prompt": "What careers are you interested in?",
|
||||
"career.interest.input.placeholder.text": "Select up to 3 new job titles",
|
||||
"career.interest.remove.button.alt.text": "Remove career interest: ",
|
||||
"matches.found.success.alert": "We found skills and courses that match your preferences!",
|
||||
"matches.not.found.danger.alert": "We were not able to retrieve recommendations at this time. Please try again later.",
|
||||
"related.skills.heading": "Related Skills",
|
||||
"related.skills.selectable.box.label.text": "Related skills:",
|
||||
"product.recommendations.header.text": "{productType} recommendations for {jobName}"
|
||||
"exit.button": "Exit"
|
||||
}
|
||||
@@ -58,27 +58,5 @@
|
||||
"skills.builder.header.subheading": "Laissez EDUlib être votre guide",
|
||||
"go.back.button": "Retour",
|
||||
"next.step.button": "Prochaine étape",
|
||||
"exit.button": "Sortie",
|
||||
"select.preferences": "Sélectionnez les préférences",
|
||||
"review.results": "Examiner les résultats",
|
||||
"skills.builder.description": "Trouvez les bons cours et programmes qui vous aideront à atteindre vos objectifs.",
|
||||
"learning.goal.prompt": "Tout d'abord, dites-nous ce que vous voulez réaliser",
|
||||
"select.learning.goal": "Sélectionnez un objectif",
|
||||
"learning.goal.start_career": "Je veux commencer ma carrière",
|
||||
"learning.goal.advance_career": "Je veux faire progresser ma carrière",
|
||||
"learning.goal.change_career": "Je veux changer de métier",
|
||||
"learning.goal.something.new": "Je veux apprendre quelque chose de nouveau",
|
||||
"learning.goal.something.else": "Autre chose",
|
||||
"job.title.prompt": "Ensuite, recherchez et sélectionnez votre titre de poste actuel",
|
||||
"job.title.input.placeholder.text": "Rechercher et sélectionner un intitulé de poste",
|
||||
"student.checkbox.prompt": "Je suis étudiant.e",
|
||||
"currently.looking.checkbox.prompt": "Je suis actuellement à la recherche d'un emploi",
|
||||
"career.interest.prompt": "Quels métiers vous intéressent ?",
|
||||
"career.interest.input.placeholder.text": "Sélectionnez jusqu'à 3 nouveaux intitulés de poste",
|
||||
"career.interest.remove.button.alt.text": "Supprimer l'intérêt professionnel :",
|
||||
"matches.found.success.alert": "Nous avons trouvé des compétences et des cours qui correspondent à vos préférences !",
|
||||
"matches.not.found.danger.alert": "Nous n'avons pas pu récupérer les recommandations pour le moment. Veuillez réessayer plus tard.",
|
||||
"related.skills.heading": "Compétences connexes",
|
||||
"related.skills.selectable.box.label.text": "Compétences connexes:",
|
||||
"product.recommendations.header.text": "{productType} recommandations pour {jobName}"
|
||||
"exit.button": "Sortie"
|
||||
}
|
||||
@@ -58,27 +58,5 @@
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit",
|
||||
"select.preferences": "Select preferences",
|
||||
"review.results": "Review results",
|
||||
"skills.builder.description": "Find the right courses and programs that help you reach your goals.",
|
||||
"learning.goal.prompt": "First, tell us what you want to achieve",
|
||||
"select.learning.goal": "Select a goal",
|
||||
"learning.goal.start_career": "I want to start my career",
|
||||
"learning.goal.advance_career": "I want to advance my career",
|
||||
"learning.goal.change_career": "I want to change careers",
|
||||
"learning.goal.something.new": "I want to learn something new",
|
||||
"learning.goal.something.else": "Something else",
|
||||
"job.title.prompt": "Next, search and select your current job title",
|
||||
"job.title.input.placeholder.text": "Search and select a job title",
|
||||
"student.checkbox.prompt": "I'm a student",
|
||||
"currently.looking.checkbox.prompt": "I'm currently looking for work",
|
||||
"career.interest.prompt": "What careers are you interested in?",
|
||||
"career.interest.input.placeholder.text": "Select up to 3 new job titles",
|
||||
"career.interest.remove.button.alt.text": "Remove career interest: ",
|
||||
"matches.found.success.alert": "We found skills and courses that match your preferences!",
|
||||
"matches.not.found.danger.alert": "We were not able to retrieve recommendations at this time. Please try again later.",
|
||||
"related.skills.heading": "Related Skills",
|
||||
"related.skills.selectable.box.label.text": "Related skills:",
|
||||
"product.recommendations.header.text": "{productType} recommendations for {jobName}"
|
||||
"exit.button": "Exit"
|
||||
}
|
||||
@@ -58,27 +58,5 @@
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit",
|
||||
"select.preferences": "Select preferences",
|
||||
"review.results": "Review results",
|
||||
"skills.builder.description": "Find the right courses and programs that help you reach your goals.",
|
||||
"learning.goal.prompt": "First, tell us what you want to achieve",
|
||||
"select.learning.goal": "Select a goal",
|
||||
"learning.goal.start_career": "I want to start my career",
|
||||
"learning.goal.advance_career": "I want to advance my career",
|
||||
"learning.goal.change_career": "I want to change careers",
|
||||
"learning.goal.something.new": "I want to learn something new",
|
||||
"learning.goal.something.else": "Something else",
|
||||
"job.title.prompt": "Next, search and select your current job title",
|
||||
"job.title.input.placeholder.text": "Search and select a job title",
|
||||
"student.checkbox.prompt": "I'm a student",
|
||||
"currently.looking.checkbox.prompt": "I'm currently looking for work",
|
||||
"career.interest.prompt": "What careers are you interested in?",
|
||||
"career.interest.input.placeholder.text": "Select up to 3 new job titles",
|
||||
"career.interest.remove.button.alt.text": "Remove career interest: ",
|
||||
"matches.found.success.alert": "We found skills and courses that match your preferences!",
|
||||
"matches.not.found.danger.alert": "We were not able to retrieve recommendations at this time. Please try again later.",
|
||||
"related.skills.heading": "Related Skills",
|
||||
"related.skills.selectable.box.label.text": "Related skills:",
|
||||
"product.recommendations.header.text": "{productType} recommendations for {jobName}"
|
||||
"exit.button": "Exit"
|
||||
}
|
||||
@@ -58,27 +58,5 @@
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit",
|
||||
"select.preferences": "Select preferences",
|
||||
"review.results": "Review results",
|
||||
"skills.builder.description": "Find the right courses and programs that help you reach your goals.",
|
||||
"learning.goal.prompt": "First, tell us what you want to achieve",
|
||||
"select.learning.goal": "Select a goal",
|
||||
"learning.goal.start_career": "I want to start my career",
|
||||
"learning.goal.advance_career": "I want to advance my career",
|
||||
"learning.goal.change_career": "I want to change careers",
|
||||
"learning.goal.something.new": "I want to learn something new",
|
||||
"learning.goal.something.else": "Something else",
|
||||
"job.title.prompt": "Next, search and select your current job title",
|
||||
"job.title.input.placeholder.text": "Search and select a job title",
|
||||
"student.checkbox.prompt": "I'm a student",
|
||||
"currently.looking.checkbox.prompt": "I'm currently looking for work",
|
||||
"career.interest.prompt": "What careers are you interested in?",
|
||||
"career.interest.input.placeholder.text": "Select up to 3 new job titles",
|
||||
"career.interest.remove.button.alt.text": "Remove career interest: ",
|
||||
"matches.found.success.alert": "We found skills and courses that match your preferences!",
|
||||
"matches.not.found.danger.alert": "We were not able to retrieve recommendations at this time. Please try again later.",
|
||||
"related.skills.heading": "Related Skills",
|
||||
"related.skills.selectable.box.label.text": "Related skills:",
|
||||
"product.recommendations.header.text": "{productType} recommendations for {jobName}"
|
||||
"exit.button": "Exit"
|
||||
}
|
||||
@@ -58,27 +58,5 @@
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit",
|
||||
"select.preferences": "Select preferences",
|
||||
"review.results": "Review results",
|
||||
"skills.builder.description": "Find the right courses and programs that help you reach your goals.",
|
||||
"learning.goal.prompt": "First, tell us what you want to achieve",
|
||||
"select.learning.goal": "Select a goal",
|
||||
"learning.goal.start_career": "I want to start my career",
|
||||
"learning.goal.advance_career": "I want to advance my career",
|
||||
"learning.goal.change_career": "I want to change careers",
|
||||
"learning.goal.something.new": "I want to learn something new",
|
||||
"learning.goal.something.else": "Something else",
|
||||
"job.title.prompt": "Next, search and select your current job title",
|
||||
"job.title.input.placeholder.text": "Search and select a job title",
|
||||
"student.checkbox.prompt": "I'm a student",
|
||||
"currently.looking.checkbox.prompt": "I'm currently looking for work",
|
||||
"career.interest.prompt": "What careers are you interested in?",
|
||||
"career.interest.input.placeholder.text": "Select up to 3 new job titles",
|
||||
"career.interest.remove.button.alt.text": "Remove career interest: ",
|
||||
"matches.found.success.alert": "We found skills and courses that match your preferences!",
|
||||
"matches.not.found.danger.alert": "We were not able to retrieve recommendations at this time. Please try again later.",
|
||||
"related.skills.heading": "Related Skills",
|
||||
"related.skills.selectable.box.label.text": "Related skills:",
|
||||
"product.recommendations.header.text": "{productType} recommendations for {jobName}"
|
||||
"exit.button": "Exit"
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
"profile.certificate.organization.label": "From",
|
||||
"profile.certificate.completion.date.label": "Completed on {date}",
|
||||
"profile.no.certificates": "You don't have any certificates yet.",
|
||||
"profile.certificates.my.certificates": "Мої сертифікати",
|
||||
"profile.certificates.my.certificates": "My Certificates",
|
||||
"profile.certificates.view.certificate": "View Certificate",
|
||||
"profile.certificates.types.verified": "Verified Certificate",
|
||||
"profile.certificates.types.professional": "Professional Certificate",
|
||||
@@ -58,27 +58,5 @@
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit",
|
||||
"select.preferences": "Select preferences",
|
||||
"review.results": "Review results",
|
||||
"skills.builder.description": "Find the right courses and programs that help you reach your goals.",
|
||||
"learning.goal.prompt": "First, tell us what you want to achieve",
|
||||
"select.learning.goal": "Select a goal",
|
||||
"learning.goal.start_career": "I want to start my career",
|
||||
"learning.goal.advance_career": "I want to advance my career",
|
||||
"learning.goal.change_career": "I want to change careers",
|
||||
"learning.goal.something.new": "I want to learn something new",
|
||||
"learning.goal.something.else": "Something else",
|
||||
"job.title.prompt": "Next, search and select your current job title",
|
||||
"job.title.input.placeholder.text": "Search and select a job title",
|
||||
"student.checkbox.prompt": "I'm a student",
|
||||
"currently.looking.checkbox.prompt": "I'm currently looking for work",
|
||||
"career.interest.prompt": "What careers are you interested in?",
|
||||
"career.interest.input.placeholder.text": "Select up to 3 new job titles",
|
||||
"career.interest.remove.button.alt.text": "Remove career interest: ",
|
||||
"matches.found.success.alert": "We found skills and courses that match your preferences!",
|
||||
"matches.not.found.danger.alert": "We were not able to retrieve recommendations at this time. Please try again later.",
|
||||
"related.skills.heading": "Related Skills",
|
||||
"related.skills.selectable.box.label.text": "Related skills:",
|
||||
"product.recommendations.header.text": "{productType} recommendations for {jobName}"
|
||||
"exit.button": "Exit"
|
||||
}
|
||||
@@ -58,27 +58,5 @@
|
||||
"skills.builder.header.subheading": "Let edX be your guide",
|
||||
"go.back.button": "Go Back",
|
||||
"next.step.button": "Next Step",
|
||||
"exit.button": "Exit",
|
||||
"select.preferences": "Select preferences",
|
||||
"review.results": "Review results",
|
||||
"skills.builder.description": "Find the right courses and programs that help you reach your goals.",
|
||||
"learning.goal.prompt": "First, tell us what you want to achieve",
|
||||
"select.learning.goal": "Select a goal",
|
||||
"learning.goal.start_career": "I want to start my career",
|
||||
"learning.goal.advance_career": "I want to advance my career",
|
||||
"learning.goal.change_career": "I want to change careers",
|
||||
"learning.goal.something.new": "I want to learn something new",
|
||||
"learning.goal.something.else": "Something else",
|
||||
"job.title.prompt": "Next, search and select your current job title",
|
||||
"job.title.input.placeholder.text": "Search and select a job title",
|
||||
"student.checkbox.prompt": "I'm a student",
|
||||
"currently.looking.checkbox.prompt": "I'm currently looking for work",
|
||||
"career.interest.prompt": "What careers are you interested in?",
|
||||
"career.interest.input.placeholder.text": "Select up to 3 new job titles",
|
||||
"career.interest.remove.button.alt.text": "Remove career interest: ",
|
||||
"matches.found.success.alert": "We found skills and courses that match your preferences!",
|
||||
"matches.not.found.danger.alert": "We were not able to retrieve recommendations at this time. Please try again later.",
|
||||
"related.skills.heading": "Related Skills",
|
||||
"related.skills.selectable.box.label.text": "Related skills:",
|
||||
"product.recommendations.header.text": "{productType} recommendations for {jobName}"
|
||||
"exit.button": "Exit"
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'regenerator-runtime/runtime';
|
||||
import {
|
||||
APP_INIT_ERROR,
|
||||
APP_READY,
|
||||
getConfig,
|
||||
initialize,
|
||||
mergeConfig,
|
||||
subscribe,
|
||||
@@ -15,25 +16,33 @@ import {
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import Header from '@edx/frontend-component-header';
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import Header, { messages as headerMessages } from '@edx/frontend-component-header';
|
||||
import Footer, { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||
|
||||
import messages from './i18n';
|
||||
import appMessages from './i18n';
|
||||
import { ProfilePage, NotFoundPage } from './profile';
|
||||
import { SkillsBuilder } from './skills-builder';
|
||||
import configureStore from './data/configureStore';
|
||||
|
||||
import './index.scss';
|
||||
import Head from './head/Head';
|
||||
|
||||
import AppRoutes from './routes/AppRoutes';
|
||||
|
||||
subscribe(APP_READY, () => {
|
||||
ReactDOM.render(
|
||||
<AppProvider store={configureStore()}>
|
||||
<Head />
|
||||
<Header />
|
||||
<main>
|
||||
<AppRoutes />
|
||||
<Switch>
|
||||
{getConfig().ENABLE_SKILLS_BUILDER && (
|
||||
<Route path="/skills" component={SkillsBuilder} />
|
||||
)}
|
||||
<Route path="/u/:username" component={ProfilePage} />
|
||||
<Route path="/notfound" component={NotFoundPage} />
|
||||
<Route path="*" component={NotFoundPage} />
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer />
|
||||
</AppProvider>,
|
||||
@@ -46,7 +55,12 @@ subscribe(APP_INIT_ERROR, (error) => {
|
||||
});
|
||||
|
||||
initialize({
|
||||
messages,
|
||||
messages: [
|
||||
appMessages,
|
||||
headerMessages,
|
||||
footerMessages,
|
||||
],
|
||||
requireAuthenticatedUser: true,
|
||||
hydrateAuthenticatedUser: true,
|
||||
handlers: {
|
||||
config: () => {
|
||||
@@ -58,7 +72,6 @@ initialize({
|
||||
ALGOLIA_JOBS_INDEX_NAME: process.env.ALGOLIA_JOBS_INDEX_NAME || null,
|
||||
ALGOLIA_PRODUCT_INDEX_NAME: process.env.ALGOLIA_PRODUCT_INDEX_NAME || null,
|
||||
ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY || null,
|
||||
MARKETING_SITE_SEARCH_URL: process.env.SEARCH_CATALOG_URL || null,
|
||||
}, 'App loadConfig override handler');
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
AuthenticatedPageRoute,
|
||||
PageRoute,
|
||||
} from '@edx/frontend-platform/react';
|
||||
import { Switch } from 'react-router-dom';
|
||||
import { ProfilePage, NotFoundPage } from '../profile';
|
||||
import { SkillsBuilder } from '../skills-builder';
|
||||
|
||||
const AppRoutes = () => (
|
||||
<Switch>
|
||||
{getConfig().ENABLE_SKILLS_BUILDER && (
|
||||
<PageRoute path="/skills" component={SkillsBuilder} />
|
||||
)}
|
||||
<AuthenticatedPageRoute path="/u/:username" component={ProfilePage} />
|
||||
<PageRoute path="/notfound" component={NotFoundPage} />
|
||||
<PageRoute path="*" component={NotFoundPage} />
|
||||
</Switch>
|
||||
);
|
||||
|
||||
export default AppRoutes;
|
||||
@@ -1,91 +0,0 @@
|
||||
import React from 'react';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { Router } from 'react-router';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
|
||||
import AppRoutes from './AppRoutes';
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getLoginRedirectUrl: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn(() => ({
|
||||
ENABLE_SKILLS_BUILDER: true,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('../profile', () => ({
|
||||
ProfilePage: () => (<div>Profile page</div>),
|
||||
NotFoundPage: () => (<div>Not found page</div>),
|
||||
}));
|
||||
|
||||
jest.mock('../skills-builder', () => ({
|
||||
SkillsBuilder: () => (<div>Skills Builder</div>),
|
||||
}));
|
||||
|
||||
const RoutesWithProvider = (context, history) => (
|
||||
<AppContext.Provider value={context}>
|
||||
<Router history={history}>
|
||||
<AppRoutes />
|
||||
</Router>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
|
||||
const unauthenticatedUser = {
|
||||
authenticatedUser: null,
|
||||
config: getConfig(),
|
||||
};
|
||||
|
||||
describe('routes', () => {
|
||||
let history;
|
||||
|
||||
beforeEach(() => {
|
||||
history = createMemoryHistory();
|
||||
});
|
||||
|
||||
test('Profile page should redirect for unauthenticated users', () => {
|
||||
history.push('/u/edx');
|
||||
render(
|
||||
RoutesWithProvider(unauthenticatedUser, history),
|
||||
);
|
||||
expect(getLoginRedirectUrl).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Profile page should be accessible for authenticated users', () => {
|
||||
history.push('/u/edx');
|
||||
render(
|
||||
RoutesWithProvider(
|
||||
{
|
||||
authenticatedUser: {
|
||||
username: 'edx',
|
||||
email: 'edx@example.com',
|
||||
},
|
||||
config: getConfig(),
|
||||
},
|
||||
history,
|
||||
),
|
||||
);
|
||||
expect(screen.getByText('Profile page')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('Skills Builder page should be accessible to unauthenticated users', () => {
|
||||
history.push('/skills');
|
||||
render(
|
||||
RoutesWithProvider(unauthenticatedUser, history),
|
||||
);
|
||||
expect(screen.getByText('Skills Builder')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should show NotFound page for a bad route', () => {
|
||||
history.push('/nonMatchingRoute');
|
||||
render(
|
||||
RoutesWithProvider(unauthenticatedUser, history),
|
||||
);
|
||||
expect(screen.getByText('Not found page')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
SET_GOAL,
|
||||
SET_CURRENT_JOB_TITLE,
|
||||
ADD_CAREER_INTEREST,
|
||||
REMOVE_CAREER_INTEREST,
|
||||
REMOVE_CAREER_INTEREEST,
|
||||
} from './constants';
|
||||
|
||||
export const setGoal = (payload) => ({
|
||||
@@ -21,6 +21,6 @@ export const addCareerInterest = (payload) => ({
|
||||
});
|
||||
|
||||
export const removeCareerInterest = (payload) => ({
|
||||
type: REMOVE_CAREER_INTEREST,
|
||||
type: REMOVE_CAREER_INTEREEST,
|
||||
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_INTEREST = 'REMOVE_CAREER_INTEREST';
|
||||
export const REMOVE_CAREER_INTEREEST = 'REMOVE_CAREER_INTEREEST';
|
||||
|
||||
// Stepper keys
|
||||
export const STEP1 = 'select-your-preferences';
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
SET_GOAL,
|
||||
SET_CURRENT_JOB_TITLE,
|
||||
ADD_CAREER_INTEREST,
|
||||
REMOVE_CAREER_INTEREST,
|
||||
REMOVE_CAREER_INTEREEST,
|
||||
} 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_INTEREST:
|
||||
case REMOVE_CAREER_INTEREEST:
|
||||
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_INTEREST,
|
||||
REMOVE_CAREER_INTEREEST,
|
||||
} from '../constants';
|
||||
|
||||
describe('skillsReducer', () => {
|
||||
@@ -48,7 +48,7 @@ describe('skillsReducer', () => {
|
||||
};
|
||||
const returnedState = skillsReducer(
|
||||
testStateWithInterest,
|
||||
{ type: REMOVE_CAREER_INTEREST, payload: newCareerInterestPayload },
|
||||
{ type: REMOVE_CAREER_INTEREEST, payload: newCareerInterestPayload },
|
||||
);
|
||||
const finalState = {
|
||||
...testStateWithInterest,
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 16 KiB |
@@ -1,10 +1,9 @@
|
||||
import React, { useState, useContext } from 'react';
|
||||
import {
|
||||
Button, Container, Stepper, ModalDialog, Form, Hyperlink,
|
||||
Button, Container, Stepper, ModalDialog,
|
||||
} from '@edx/paragon';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useHistory } from 'react-router';
|
||||
import {
|
||||
STEP1, STEP2,
|
||||
} from '../data/constants';
|
||||
@@ -18,36 +17,15 @@ import ViewResults from './view-results/ViewResults';
|
||||
import headerImage from '../images/headerImage.png';
|
||||
|
||||
const SkillsBuilderModal = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const intl = useIntl();
|
||||
const { state } = useContext(SkillsBuilderContext);
|
||||
const { currentGoal, currentJobTitle, careerInterests } = state;
|
||||
const { careerInterests } = state;
|
||||
const [currentStep, setCurrentStep] = useState(STEP1);
|
||||
|
||||
const sendActionButtonEvent = (eventSuffix) => {
|
||||
sendTrackEvent(
|
||||
`edx.skills_builder.${eventSuffix}`,
|
||||
{
|
||||
app_name: 'skills_builder',
|
||||
category: 'skills_builder',
|
||||
learner_data: {
|
||||
current_goal: currentGoal,
|
||||
current_job_title: currentJobTitle,
|
||||
career_interests: careerInterests,
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
const history = useHistory();
|
||||
|
||||
const nextStepHandle = () => {
|
||||
setCurrentStep(STEP2);
|
||||
sendActionButtonEvent('next_step');
|
||||
};
|
||||
const exitButtonHandle = () => {
|
||||
sendActionButtonEvent('exit');
|
||||
};
|
||||
const closeButtonHandle = () => {
|
||||
sendActionButtonEvent('close');
|
||||
window.location.href = getConfig().MARKETING_SITE_SEARCH_URL;
|
||||
const onCloseHandle = () => {
|
||||
history.goBack();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -55,9 +33,9 @@ const SkillsBuilderModal = () => {
|
||||
<ModalDialog
|
||||
title="Skills Builder"
|
||||
size="fullscreen"
|
||||
className="skills-builder-modal bg-light-200"
|
||||
className="skills-builder-modal"
|
||||
isOpen
|
||||
onClose={closeButtonHandle}
|
||||
onClose={onCloseHandle}
|
||||
>
|
||||
<ModalDialog.Hero>
|
||||
<ModalDialog.Hero.Background className="bg-primary-500">
|
||||
@@ -71,38 +49,41 @@ const SkillsBuilderModal = () => {
|
||||
<Stepper.Header />
|
||||
|
||||
<ModalDialog.Body>
|
||||
<Container size="md" className="p-4.5">
|
||||
<Form>
|
||||
<Stepper.Step eventKey={STEP1} title={formatMessage(messages.selectPreferences)}>
|
||||
<SelectPreferences />
|
||||
</Stepper.Step>
|
||||
<Container size="md">
|
||||
<Stepper.Step eventKey={STEP1} title={intl.formatMessage(messages.selectPreferences)}>
|
||||
<SelectPreferences />
|
||||
</Stepper.Step>
|
||||
|
||||
<Stepper.Step eventKey={STEP2} title={formatMessage(messages.reviewResults)}>
|
||||
<ViewResults />
|
||||
</Stepper.Step>
|
||||
</Form>
|
||||
<Stepper.Step eventKey={STEP2} title={intl.formatMessage(messages.reviewResults)}>
|
||||
<ViewResults />
|
||||
</Stepper.Step>
|
||||
</Container>
|
||||
</ModalDialog.Body>
|
||||
|
||||
<ModalDialog.Footer>
|
||||
<Stepper.ActionRow eventKey={STEP1}>
|
||||
<Button variant="outline-primary" onClick={onCloseHandle}>
|
||||
<FormattedMessage {...messages.goBackButton} />
|
||||
</Button>
|
||||
<Stepper.ActionRow.Spacer />
|
||||
<Button
|
||||
onClick={nextStepHandle}
|
||||
onClick={() => setCurrentStep(STEP2)}
|
||||
disabled={careerInterests.length === 0}
|
||||
>
|
||||
{formatMessage(messages.nextStepButton)}
|
||||
<FormattedMessage {...messages.nextStepButton} />
|
||||
</Button>
|
||||
</Stepper.ActionRow>
|
||||
<Stepper.ActionRow eventKey={STEP2}>
|
||||
<Button variant="outline-primary" onClick={() => setCurrentStep(STEP1)}>
|
||||
{formatMessage(messages.goBackButton)}
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
onClick={() => setCurrentStep(STEP1)}
|
||||
>
|
||||
<FormattedMessage {...messages.goBackButton} />
|
||||
</Button>
|
||||
<Stepper.ActionRow.Spacer />
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_SEARCH_URL}>
|
||||
<Button onClick={exitButtonHandle}>
|
||||
{formatMessage(messages.exitButton)}
|
||||
</Button>
|
||||
</Hyperlink>
|
||||
<Button onClick={onCloseHandle}>
|
||||
<FormattedMessage {...messages.exitButton} />
|
||||
</Button>
|
||||
</Stepper.ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</ModalDialog>
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
IconButton, Icon,
|
||||
} from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
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 { formatMessage } = useIntl();
|
||||
const { dispatch } = useContext(SkillsBuilderContext);
|
||||
|
||||
const handleRemoveCareerInterest = () => {
|
||||
dispatch(removeCareerInterest(interest));
|
||||
|
||||
sendTrackEvent(
|
||||
'edx.skills_builder.career_interest.removed',
|
||||
{
|
||||
app_name: 'skills_builder',
|
||||
category: 'skills_builder',
|
||||
learner_data: {
|
||||
career_interest: interest,
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
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={`${formatMessage(messages.removeCareerInterestButtonAltText)} ${interest}`}
|
||||
onClick={handleRemoveCareerInterest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CareerInterestCard.propTypes = {
|
||||
interest: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default CareerInterestCard;
|
||||
@@ -1,65 +1,16 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import {
|
||||
Stack, Row, Col, Form,
|
||||
} from '@edx/paragon';
|
||||
import { Configure, 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 React from 'react';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
|
||||
const CareerInterestSelect = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { state, dispatch, algolia } = useContext(SkillsBuilderContext);
|
||||
const { careerInterests } = state;
|
||||
const { searchClient } = algolia;
|
||||
|
||||
const handleCareerInterestSelect = (value) => {
|
||||
if (!careerInterests.includes(value) && careerInterests.length < 3) {
|
||||
dispatch(addCareerInterest(value));
|
||||
|
||||
sendTrackEvent(
|
||||
'edx.skills_builder.career_interest.added',
|
||||
{
|
||||
app_name: 'skills_builder',
|
||||
category: 'skills_builder',
|
||||
learner_data: {
|
||||
career_interest: value,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={2}>
|
||||
<Form.Label>
|
||||
<h4 className="mb-3">
|
||||
{formatMessage(messages.careerInterestPrompt)}
|
||||
</h4>
|
||||
<InstantSearch searchClient={searchClient} indexName={getConfig().ALGOLIA_JOBS_INDEX_NAME}>
|
||||
<Configure filters="b2c_opt_in:true" />
|
||||
<JobTitleInstantSearch
|
||||
onSelected={handleCareerInterestSelect}
|
||||
placeholder={formatMessage(messages.careerInterestInputPlaceholderText)}
|
||||
data-testid="career-interest-select"
|
||||
/>
|
||||
</InstantSearch>
|
||||
</Form.Label>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
const CareerInterestSelect = () => (
|
||||
<div>
|
||||
<h4>
|
||||
<FormattedMessage {...messages.careerInterestPrompt} />
|
||||
</h4>
|
||||
<p>
|
||||
JobTitleAutosuggest component can be reused here
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default CareerInterestSelect;
|
||||
|
||||
@@ -1,55 +1,37 @@
|
||||
import React, { useContext } from 'react';
|
||||
import {
|
||||
Form,
|
||||
Stack,
|
||||
} from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { setGoal } from '../../data/actions';
|
||||
import { SkillsBuilderContext } from '../../skills-builder-context';
|
||||
import messages from './messages';
|
||||
|
||||
const GoalDropdown = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const intl = useIntl();
|
||||
const { state, dispatch } = useContext(SkillsBuilderContext);
|
||||
const { currentGoal } = state;
|
||||
|
||||
const handleGoalSelect = (e) => {
|
||||
const { value } = e.target;
|
||||
dispatch(setGoal(value));
|
||||
|
||||
sendTrackEvent(
|
||||
'edx.skills_builder.goal.select',
|
||||
{
|
||||
app_name: 'skills_builder',
|
||||
category: 'skills_builder',
|
||||
learner_data: {
|
||||
current_goal: value,
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Label>
|
||||
<h4>
|
||||
{formatMessage(messages.learningGoalPrompt)}
|
||||
</h4>
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
value={currentGoal}
|
||||
onChange={handleGoalSelect}
|
||||
data-testid="goal-select-dropdown"
|
||||
>
|
||||
<option value="" disabled={currentGoal}>{formatMessage(messages.selectLearningGoal)}</option>
|
||||
<option>{formatMessage(messages.learningGoalStartCareer)}</option>
|
||||
<option>{formatMessage(messages.learningGoalAdvanceCareer)}</option>
|
||||
<option>{formatMessage(messages.learningGoalChangeCareer)}</option>
|
||||
<option>{formatMessage(messages.learningGoalSomethingNew)}</option>
|
||||
<option>{formatMessage(messages.learningGoalSomethingElse)}</option>
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
<Stack gap={2}>
|
||||
<h4><FormattedMessage {...messages.learningGoalPrompt} /></h4>
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
as="select"
|
||||
value={currentGoal}
|
||||
onChange={(e) => dispatch(setGoal(e.target.value))}
|
||||
>
|
||||
<option value="">{intl.formatMessage(messages.selectLearningGoal)}</option>
|
||||
<option>{intl.formatMessage(messages.learningGoalStartCareer)}</option>
|
||||
<option>{intl.formatMessage(messages.learningGoalAdvanceCareer)}</option>
|
||||
<option>{intl.formatMessage(messages.learningGoalChangeCareer)}</option>
|
||||
<option>{intl.formatMessage(messages.learningGoalSomethingNew)}</option>
|
||||
<option>{intl.formatMessage(messages.learningGoalSomethingElse)}</option>
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
</Stack>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Form,
|
||||
@@ -13,22 +13,18 @@ const JobTitleInstantSearch = (props) => {
|
||||
|
||||
const handleAutosuggestChange = (value) => {
|
||||
setJobInput(value);
|
||||
refine(value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refine(jobInput);
|
||||
}, [jobInput, refine]);
|
||||
|
||||
return (
|
||||
<Form.Autosuggest
|
||||
value={jobInput}
|
||||
onChange={handleAutosuggestChange}
|
||||
name="job-title-suggest"
|
||||
autoComplete="off"
|
||||
{...props}
|
||||
onSelected={props.onSelected}
|
||||
>
|
||||
{hits.map(job => (
|
||||
<Form.AutosuggestOption key={job.id} id={job.name.replaceAll(' ', '-').toLowerCase()}>
|
||||
<Form.AutosuggestOption key={job.id}>
|
||||
{job.name}
|
||||
</Form.AutosuggestOption>
|
||||
))}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
Form, Stack,
|
||||
} from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
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';
|
||||
import { SkillsBuilderContext } from '../../skills-builder-context';
|
||||
@@ -12,63 +11,31 @@ import JobTitleInstantSearch from './JobTitleInstantSearch';
|
||||
import messages from './messages';
|
||||
|
||||
const JobTitleSelect = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { state, dispatch, algolia } = useContext(SkillsBuilderContext);
|
||||
const { dispatch, algolia } = useContext(SkillsBuilderContext);
|
||||
const { searchClient } = algolia;
|
||||
const { currentJobTitle } = state;
|
||||
|
||||
const handleCurrentJobTitleSelect = (value) => {
|
||||
dispatch(setCurrentJobTitle(value));
|
||||
sendTrackEvent(
|
||||
'edx.skills_builder.current_job.select',
|
||||
{
|
||||
app_name: 'skills_builder',
|
||||
category: 'skills_builder',
|
||||
learner_data: {
|
||||
current_job_title: value,
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (e) => {
|
||||
const { value } = e.target;
|
||||
// only setCurrentJobTitle if the user hasn't selected a current job as we don't want to override their selection
|
||||
if (!currentJobTitle) { dispatch(setCurrentJobTitle(value)); }
|
||||
|
||||
sendTrackEvent(
|
||||
`edx.skills_builder.current_job.${value}`,
|
||||
{
|
||||
app_name: 'skills_builder',
|
||||
category: 'skills_builder',
|
||||
},
|
||||
);
|
||||
};
|
||||
// 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));
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Form.Label>
|
||||
<h4 className="mb-3">
|
||||
{formatMessage(messages.jobTitlePrompt)}
|
||||
</h4>
|
||||
<InstantSearch searchClient={searchClient} indexName={getConfig().ALGOLIA_JOBS_INDEX_NAME}>
|
||||
<JobTitleInstantSearch
|
||||
onSelected={handleCurrentJobTitleSelect}
|
||||
placeholder={formatMessage(messages.jobTitleInputPlaceholderText)}
|
||||
data-testid="job-title-select"
|
||||
/>
|
||||
</InstantSearch>
|
||||
</Form.Label>
|
||||
<Stack gap={2}>
|
||||
<h4>
|
||||
<FormattedMessage {...messages.jobTitlePrompt} />
|
||||
</h4>
|
||||
<InstantSearch searchClient={searchClient} indexName={getConfig().ALGOLIA_JOBS_INDEX_NAME}>
|
||||
<JobTitleInstantSearch onSelected={(value) => dispatch(setCurrentJobTitle(value))} />
|
||||
</InstantSearch>
|
||||
<Form.Group>
|
||||
<Form.CheckboxSet
|
||||
name="other-occupations"
|
||||
onChange={handleCheckboxChange}
|
||||
>
|
||||
<Form.Checkbox value="student">
|
||||
{formatMessage(messages.studentCheckboxPrompt)}
|
||||
<FormattedMessage {...messages.studentCheckboxPrompt} />
|
||||
</Form.Checkbox>
|
||||
<Form.Checkbox value="looking_for_work">
|
||||
{formatMessage(messages.currentlyLookingCheckboxPrompt)}
|
||||
<FormattedMessage {...messages.currentlyLookingCheckboxPrompt} />
|
||||
</Form.Checkbox>
|
||||
</Form.CheckboxSet>
|
||||
</Form.Group>
|
||||
|
||||
@@ -2,35 +2,33 @@ import React, { useContext } from 'react';
|
||||
import {
|
||||
Stack,
|
||||
} from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
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';
|
||||
import messages from './messages';
|
||||
|
||||
const SelectPreferences = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { state } = useContext(SkillsBuilderContext);
|
||||
const { currentGoal, currentJobTitle } = state;
|
||||
|
||||
return (
|
||||
<Stack gap={4}>
|
||||
<Stack gap={5}>
|
||||
<p className="lead">
|
||||
{formatMessage(messages.skillsBuilderDescription)}
|
||||
<FormattedMessage {...messages.skillsBuilderDescription} />
|
||||
</p>
|
||||
<Stack gap={4}>
|
||||
|
||||
<GoalSelect />
|
||||
<GoalSelect />
|
||||
|
||||
{currentGoal && (
|
||||
<JobTitleSelect />
|
||||
)}
|
||||
{currentGoal && (
|
||||
<JobTitleSelect />
|
||||
)}
|
||||
|
||||
{currentGoal && currentJobTitle && (
|
||||
<CareerInterestSelect />
|
||||
)}
|
||||
</Stack>
|
||||
{currentJobTitle && (
|
||||
<CareerInterestSelect />
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -46,11 +46,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Next, search and select your current job title',
|
||||
description: 'Prompts the user to select their current job title or occupation.',
|
||||
},
|
||||
jobTitleInputPlaceholderText: {
|
||||
id: 'job.title.input.placeholder.text',
|
||||
defaultMessage: 'Search and select a job title',
|
||||
description: 'Placeholder text for the job title input control.',
|
||||
},
|
||||
studentCheckboxPrompt: {
|
||||
id: 'student.checkbox.prompt',
|
||||
defaultMessage: 'I\'m a student',
|
||||
@@ -66,15 +61,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'What careers are you interested in?',
|
||||
description: 'Prompts the user to select careers they are interested in pursuing.',
|
||||
},
|
||||
careerInterestInputPlaceholderText: {
|
||||
id: 'career.interest.input.placeholder.text',
|
||||
defaultMessage: 'Select up to 3 new job titles',
|
||||
description: 'Placeholder text for the career interest input control.',
|
||||
},
|
||||
removeCareerInterestButtonAltText: {
|
||||
id: 'career.interest.remove.button.alt.text',
|
||||
defaultMessage: 'Remove career interest: ',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
import {
|
||||
screen, render, cleanup, fireEvent,
|
||||
} from '@testing-library/react';
|
||||
import { mergeConfig } from '@edx/frontend-platform';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { SkillsBuilderWrapperWithContext, dispatchMock, contextValue } from '../../../test/setupSkillsBuilder';
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
sendTrackEvent: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('select-preferences', () => {
|
||||
beforeAll(() => {
|
||||
mergeConfig({
|
||||
ALGOLIA_JOBS_INDEX_NAME: 'test-job-index-name',
|
||||
});
|
||||
});
|
||||
beforeEach(() => cleanup());
|
||||
|
||||
describe('render behavior', () => {
|
||||
it('should render the second prompt if a goal is selected', () => {
|
||||
render(
|
||||
SkillsBuilderWrapperWithContext(
|
||||
{
|
||||
...contextValue,
|
||||
state: {
|
||||
...contextValue.state,
|
||||
currentGoal: 'I want to start my career',
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
const expectedGoal = {
|
||||
payload: 'I want to advance my career',
|
||||
type: 'SET_GOAL',
|
||||
};
|
||||
const expectedStudent = {
|
||||
payload: 'student',
|
||||
type: 'SET_CURRENT_JOB_TITLE',
|
||||
};
|
||||
|
||||
const expectedJobTitle = {
|
||||
payload: 'Prospector',
|
||||
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);
|
||||
|
||||
const jobTitleInput = screen.getByTestId('job-title-select');
|
||||
fireEvent.change(jobTitleInput, { target: { value: 'Prospector' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Prospector' }));
|
||||
|
||||
expect(screen.getByText('Next, search and select your current job title')).toBeTruthy();
|
||||
expect(dispatchMock).toHaveBeenCalledWith(expectedGoal);
|
||||
expect(dispatchMock).toHaveBeenCalledWith(expectedStudent);
|
||||
expect(dispatchMock).toHaveBeenCalledWith(expectedJobTitle);
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith(
|
||||
'edx.skills_builder.goal.select',
|
||||
{
|
||||
app_name: 'skills_builder',
|
||||
category: 'skills_builder',
|
||||
learner_data: {
|
||||
current_goal: 'I want to advance my career',
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith(
|
||||
'edx.skills_builder.current_job.student',
|
||||
{
|
||||
app_name: 'skills_builder',
|
||||
category: 'skills_builder',
|
||||
},
|
||||
);
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith(
|
||||
'edx.skills_builder.current_job.select',
|
||||
{
|
||||
app_name: 'skills_builder',
|
||||
category: 'skills_builder',
|
||||
learner_data: {
|
||||
current_job_title: 'Prospector',
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the third prompt if a current job title is selected', () => {
|
||||
render(
|
||||
SkillsBuilderWrapperWithContext(
|
||||
{
|
||||
...contextValue,
|
||||
state: {
|
||||
...contextValue.state,
|
||||
currentGoal: 'I want to start my career',
|
||||
currentJobTitle: 'Goblin Guide',
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
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'],
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(screen.getByText('Prospector')).toBeTruthy();
|
||||
|
||||
const careerInterestInput = screen.getByTestId('career-interest-select');
|
||||
fireEvent.change(careerInterestInput, { target: { value: 'Mirror Breaker' } });
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Mirror Breaker' }));
|
||||
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith(
|
||||
'edx.skills_builder.career_interest.added',
|
||||
{
|
||||
app_name: 'skills_builder',
|
||||
category: 'skills_builder',
|
||||
learner_data: {
|
||||
career_interest: 'Mirror Breaker',
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(dispatchMock).toHaveBeenCalledWith(
|
||||
{
|
||||
payload: 'Mirror Breaker',
|
||||
type: 'ADD_CAREER_INTEREST',
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('controlled behavior', () => {
|
||||
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);
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith(
|
||||
'edx.skills_builder.career_interest.removed',
|
||||
{
|
||||
app_name: 'skills_builder',
|
||||
category: 'skills_builder',
|
||||
learner_data: {
|
||||
career_interest: 'Prospector',
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
import React from 'react';
|
||||
import { CardCarousel } from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import RecommendationCard from './RecommendationCard';
|
||||
import messages from './messages';
|
||||
|
||||
const CarouselStack = ({ selectedRecommendations }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { id: jobId, name: jobName, recommendations } = selectedRecommendations;
|
||||
const productTypeNames = Object.keys(recommendations);
|
||||
const courseKeys = recommendations.course?.map(rec => ({
|
||||
title: rec.title,
|
||||
courserun_key: rec.active_run_key,
|
||||
}));
|
||||
|
||||
const normalizeProductTypeName = (productType) => {
|
||||
// If the productType is more than one word (i.e. boot_camp)
|
||||
if (productType.includes('_')) {
|
||||
// split to remove underscore and return an array of strings (i.e. ['boot', 'camp'])
|
||||
const splitStrings = productType.split('_');
|
||||
|
||||
// map through the array and normalize each string (i.e. ['Boot', 'Camp'])
|
||||
const normalizeStrings = splitStrings.map(word => word[0].toUpperCase() + word.slice(1).toLowerCase());
|
||||
|
||||
// return the array as a string joined by white spaces (i.e. Boot Camp)
|
||||
return normalizeStrings.join(' ');
|
||||
}
|
||||
// Otherwise, return a normalized string
|
||||
const normalizeString = productType[0].toUpperCase() + productType.slice(1).toLowerCase();
|
||||
return normalizeString;
|
||||
};
|
||||
|
||||
const renderCarouselTitle = (productType) => (
|
||||
<h3>
|
||||
{formatMessage(messages.productRecommendationsHeaderText, {
|
||||
productType: normalizeProductTypeName(productType),
|
||||
jobName,
|
||||
})}
|
||||
</h3>
|
||||
);
|
||||
|
||||
const handleCourseCardClick = (courseKey, productType) => {
|
||||
sendTrackEvent(
|
||||
'edx.skills_builder.recommendation.click',
|
||||
{
|
||||
app_name: 'skills_builder',
|
||||
category: 'skills_builder',
|
||||
page: 'skills_builder',
|
||||
courserun_key: courseKey,
|
||||
product_type: productType,
|
||||
selected_recommendations: {
|
||||
job_id: jobId,
|
||||
job_name: jobName,
|
||||
courserun_keys: courseKeys,
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
productTypeNames.map(productType => (
|
||||
<CardCarousel
|
||||
key={productType}
|
||||
ariaLabel="card carousel"
|
||||
title={renderCarouselTitle(productType)}
|
||||
>
|
||||
{recommendations[productType].map(rec => (
|
||||
<RecommendationCard
|
||||
key={rec.uuid}
|
||||
handleCourseCardClick={handleCourseCardClick}
|
||||
rec={rec}
|
||||
productType={productType}
|
||||
/>
|
||||
))}
|
||||
</CardCarousel>
|
||||
)));
|
||||
};
|
||||
|
||||
export default CarouselStack;
|
||||
@@ -1,60 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Card, Chip, Hyperlink } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import cardImageCapFallbackSrc from '../../images/card-imagecap-fallback.png';
|
||||
|
||||
const RecommendationCard = ({ rec, productType, handleCourseCardClick }) => {
|
||||
const {
|
||||
card_image_url: cardImageUrl,
|
||||
marketing_url: marketingUrl,
|
||||
active_run_key: courseKey,
|
||||
owners,
|
||||
partner,
|
||||
title,
|
||||
} = rec;
|
||||
|
||||
const { logoImageUrl } = owners[0];
|
||||
|
||||
return (
|
||||
<Hyperlink destination={marketingUrl} target="_blank" showLaunchIcon={false}>
|
||||
<Card
|
||||
className="carousel-card"
|
||||
onClick={() => handleCourseCardClick(courseKey, productType)}
|
||||
>
|
||||
<Card.ImageCap
|
||||
src={cardImageUrl}
|
||||
logoSrc={logoImageUrl}
|
||||
fallbackSrc={cardImageCapFallbackSrc}
|
||||
fallbackLogoSrc={cardImageCapFallbackSrc}
|
||||
/>
|
||||
<Card.Header title={title} />
|
||||
<Card.Section>
|
||||
{partner.map((orgName, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Chip key={index}>
|
||||
{orgName}
|
||||
</Chip>
|
||||
))}
|
||||
</Card.Section>
|
||||
</Card>
|
||||
</Hyperlink>
|
||||
);
|
||||
};
|
||||
|
||||
RecommendationCard.propTypes = {
|
||||
rec: PropTypes.shape({
|
||||
title: PropTypes.string,
|
||||
card_image_url: PropTypes.string,
|
||||
marketing_url: PropTypes.string,
|
||||
partner: PropTypes.arrayOf(PropTypes.string),
|
||||
owners: PropTypes.arrayOf(PropTypes.shape({
|
||||
key: PropTypes.string,
|
||||
logoImageUrl: PropTypes.string,
|
||||
})),
|
||||
active_run_key: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
productType: PropTypes.string.isRequired,
|
||||
handleCourseCardClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default RecommendationCard;
|
||||
@@ -1,57 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
SelectableBox, Chip, Stack, useMediaQuery, breakpoints,
|
||||
} from '@edx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
|
||||
const RelatedSkillsSelectableBoxSet = ({ jobSkillsList, selectedJobTitle, onChange }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.extraSmall.maxWidth });
|
||||
|
||||
const renderTopFiveSkills = (skills) => {
|
||||
const topFiveSkills = skills.sort((a, b) => b.significance - a.significance).slice(0, 5);
|
||||
return (
|
||||
topFiveSkills.map(skill => (
|
||||
<Chip key={skill.external_id}>
|
||||
{skill.name}
|
||||
</Chip>
|
||||
))
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SelectableBox.Set
|
||||
name="selected job title"
|
||||
type="radio"
|
||||
value={selectedJobTitle}
|
||||
onChange={onChange}
|
||||
columns={isExtraSmall ? 1 : 3}
|
||||
>
|
||||
{jobSkillsList.map(job => (
|
||||
<SelectableBox
|
||||
key={job.id}
|
||||
type="radio"
|
||||
value={job.name}
|
||||
aria-label={job.name}
|
||||
inputHidden={false}
|
||||
>
|
||||
<p>{job.name}</p>
|
||||
<Stack gap={2} className="align-items-start">
|
||||
<p className="heading-label x-small">{formatMessage(messages.relatedSkillsHeading)}</p>
|
||||
{renderTopFiveSkills(job.skills)}
|
||||
</Stack>
|
||||
</SelectableBox>
|
||||
))}
|
||||
</SelectableBox.Set>
|
||||
);
|
||||
};
|
||||
|
||||
RelatedSkillsSelectableBoxSet.propTypes = {
|
||||
jobSkillsList: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
||||
selectedJobTitle: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default RelatedSkillsSelectableBoxSet;
|
||||
@@ -1,165 +1,7 @@
|
||||
import React, {
|
||||
useContext, useEffect, useState,
|
||||
} from 'react';
|
||||
import {
|
||||
Stack, Row, Alert, Spinner,
|
||||
} from '@edx/paragon';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { CheckCircle, ErrorOutline } from '@edx/paragon/icons';
|
||||
import { SkillsBuilderContext } from '../../skills-builder-context';
|
||||
import RelatedSkillsSelectableBoxSet from './RelatedSkillsSelectableBoxSet';
|
||||
import { searchJobs, getProductRecommendations } from '../../utils/search';
|
||||
import messages from './messages';
|
||||
import { productTypes } from './data/constants';
|
||||
import CarouselStack from './CarouselStack';
|
||||
import React from 'react';
|
||||
|
||||
const ViewResults = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { algolia, state } = useContext(SkillsBuilderContext);
|
||||
const { jobSearchIndex, productSearchIndex } = algolia;
|
||||
const { careerInterests } = state;
|
||||
|
||||
const [selectedJobTitle, setSelectedJobTitle] = useState('');
|
||||
const [jobSkillsList, setJobSkillsList] = useState([]);
|
||||
const [productRecommendations, setProductRecommendations] = useState([]);
|
||||
const [selectedRecommendations, setSelectedRecommendations] = useState({});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [fetchError, setFetchError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const getRecommendations = async () => {
|
||||
// fetch list of jobs with related skills
|
||||
const jobInfo = await searchJobs(jobSearchIndex, careerInterests);
|
||||
|
||||
// fetch course recommendations based on related skills for each job
|
||||
const results = await Promise.all(jobInfo.map(async (job) => {
|
||||
const formattedSkills = job.skills.map(skill => skill.name);
|
||||
|
||||
// create a data object for each job
|
||||
const data = {
|
||||
id: job.id,
|
||||
name: job.name,
|
||||
recommendations: {},
|
||||
};
|
||||
|
||||
// get recommendations for each product type based on the skills for the current job
|
||||
await Promise.all(productTypes.map(async (productType) => {
|
||||
const response = await getProductRecommendations(productSearchIndex, productType, formattedSkills);
|
||||
|
||||
// replace all white spaces with an underscore
|
||||
const formattedProductType = productType.replace(' ', '_');
|
||||
|
||||
// add a new key to the recommendations object and set the value to the response
|
||||
data.recommendations[formattedProductType] = response;
|
||||
}));
|
||||
|
||||
return data;
|
||||
}));
|
||||
|
||||
setJobSkillsList(jobInfo);
|
||||
setSelectedJobTitle(results[0].name);
|
||||
setProductRecommendations(results);
|
||||
setIsLoading(false);
|
||||
sendTrackEvent('edx.skills_builder.recommendation.shown', {
|
||||
app_name: 'skills_builder',
|
||||
category: 'skills_builder',
|
||||
page: 'skills_builder',
|
||||
selected_recommendations: {
|
||||
job_id: results[0].id,
|
||||
job_name: results[0].name,
|
||||
/* We extract the title and course key into an array of objects */
|
||||
courserun_keys: results[0].recommendations.course?.map(rec => ({
|
||||
title: rec.title,
|
||||
courserun_key: rec.active_run_key,
|
||||
})),
|
||||
},
|
||||
is_default: true,
|
||||
});
|
||||
};
|
||||
getRecommendations()
|
||||
.catch(() => {
|
||||
setFetchError(true);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, [careerInterests, jobSearchIndex, productSearchIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedRecommendations(productRecommendations.find(rec => rec.name === selectedJobTitle));
|
||||
}, [productRecommendations, selectedJobTitle]);
|
||||
|
||||
const handleJobTitleChange = (e) => {
|
||||
const { value } = e.target;
|
||||
setSelectedJobTitle(value);
|
||||
const currentSelection = productRecommendations.find(rec => rec.name === value);
|
||||
const { id: jobId, name: jobName, recommendations } = currentSelection;
|
||||
const courseKeys = recommendations.course?.map(rec => ({
|
||||
title: rec.title,
|
||||
courserun_key: rec.active_run_key,
|
||||
}));
|
||||
/*
|
||||
The is_default value will be set to false for any selections made by the user.
|
||||
This code is intentionally duplicated from the event that fires in the useEffect for fetching recommendations.
|
||||
This proved less clunky than refactoring to make things DRY as we have to ensure the first call fires only once.
|
||||
The previous implementation wrapped the event in an additional useEffect that was looping unnecessarily.
|
||||
We have plans to refactor all of the event code as part of APER-2392, where we will revisit this approach.
|
||||
*/
|
||||
sendTrackEvent('edx.skills_builder.recommendation.shown', {
|
||||
app_name: 'skills_builder',
|
||||
category: 'skills_builder',
|
||||
page: 'skills_builder',
|
||||
selected_recommendations: {
|
||||
job_id: jobId,
|
||||
job_name: jobName,
|
||||
courserun_keys: courseKeys,
|
||||
},
|
||||
is_default: false,
|
||||
});
|
||||
};
|
||||
|
||||
if (fetchError) {
|
||||
return (
|
||||
<Alert
|
||||
variant="danger"
|
||||
icon={ErrorOutline}
|
||||
>
|
||||
<Alert.Heading>
|
||||
{formatMessage(messages.matchesNotFoundDangerAlert)}
|
||||
</Alert.Heading>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
isLoading ? (
|
||||
<Row>
|
||||
<Spinner
|
||||
animation="border"
|
||||
screenReaderText="loading"
|
||||
className="mx-auto"
|
||||
/>
|
||||
</Row>
|
||||
) : (
|
||||
<Stack gap={4.5} className="pb-4.5">
|
||||
<Alert
|
||||
variant="success"
|
||||
icon={CheckCircle}
|
||||
>
|
||||
<Alert.Heading>
|
||||
{formatMessage(messages.matchesFoundSuccessAlert)}
|
||||
</Alert.Heading>
|
||||
</Alert>
|
||||
|
||||
<RelatedSkillsSelectableBoxSet
|
||||
jobSkillsList={jobSkillsList}
|
||||
selectedJobTitle={selectedJobTitle}
|
||||
onChange={handleJobTitleChange}
|
||||
/>
|
||||
|
||||
<CarouselStack selectedRecommendations={selectedRecommendations} />
|
||||
</Stack>
|
||||
)
|
||||
);
|
||||
};
|
||||
const ViewResults = () => (
|
||||
<h3>Results will render on this step</h3>
|
||||
);
|
||||
|
||||
export default ViewResults;
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
const COURSE = 'course';
|
||||
|
||||
/* The below strings can be used to demonstrate how we are able to retrieve recommendations for other product types
|
||||
const BOOT_CAMP = 'boot camp';
|
||||
const EXECUTIVE_EDUCATION = 'executive education';
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const productTypes = [
|
||||
COURSE,
|
||||
];
|
||||
@@ -1,31 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
matchesFoundSuccessAlert: {
|
||||
id: 'matches.found.success.alert',
|
||||
defaultMessage: 'We found skills and courses that match your preferences!',
|
||||
description: 'Success alert message to display when recommendations are presented to the learner.',
|
||||
},
|
||||
matchesNotFoundDangerAlert: {
|
||||
id: 'matches.not.found.danger.alert',
|
||||
defaultMessage: 'We were not able to retrieve recommendations at this time. Please try again later.',
|
||||
description: 'Danger alert message to display when the component fails to get recommendations.',
|
||||
},
|
||||
relatedSkillsHeading: {
|
||||
id: 'related.skills.heading',
|
||||
defaultMessage: 'Related Skills',
|
||||
description: 'Heading text for a selectable box that displays related skills for a corresponding selected job title.',
|
||||
},
|
||||
relatedSkillsSelectableBoxLabelText: {
|
||||
id: 'related.skills.selectable.box.label.text',
|
||||
defaultMessage: 'Related skills:',
|
||||
description: 'Label text for a selectable box that displays related skills for a corresponding selected job title.',
|
||||
},
|
||||
productRecommendationsHeaderText: {
|
||||
id: 'product.recommendations.header.text',
|
||||
defaultMessage: '{productType} recommendations for {jobName}',
|
||||
description: 'Header text for a carousel of product recommendations.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,179 +0,0 @@
|
||||
import {
|
||||
screen, render, cleanup, fireEvent, act,
|
||||
} from '@testing-library/react';
|
||||
import { mergeConfig } from '@edx/frontend-platform';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { SkillsBuilderWrapperWithContext, contextValue } from '../../../test/setupSkillsBuilder';
|
||||
import { getProductRecommendations } from '../../../utils/search';
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
sendTrackEvent: jest.fn(),
|
||||
}));
|
||||
|
||||
const renderSkillsBuilderWrapper = (
|
||||
value = {
|
||||
...contextValue,
|
||||
state: {
|
||||
...contextValue.state,
|
||||
currentGoal: 'I want to start my career',
|
||||
currentJobTitle: 'Goblin Lackey',
|
||||
careerInterests: ['Prospector', 'Mirror Breaker', 'Bombardment'],
|
||||
},
|
||||
},
|
||||
) => {
|
||||
render(SkillsBuilderWrapperWithContext(value));
|
||||
};
|
||||
|
||||
describe('view-results', () => {
|
||||
beforeAll(() => {
|
||||
mergeConfig({
|
||||
ALGOLIA_JOBS_INDEX_NAME: 'test-job-index-name',
|
||||
});
|
||||
});
|
||||
|
||||
describe('user interface', () => {
|
||||
beforeEach(async () => {
|
||||
cleanup();
|
||||
// Render the form filled out
|
||||
renderSkillsBuilderWrapper();
|
||||
// Click the next button to trigger "fetching" the data
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Next Step' }));
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render a <JobSillsSelectableBox> for each career interest the learner has submitted', () => {
|
||||
expect(screen.getByText('Prospector')).toBeTruthy();
|
||||
expect(screen.getByText('Mirror Breaker')).toBeTruthy();
|
||||
|
||||
const chipComponents = document.querySelectorAll('.pgn__chip');
|
||||
expect(chipComponents[0].textContent).toEqual('finding shiny things');
|
||||
expect(chipComponents[1].textContent).toEqual('mining');
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith(
|
||||
'edx.skills_builder.recommendation.shown',
|
||||
{
|
||||
app_name: 'skills_builder',
|
||||
category: 'skills_builder',
|
||||
page: 'skills_builder',
|
||||
selected_recommendations: {
|
||||
job_id: 0,
|
||||
job_name: 'Prospector',
|
||||
courserun_keys: [
|
||||
{
|
||||
title: 'Mining with the Mons',
|
||||
courserun_key: 'MONS101',
|
||||
},
|
||||
{
|
||||
title: 'The Art of Warren Upkeep',
|
||||
courserun_key: 'WAR101',
|
||||
},
|
||||
],
|
||||
},
|
||||
is_default: true,
|
||||
},
|
||||
);
|
||||
// called once when "Next Step" button is clicked and then again for above event
|
||||
expect(sendTrackEvent).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('renders a carousel of <Card> components', () => {
|
||||
expect(screen.getByText('Course recommendations for Prospector')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('changes the recommendations based on the selected job title', () => {
|
||||
fireEvent.click(screen.getByRole('radio', { name: 'Mirror Breaker' }));
|
||||
expect(screen.getByText('Course recommendations for Mirror Breaker')).toBeTruthy();
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith(
|
||||
'edx.skills_builder.recommendation.shown',
|
||||
{
|
||||
app_name: 'skills_builder',
|
||||
category: 'skills_builder',
|
||||
page: 'skills_builder',
|
||||
selected_recommendations: {
|
||||
job_id: 1,
|
||||
job_name: 'Mirror Breaker',
|
||||
courserun_keys: [
|
||||
{
|
||||
title: 'Mining with the Mons',
|
||||
courserun_key: 'MONS101',
|
||||
},
|
||||
{
|
||||
title: 'The Art of Warren Upkeep',
|
||||
courserun_key: 'WAR101',
|
||||
},
|
||||
],
|
||||
},
|
||||
is_default: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('sends an event when the "Next Step" button is clicked', () => {
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith(
|
||||
'edx.skills_builder.next_step',
|
||||
{
|
||||
app_name: 'skills_builder',
|
||||
category: 'skills_builder',
|
||||
learner_data: {
|
||||
current_goal: 'I want to start my career',
|
||||
current_job_title: 'Goblin Lackey',
|
||||
career_interests: ['Prospector', 'Mirror Breaker', 'Bombardment'],
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('fires an event when a product recommendation is clicked', () => {
|
||||
fireEvent.click(screen.getByText('Mining with the Mons'));
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith(
|
||||
'edx.skills_builder.recommendation.click',
|
||||
{
|
||||
app_name: 'skills_builder',
|
||||
category: 'skills_builder',
|
||||
page: 'skills_builder',
|
||||
courserun_key: 'MONS101',
|
||||
product_type: 'course',
|
||||
selected_recommendations: {
|
||||
job_id: 0,
|
||||
job_name: 'Prospector',
|
||||
courserun_keys: [
|
||||
{
|
||||
title: 'Mining with the Mons',
|
||||
courserun_key: 'MONS101',
|
||||
},
|
||||
{
|
||||
title: 'The Art of Warren Upkeep',
|
||||
courserun_key: 'WAR101',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetch recommendations', () => {
|
||||
beforeEach(() => {
|
||||
cleanup();
|
||||
// Render the form filled out
|
||||
renderSkillsBuilderWrapper();
|
||||
});
|
||||
|
||||
it('renders an alert if an error is thrown while fetching', async () => {
|
||||
getProductRecommendations.mockImplementationOnce(() => {
|
||||
throw new Error();
|
||||
});
|
||||
|
||||
// Click the next button to trigger "fetching" the data
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Next Step' }));
|
||||
});
|
||||
|
||||
expect(screen.getByText('We were not able to retrieve recommendations at this time. Please try again later.')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,50 @@
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import React from 'react';
|
||||
import {
|
||||
screen, render, act,
|
||||
screen, render, cleanup, fireEvent, act,
|
||||
} from '@testing-library/react';
|
||||
import { mergeConfig } from '@edx/frontend-platform';
|
||||
import { SkillsBuilder } from '..';
|
||||
import { SkillsBuilderProvider } from '../skills-builder-context';
|
||||
import { SkillsBuilderModal } from '../skills-builder-modal';
|
||||
import { SkillsBuilderProvider, SkillsBuilderContext } from '../skills-builder-context';
|
||||
import { skillsInitialState } from '../data/reducer';
|
||||
|
||||
jest.mock('react-instantsearch-hooks-web', () => ({
|
||||
// eslint-disable-next-line react/prop-types
|
||||
InstantSearch: ({ children }) => (<div>{children}</div>),
|
||||
useSearchBox: jest.fn(() => ({ refine: jest.fn() })),
|
||||
useHits: jest.fn(() => ({ hits: [{ name: 'Text File Engineer' }, { name: 'Screen Viewer' }] })),
|
||||
}));
|
||||
|
||||
const dispatchMock = jest.fn();
|
||||
|
||||
const contextValue = {
|
||||
state: {
|
||||
...skillsInitialState,
|
||||
},
|
||||
dispatch: dispatchMock,
|
||||
algolia: {
|
||||
// Without this, tests would fail to destructure `searchClient` in the <JobTitleSelect> component
|
||||
searchClient: {},
|
||||
},
|
||||
};
|
||||
|
||||
const SkillsBuilderWrapperWithContext = (value) => (
|
||||
<IntlProvider locale="en">
|
||||
<SkillsBuilderContext.Provider value={value}>
|
||||
<SkillsBuilderModal />
|
||||
</SkillsBuilderContext.Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
describe('skills-builder', () => {
|
||||
beforeAll(async () => {
|
||||
await mergeConfig({
|
||||
ALGOLIA_JOBS_INDEX_NAME: 'test-job-index-name',
|
||||
});
|
||||
});
|
||||
beforeEach(() => cleanup());
|
||||
|
||||
it('should render a Skills Builder modal with a prompt for the user', () => {
|
||||
act(() => {
|
||||
render(
|
||||
@@ -20,4 +58,39 @@ describe('skills-builder', () => {
|
||||
expect(screen.getByText('Skills Builder')).toBeTruthy();
|
||||
expect(screen.getByText('First, tell us what you want to achieve')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render the second prompt if a goal is selected', () => {
|
||||
render(
|
||||
SkillsBuilderWrapperWithContext(
|
||||
{
|
||||
...contextValue,
|
||||
state: {
|
||||
...contextValue.state,
|
||||
currentGoal: 'I want to start my career',
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(screen.getByText('Next, search and select your current job title')).toBeTruthy();
|
||||
|
||||
const checkbox = screen.getByRole('checkbox', { name: 'I\'m a student' });
|
||||
fireEvent.click(checkbox);
|
||||
expect(dispatchMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should render the third prompt if a goal is selected', () => {
|
||||
render(
|
||||
SkillsBuilderWrapperWithContext(
|
||||
{
|
||||
...contextValue,
|
||||
state: {
|
||||
...contextValue.state,
|
||||
currentGoal: 'I want to start my career',
|
||||
currentJobTitle: 'Goblin Guide',
|
||||
},
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(screen.getByText('What careers are you interested in?')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
export const mockData = {
|
||||
hits: [
|
||||
{
|
||||
id: 0,
|
||||
name: 'Prospector'
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Mirror Breaker'
|
||||
},
|
||||
],
|
||||
searchJobs: [
|
||||
{
|
||||
id: 0,
|
||||
name: 'Prospector',
|
||||
skills: [
|
||||
{ external_id: 0,
|
||||
name: 'mining',
|
||||
significance: 50,
|
||||
},
|
||||
{ external_id: 1,
|
||||
name: 'finding shiny things',
|
||||
significance: 100,
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Mirror Breaker',
|
||||
skills: [
|
||||
{ external_id: 0,
|
||||
name: 'mining',
|
||||
significance: 50,
|
||||
},
|
||||
{ external_id: 1,
|
||||
name: 'finding shiny things',
|
||||
significance: 100,
|
||||
}],
|
||||
},
|
||||
],
|
||||
productRecommendations: [
|
||||
{
|
||||
title: 'Mining with the Mons',
|
||||
uuid: 'thisIsARandomString01',
|
||||
partner: ['edx'],
|
||||
card_image_url: 'https://thisIsAUrl.ForAnImage.01.jpeg',
|
||||
marketing_url: 'https://thisIsAUrl.ForTheRecommendedContent.01.com',
|
||||
active_run_key: 'MONS101',
|
||||
owners: [
|
||||
{
|
||||
logoImageUrl: 'https://thisIsAUrl.ForALogoImage.01.jpeg',
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'The Art of Warren Upkeep',
|
||||
uuid: 'thisIsARandomString02',
|
||||
partner: ['edx'],
|
||||
card_image_url: 'https://thisIsAUrl.ForAnImage.02.jpeg',
|
||||
marketing_url: 'https://thisIsAUrl.ForTheRecommendedContent.02.com',
|
||||
active_run_key: 'WAR101',
|
||||
owners: [
|
||||
{
|
||||
logoImageUrl: 'https://thisIsAUrl.ForALogoImage.02.jpeg',
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
useAlgoliaSearch: [{}, {}, {}],
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import React from 'react';
|
||||
import { SkillsBuilderModal } from '../skills-builder-modal';
|
||||
import { SkillsBuilderContext } from '../skills-builder-context';
|
||||
import { skillsInitialState } from '../data/reducer';
|
||||
import { mockData } from './__mocks__/jobSkills.mockData';
|
||||
import { getProductRecommendations, searchJobs, useAlgoliaSearch } from '../utils/search';
|
||||
|
||||
jest.mock('@edx/frontend-platform/logging');
|
||||
|
||||
jest.mock('react-instantsearch-hooks-web', () => ({
|
||||
// eslint-disable-next-line react/prop-types
|
||||
InstantSearch: ({ children }) => (<div>{children}</div>),
|
||||
Configure: jest.fn(() => (null)),
|
||||
useSearchBox: jest.fn(() => ({ refine: jest.fn() })),
|
||||
useHits: jest.fn(() => ({ hits: mockData.hits })),
|
||||
}));
|
||||
|
||||
jest.mock('../utils/search', () => ({
|
||||
searchJobs: jest.fn(),
|
||||
getProductRecommendations: jest.fn(),
|
||||
useAlgoliaSearch: jest.fn(),
|
||||
}));
|
||||
|
||||
searchJobs.mockReturnValue(mockData.searchJobs);
|
||||
getProductRecommendations.mockReturnValue(mockData.productRecommendations);
|
||||
useAlgoliaSearch.mockReturnValue(mockData.useAlgoliaSearch);
|
||||
|
||||
export const dispatchMock = jest.fn();
|
||||
|
||||
export const contextValue = {
|
||||
state: {
|
||||
...skillsInitialState,
|
||||
},
|
||||
dispatch: dispatchMock,
|
||||
algolia: {
|
||||
// Without this, tests would fail to destructure `searchClient` in the <JobTitleSelect> component
|
||||
searchClient: {},
|
||||
productSearchIndex: {},
|
||||
jobSearchIndex: {},
|
||||
},
|
||||
};
|
||||
|
||||
export const SkillsBuilderWrapperWithContext = (value = contextValue) => (
|
||||
<IntlProvider locale="en">
|
||||
<SkillsBuilderContext.Provider value={value}>
|
||||
<SkillsBuilderModal />
|
||||
</SkillsBuilderContext.Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
@@ -96,7 +96,7 @@ export const getProductRecommendations = async (productIndex, productType, skill
|
||||
const formattedSkillNames = formatFacetFilterData('skills.skill', skills);
|
||||
try {
|
||||
const { hits } = await productIndex.search('', {
|
||||
filters: `product: "${productType}" AND language: "English"`,
|
||||
filters: `product:${productType}`,
|
||||
facetFilters: [
|
||||
formattedSkillNames,
|
||||
],
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('Algolias utility function', () => {
|
||||
|
||||
it('getProductRecommendations() queries Algolia with the expected search parameters', async () => {
|
||||
const expectedSearchParameters = {
|
||||
filters: 'product: "Course" AND language: "English"',
|
||||
filters: 'product:Course',
|
||||
facetFilters: [
|
||||
['skills.skill:Sword Lobbing'],
|
||||
],
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('webpack-dev', {
|
||||
devServer: {
|
||||
allowedHosts: 'all',
|
||||
https: true,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user