From 9d5a89d4c6537e8160c38efd24c0630c845b4624 Mon Sep 17 00:00:00 2001
From: Eemaan Amir <57627710+eemaanamir@users.noreply.github.com>
Date: Fri, 11 Jul 2025 11:09:13 +0500
Subject: [PATCH] feat: removed flag for new profile UI and depreacted old code
(#1233)
---
.env | 1 -
.env.development | 1 -
.env.test | 1 -
src/data/reducers.js | 7 +-
src/data/sagas.js | 6 +-
src/index-v2.scss | 6 -
src/index.jsx | 12 +-
src/index.scss | 2 +-
src/profile-v2/DateJoined.jsx | 29 -
src/profile-v2/NotFoundPage.jsx | 16 -
src/profile-v2/PageLoading.jsx | 18 -
src/profile-v2/ProfilePage.jsx | 463 -
src/profile-v2/ProfilePage.messages.jsx | 26 -
src/profile-v2/ProfilePage.test.jsx | 259 -
.../__mocks__/invalidUser.mockStore.js | 42 -
.../__mocks__/loadingApp.mockStore.js | 42 -
.../__mocks__/savingEditedBio.mockStore.js | 139 -
.../__mocks__/viewOtherProfile.mockStore.js | 105 -
.../__mocks__/viewOwnProfile.mockStore.js | 139 -
.../__snapshots__/ProfilePage.test.jsx.snap | 2019 ---
src/profile-v2/assets/avatar.svg | 9 -
src/profile-v2/assets/dot-pattern-light.png | Bin 38914 -> 0 bytes
src/profile-v2/assets/micro-masters.svg | 13 -
.../assets/professional-certificate.svg | 1 -
.../assets/verified-certificate.svg | 1 -
src/profile-v2/data/actions.js | 137 -
src/profile-v2/data/actions.test.js | 98 -
src/profile-v2/data/constants.js | 33 -
src/profile-v2/data/mock_data.js | 7 -
src/profile-v2/data/pact-profile.test.js | 84 -
src/profile-v2/data/reducers.js | 181 -
src/profile-v2/data/sagas.js | 191 -
src/profile-v2/data/sagas.test.js | 167 -
src/profile-v2/data/selectors.js | 338 -
src/profile-v2/data/services.js | 168 -
src/profile-v2/forms/Bio.jsx | 151 -
src/profile-v2/forms/Bio.messages.jsx | 11 -
src/profile-v2/forms/Country.jsx | 163 -
src/profile-v2/forms/Country.messages.jsx | 16 -
src/profile-v2/forms/Education.jsx | 171 -
src/profile-v2/forms/Education.messages.jsx | 56 -
src/profile-v2/forms/Name.jsx | 192 -
src/profile-v2/forms/Name.messages.jsx | 26 -
src/profile-v2/forms/PreferredLanguage.jsx | 166 -
.../forms/PreferredLanguage.messages.jsx | 16 -
src/profile-v2/forms/ProfileAvatar.jsx | 170 -
.../forms/ProfileAvatar.messages.jsx | 26 -
src/profile-v2/forms/SocialLinks.jsx | 258 -
src/profile-v2/forms/SocialLinks.messages.jsx | 11 -
src/profile-v2/forms/elements/EditButton.jsx | 46 -
.../forms/elements/EditButton.messages.jsx | 11 -
.../forms/elements/EditableItemHeader.jsx | 69 -
.../forms/elements/EmptyContent.jsx | 35 -
.../forms/elements/FormControls.jsx | 84 -
.../forms/elements/FormControls.messages.jsx | 31 -
.../forms/elements/SwitchContent.jsx | 59 -
src/profile-v2/forms/elements/Visibility.jsx | 76 -
.../forms/elements/Visibility.messages.jsx | 16 -
src/profile-v2/index.js | 5 -
src/profile-v2/index.scss | 265 -
src/profile-v2/utils.js | 69 -
src/profile-v2/utils.test.js | 103 -
src/profile/AgeMessage.jsx | 43 -
src/profile/Banner.jsx | 5 -
.../CertificateCard.jsx | 0
src/{profile-v2 => profile}/Certificates.jsx | 0
.../Certificates.messages.jsx | 0
src/profile/DateJoined.jsx | 14 +-
src/profile/NotFoundPage.jsx | 2 +-
src/profile/PageLoading.jsx | 41 +-
src/profile/ProfilePage.jsx | 679 +-
src/profile/ProfilePage.messages.jsx | 10 +
src/profile/ProfilePage.test.jsx | 295 +-
.../UserCertificateSummary.jsx | 0
src/profile/UsernameDescription.jsx | 23 -
.../__mocks__/invalidUser.mockStore.js | 2 +-
src/profile/__mocks__/loadingApp.mockStore.js | 1 +
.../__mocks__/savingEditedBio.mockStore.js | 2 +-
.../__mocks__/viewOtherProfile.mockStore.js | 12 +-
.../__mocks__/viewOwnProfile.mockStore.js | 4 +-
.../__snapshots__/ProfilePage.test.jsx.snap | 11673 +++-------------
src/profile/data/actions.js | 14 -
src/profile/data/actions.test.js | 108 +-
src/{profile-v2 => profile}/data/hooks.js | 2 +-
src/profile/data/pact-profile.test.js | 4 +
src/profile/data/reducers.js | 44 +-
.../data/reducers.test.js | 0
src/profile/data/sagas.js | 40 +-
src/profile/data/sagas.test.js | 4 -
src/profile/data/selectors.js | 68 +-
src/profile/data/services.js | 26 +-
src/profile/forms/Bio.jsx | 223 +-
src/profile/forms/Bio.messages.jsx | 2 +-
src/profile/forms/Certificates.jsx | 231 -
src/profile/forms/Certificates.messages.jsx | 31 -
src/profile/forms/Country.jsx | 252 +-
src/profile/forms/Country.messages.jsx | 4 +-
src/profile/forms/Education.jsx | 268 +-
src/profile/forms/LearningGoal.jsx | 92 -
src/profile/forms/LearningGoal.messages.jsx | 31 -
src/profile/forms/LearningGoal.test.jsx | 116 -
src/profile/forms/Name.jsx | 267 +-
src/profile/forms/Name.messages.jsx | 19 +-
src/profile/forms/PreferredLanguage.jsx | 253 +-
.../forms/PreferredLanguage.messages.jsx | 2 +-
src/profile/forms/ProfileAvatar.jsx | 252 +-
src/profile/forms/ProfileAvatar.messages.jsx | 10 +
src/profile/forms/SocialLinks.jsx | 457 +-
src/profile/forms/SocialLinks.test.jsx | 165 -
.../__snapshots__/SocialLinks.test.jsx.snap | 504 -
src/profile/forms/elements/EditButton.jsx | 37 +-
.../forms/elements/EditableItemHeader.jsx | 47 +-
src/profile/forms/elements/EmptyContent.jsx | 6 +-
src/profile/forms/elements/FormControls.jsx | 74 +-
src/profile/forms/elements/SwitchContent.jsx | 5 -
src/profile/forms/elements/Visibility.jsx | 4 +-
src/profile/index.scss | 178 +-
src/profile/utils.js | 2 -
src/routes/AppRoutes.jsx | 20 +-
src/routes/routes.test.jsx | 5 -
120 files changed, 3662 insertions(+), 20074 deletions(-)
delete mode 100755 src/index-v2.scss
delete mode 100644 src/profile-v2/DateJoined.jsx
delete mode 100644 src/profile-v2/NotFoundPage.jsx
delete mode 100644 src/profile-v2/PageLoading.jsx
delete mode 100644 src/profile-v2/ProfilePage.jsx
delete mode 100644 src/profile-v2/ProfilePage.messages.jsx
delete mode 100644 src/profile-v2/ProfilePage.test.jsx
delete mode 100644 src/profile-v2/__mocks__/invalidUser.mockStore.js
delete mode 100644 src/profile-v2/__mocks__/loadingApp.mockStore.js
delete mode 100644 src/profile-v2/__mocks__/savingEditedBio.mockStore.js
delete mode 100644 src/profile-v2/__mocks__/viewOtherProfile.mockStore.js
delete mode 100644 src/profile-v2/__mocks__/viewOwnProfile.mockStore.js
delete mode 100644 src/profile-v2/__snapshots__/ProfilePage.test.jsx.snap
delete mode 100644 src/profile-v2/assets/avatar.svg
delete mode 100644 src/profile-v2/assets/dot-pattern-light.png
delete mode 100644 src/profile-v2/assets/micro-masters.svg
delete mode 100644 src/profile-v2/assets/professional-certificate.svg
delete mode 100644 src/profile-v2/assets/verified-certificate.svg
delete mode 100644 src/profile-v2/data/actions.js
delete mode 100644 src/profile-v2/data/actions.test.js
delete mode 100644 src/profile-v2/data/constants.js
delete mode 100644 src/profile-v2/data/mock_data.js
delete mode 100644 src/profile-v2/data/pact-profile.test.js
delete mode 100644 src/profile-v2/data/reducers.js
delete mode 100644 src/profile-v2/data/sagas.js
delete mode 100644 src/profile-v2/data/sagas.test.js
delete mode 100644 src/profile-v2/data/selectors.js
delete mode 100644 src/profile-v2/data/services.js
delete mode 100644 src/profile-v2/forms/Bio.jsx
delete mode 100644 src/profile-v2/forms/Bio.messages.jsx
delete mode 100644 src/profile-v2/forms/Country.jsx
delete mode 100644 src/profile-v2/forms/Country.messages.jsx
delete mode 100644 src/profile-v2/forms/Education.jsx
delete mode 100644 src/profile-v2/forms/Education.messages.jsx
delete mode 100644 src/profile-v2/forms/Name.jsx
delete mode 100644 src/profile-v2/forms/Name.messages.jsx
delete mode 100644 src/profile-v2/forms/PreferredLanguage.jsx
delete mode 100644 src/profile-v2/forms/PreferredLanguage.messages.jsx
delete mode 100644 src/profile-v2/forms/ProfileAvatar.jsx
delete mode 100644 src/profile-v2/forms/ProfileAvatar.messages.jsx
delete mode 100644 src/profile-v2/forms/SocialLinks.jsx
delete mode 100644 src/profile-v2/forms/SocialLinks.messages.jsx
delete mode 100644 src/profile-v2/forms/elements/EditButton.jsx
delete mode 100644 src/profile-v2/forms/elements/EditButton.messages.jsx
delete mode 100644 src/profile-v2/forms/elements/EditableItemHeader.jsx
delete mode 100644 src/profile-v2/forms/elements/EmptyContent.jsx
delete mode 100644 src/profile-v2/forms/elements/FormControls.jsx
delete mode 100644 src/profile-v2/forms/elements/FormControls.messages.jsx
delete mode 100644 src/profile-v2/forms/elements/SwitchContent.jsx
delete mode 100644 src/profile-v2/forms/elements/Visibility.jsx
delete mode 100644 src/profile-v2/forms/elements/Visibility.messages.jsx
delete mode 100644 src/profile-v2/index.js
delete mode 100644 src/profile-v2/index.scss
delete mode 100644 src/profile-v2/utils.js
delete mode 100644 src/profile-v2/utils.test.js
delete mode 100644 src/profile/AgeMessage.jsx
delete mode 100644 src/profile/Banner.jsx
rename src/{profile-v2 => profile}/CertificateCard.jsx (100%)
rename src/{profile-v2 => profile}/Certificates.jsx (100%)
rename src/{profile-v2 => profile}/Certificates.messages.jsx (100%)
rename src/{profile-v2 => profile}/UserCertificateSummary.jsx (100%)
delete mode 100644 src/profile/UsernameDescription.jsx
rename src/{profile-v2 => profile}/data/hooks.js (93%)
rename src/{profile-v2 => profile}/data/reducers.test.js (100%)
delete mode 100644 src/profile/forms/Certificates.jsx
delete mode 100644 src/profile/forms/Certificates.messages.jsx
delete mode 100644 src/profile/forms/LearningGoal.jsx
delete mode 100644 src/profile/forms/LearningGoal.messages.jsx
delete mode 100644 src/profile/forms/LearningGoal.test.jsx
delete mode 100644 src/profile/forms/SocialLinks.test.jsx
delete mode 100644 src/profile/forms/__snapshots__/SocialLinks.test.jsx.snap
diff --git a/.env b/.env
index f73d2e4..4af7473 100644
--- a/.env
+++ b/.env
@@ -31,5 +31,4 @@ SEARCH_CATALOG_URL=''
ENABLE_SKILLS_BUILDER_PROFILE=''
# Fallback in local style files
PARAGON_THEME_URLS={}
-ENABLE_NEW_PROFILE_VIEW=''
DISABLE_VISIBILITY_EDITING=''
diff --git a/.env.development b/.env.development
index a9a5e96..fa5041d 100644
--- a/.env.development
+++ b/.env.development
@@ -32,5 +32,4 @@ SEARCH_CATALOG_URL='http://localhost:18000/courses'
ENABLE_SKILLS_BUILDER_PROFILE=''
# Fallback in local style files
PARAGON_THEME_URLS={}
-ENABLE_NEW_PROFILE_VIEW=''
DISABLE_VISIBILITY_EDITING=''
diff --git a/.env.test b/.env.test
index 839f7fe..15f0042 100644
--- a/.env.test
+++ b/.env.test
@@ -26,5 +26,4 @@ COLLECT_YEAR_OF_BIRTH=true
APP_ID=''
MFE_CONFIG_API_URL=''
PARAGON_THEME_URLS={}
-ENABLE_NEW_PROFILE_VIEW=''
DISABLE_VISIBILITY_EDITING=''
diff --git a/src/data/reducers.js b/src/data/reducers.js
index 98b95d2..fa8f1bf 100755
--- a/src/data/reducers.js
+++ b/src/data/reducers.js
@@ -1,14 +1,9 @@
import { combineReducers } from 'redux';
-import { getConfig } from '@edx/frontend-platform';
-
import { reducer as profilePageReducer } from '../profile';
-import { reducer as newProfilePageReducer } from '../profile-v2';
-
-const isNewProfileEnabled = getConfig().ENABLE_NEW_PROFILE_VIEW;
const createRootReducer = () => combineReducers({
- profilePage: isNewProfileEnabled ? newProfilePageReducer : profilePageReducer,
+ profilePage: profilePageReducer,
});
export default createRootReducer;
diff --git a/src/data/sagas.js b/src/data/sagas.js
index fab5eca..91762b8 100644
--- a/src/data/sagas.js
+++ b/src/data/sagas.js
@@ -1,12 +1,8 @@
import { all } from 'redux-saga/effects';
-import { getConfig } from '@edx/frontend-platform';
import { saga as profileSaga } from '../profile';
-import { saga as newProfileSaga } from '../profile-v2';
-
-const isNewProfileEnabled = getConfig().ENABLE_NEW_PROFILE_VIEW;
export default function* rootSaga() {
yield all([
- isNewProfileEnabled ? newProfileSaga() : profileSaga(),
+ profileSaga(),
]);
}
diff --git a/src/index-v2.scss b/src/index-v2.scss
deleted file mode 100755
index d120211..0000000
--- a/src/index-v2.scss
+++ /dev/null
@@ -1,6 +0,0 @@
-@use "@openedx/paragon/styles/css/core/custom-media-breakpoints" as paragonCustomMediaBreakpoints;
-
-@import "~@edx/frontend-component-header/dist/index";
-@import "~@edx/frontend-component-footer/dist/footer";
-
-@import './profile-v2/index';
diff --git a/src/index.jsx b/src/index.jsx
index 44779d4..0b00264 100755
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -7,7 +7,6 @@ import {
initialize,
mergeConfig,
subscribe,
- getConfig,
} from '@edx/frontend-platform';
import {
AppProvider,
@@ -28,20 +27,16 @@ import Head from './head/Head';
import AppRoutes from './routes/AppRoutes';
+import './index.scss';
+
const rootNode = createRoot(document.getElementById('root'));
subscribe(APP_READY, async () => {
- const isNewProfileEnabled = getConfig().ENABLE_NEW_PROFILE_VIEW === 'true';
- if (isNewProfileEnabled) {
- await import('./index-v2.scss');
- } else {
- await import('./index.scss');
- }
rootNode.render(
-
- {params.username} -
- {isBlockVisible(name) && ( -- {name} -
- )} -
- {isMobileView ? (
-
- {intl.formatMessage(messages['profile.username'])} -
-- {intl.formatMessage(messages['profile.username.tooltip'])} -
- - )} - > -- Profile information -
-- Username -
- -- Profile information -
-- Username -
- -- Full name -
- -- Country -
-- Primary language spoken -
-- Education -
-- Bio -
-- X -
-- Profile information -
-- Username -
- -- Full name -
- -- Country -
-- Primary language spoken -
-- Education -
-- Bio -
-- X -
-- Facebook -
-- LinkedIn -
-- Your certificates -
-- Your learner records information is only visible to you. Only your username and profile image are visible to others on localhost. -
-- Verified Certificate -
-- edX Demonstration Course -
-- From -
-- Completed on - 3/4/2019 -
-- Credential ID -
-- Profile information -
-- Username -
- -- Full name -
- -- Country -
-- Primary language spoken -
-- Education -
-- Bio -
-- X -
-- Facebook -
-- LinkedIn -
-- Your certificates -
-- Your learner records information is only visible to you. Only your username and profile image are visible to others on localhost. -
-- Verified Certificate -
-- edX Demonstration Course -
-- From -
-- Completed on - 3/4/2019 -
-- Credential ID -
-J14Dcafee1i2{=uVPcF=>(xs*@5v znRbMM9||0jfFqEOWvPP(ES7?APbX+J1R42j^8aOEP(^`j_El@vgVxV5PKL!jVbqtv z3%*@J4+d*ZSs=@4H=I?Q8gC@CY`j*98#wk!mq=M}XcxnqClYlmj&jHVPM{qFN=?<8 z#>{)sM9@5Hz+x1kANbE)Tl`DQaP;2Z%F!CyC+omjSKgIeQ2iQE>NbRjgyIYAKVUih z@I5K-r{lZ+0$3VlB~&X)CuJUg)}Lh`=p61%3;2aEiI`p}@0XgarbKNud3s+K=ut{> z`K`8WAwq8W4{J2vfVXc*EB`G*OliqI_KOSe>_Tl*%By-X0RxV8B~ktSN9_&7<~ A86G)?Hen-QS#jf%X$1Q+&j76Wbym^lbI!*&WpKLH@ z$je2_?mGsN*VwMc6YSEqBl{<9>##A4fcM)klF=rG=S~aUNwR&j$M(hqk#l&UkBMn3 z+c+Y#mFCPt2?_L}vL2w^sC5fP#lQy aW>zmc>h|3O&)x9#BnEMxW?@L_%W Wko3^izS zy=Th`VgtuX0X=yf^iz*-quAZqPZkHrF43$h+S6B4w7g!f?d_hO()DTa6lV$_{4M{s zNmCh&OsI$TnyjKOj{^19%=Sd>Hr>mIfIQ;5`)1_kGlSyxb#j=Q69a4A#th?z9VNXE zA1(oZLkOI2@*-s;uWbM1I`7^~vB1Zp_mj2NIMOMa VK*f1oKi_G8y2S<_c)y!nD-j90#`!!`givd9%o zT{z-5SjMJc`km?VpoI=xp9p2!FjE82R|s^@r=Xyp*wcfq)?dxREV9MPV_v)X!L1J7 z3<#<{PPr}PX8RoWH7U3Z?hTtgcWqg+pUmn6O-70FO|S!1&q{>I(n{1kw&y6Ik)SCH zt#1^cm>%xgnR|~adXjHA`bH#%%m_^T?&e@rRvE|#^#;>nV<|vIL`*foo~CyX0KptI z5rQTIEj_EP-K8$UIDVoVIK2o !|j7?uR^Bbf~^fN z$|6vvA4(U3T>uXSC;4Cngb o&%MYPpGRv>N8WXedhgxA0n0Rc7PjK>(9$> zw4BDvDbCWg@$s<#UKlDoWqJv@cr~p2a;7Wxcv9F-5hy8xQ$urJ<9;$JPYTuSYY=3; zIwG6TQl8CO&7tYlbWg3u`#o3l-3;`7_B!-U5ii_l&xcxxbTqMn3Hv00P=mA D25s?qzl}`e&+Ha-Q$;cO(c7^aM6lSY z9Y^b{WPqF#n;HTdpTe$Q<0vURQAFF$43hHDl!56wT5rLVNG7-*INxEy={In%jVGC+ z!dhi8Q+PN5%1HJ^AbgXHtzfU?bp0{0p)RJfB{6cMt8@Lxt}s`(d71jwH-DbLR?YwZ z XZsmG2UdyeKmE?S1JStIfmv4;GJmT^B@8K?%%A z$38l!#mp*le( $0uusJu_rr|MJcKn4rOxPKK>VTysCcsq58Ui@X(1|N3&!fcX-|j{DloOZAr+S zxe=xx)BlFY(7SbRTk$>uGYoKger;Z`%_3U~GwkRT)@c@9-P&^BQF~5L-1dz94+P^e zINfyTpInqly0w^_&dTx2NcpDfCp#Lo%FvVE4$X_iS6@K(GOqXKGw?J5CqU)JWmmVO z=;@FFR#~y=6M}9Qy@{>QqPDcQH)vF5J?K+bU H6KV^5uWxc_&Q?{Mn0Op9Q}J7^l-G zNPi)fY})&_zc}OevFq-(5tBCY|Hi6{n0?g9Q-|~OlaYR*DR1gt@QYR=%Nd7H;
-}C%h-;yc7auG zBNOWls=^0+(Ma>e6o?eYO}|##H@w!oRSnhBEMvL(Y=uvUTq?VKAM1GcjkUwxyq$ty zAAdB*XdUF1F$K8Hv0i_Ake;7?E-lL%PEJtsTg+tGb%}-qmtp=>N=(Y0zQG05(G@ox z A&?ox;kjQ42vQ%<_~^uP4afHN)d)4;J{l6mirm> MD3G2 zf#q>KusJst2h-M$kh&&cmKo5*Brzd$> zS283cxEnvVeVjfPPrfd3vtWJ)Hr?` tRZcejqn;mSD*Y;Ea _^*P_K{}=&Yc3v8K@Ocx- zIw-Nr{sdhV@Sxp;_Ji&wkVWpCzq>b1Ks8%^H6vpyBFxDKzvmacf%zOjk8NM^ZkyQH zV)o03? HQC~8*AybloM^ehFCM>I&?nt@3B&`w7h_I5II}PK9A`{}RoYQa z_t?bMrfEDeNxv7vA3Il6nHkv+7G#-u?-jtPaoVmvyYxZX3v@2={d;emji_ev@$U>| zFR;y;)B$B6l-$_e(2mTY0B-$#mdAQEzL9beCT%>S@y*b{DfoK<_fc^dqxG=hQ%eCx zeus#Ven)lM;9lA4x&AZ9sQw*(C)j!Z@V6;bh{_}tD_8Dm8}Ur2$FX{&-m!5?Cp#31 zJ(8H`iqZ&n!RLsn5izWYV@Qw6FCUM0+~7+lD*1NeMt(ihal!EJG7tyQoN-Pwp>oa! zUpJDWbc#(?pI(@y=$sW5a-v4i_41&sL7&dmzk`rS#|684|Bi=$u(733Eqb^`XU@jg z8pxi(p%xbmZU(b*eypFc3~wPQmRlw=Ro2K-PF(|E*`*cD;WRJr{w hDdT1GGuqg5ZX1P4}E;e2yUVxf=7t!8C;?rEhM*IF`xwUa*muWRA7B zFx7zGQF!n-{=VmGTftpNb9QBJ;5t!w&pG#~JT+=@RZ~+qtjtWA@QKoUmt95x6lI>Z zi_M@PwZo6==LI=ESaaxh-W%UT)IwLlYBN-}zaLO6!+rj1-5Xzq?Z&%(GNdTksA!hc z)k~Q}pnmg_Q3FCrL~PgLWn9#b8gucb>m@O)6##FnQwerQ8le&Wwmg`|zY)}5Cn$o+ z)G5`~kEnCD;K4!})vo=DYN7${qVtVYxwPU@$SR(XwA&^DRUXLoBlud6ajkkM=gG4J zHqY6sK-r|wv=!xcdSFsdNN;?xv)`E6=6~NGf|tpSy@;8DT#j}}M0c#4xDc>X?r_bw z43{|u$=>qmkQR*{Bd~ee=KXm#Z&^nHVqp~8{s+?4Cisb~dtnUDSuo-LSVWra7O6`6 zMSYy?KDL`bc*A(c7&BWiN9Xx+`xFl~D!5utUI!kSi$%Z)9Ta>{qJEP-uILS$0=yTx zR_|iEfnH0e?($RBE|YT{W4oFxB2ifVo%ylQ>A#k}(y>Jc+Y^>)aMV%{fF-0KIAJ#7 z-vGO1HYvTES3p}$l-a6a)wx0)1uVnCGz8eBB^kjDpW*{KRE^#_2>6BC_MX*cfL18m zLCS8%T1Q(x=&(9lciC4NBdd4#f=PVp8Km1lq1{>nnAu%aFrB6LQ%3agW!Iya)pIsj z^|LRAFWCbxj$pumor<&1U*qPm9z1B{TOI^_$ANj49zKl*ubFA>ffKnc$>dh^Uryn0 z)w2rQm)q^9v!>!qBx^zkDz?&go($c1%(_qgBoD0l%V!gpZli;atcPJ4BNr>7*yB?6 z{iC3Pp8yE%a*#e|&<}gHxXYTS(vpsFgXB}R8Z>g;L+0`K2InIHF7Q#_L%XuE33D>? z?aQ8pjHdf1lV>5Lb(xG~`j}6M<8;#{!V<$&8Rm`y%w?X75kxh>R<5QEmzSm#wF$`{ zB>$J<#ZP-NF1=Z%5tBB=Hzxb+ar2ix>kp+94~kx&lM~&pdEUHLw~ieA`WSq_e=hWR zxTxnL^YQ%Ib*CRZF)N}tRnLNn R3_Q)W!l(*vUFXARnGng zER%zD#jM~WVDL8-gO3`}`y*x@BsgdhrH!K2m*eTMbFzGR %oq6< z!=)KOfZO`%koV^%_-L^+lnF-XyFM-`aNau`O2En1;o={GLOj{^SVvwz`4spY(c*O` zf(FZ|b%0PAu>ERqpl77b(qP58mwPYVE;<$@*a!~KT$J1W+{RV|Gs~%81_ci@z*SK& zf?ope)#l>n{-_y1Ot+fKo(JC@_Bn0DKJXjm-sP-N_$wnCn~3fx26ScMF0PLu^PM&R zOw|{{Mc?8+hA`w*yP+ip6>{pAOPEdjZiOg1g?zENO10{d?ZSX9+k0?{5U R|W1_ zg-8Rx?FLix=yv^ldgouG$S-ujzfXqT51KV_z}+DwS&xj?zM-X68;3tofx4QpKInAO z2HgWWQ;ib81h_RoDI;q`3tn&r$xYn1ocM9Vr%4y=$<&)U;~fkU=3*0^`v1VgKq5?t z?W8H&?o^c6Wm3qut<-ss4V6ht;#kKB@n%(PeUHd3bI@C+5p~KPwlI8Wcke?ISNZl# zq88k^NNYy-IB;VlcFAhm?l%1dsvvrACBA>f_KD^5H 0Dy` b4gWl6o~&eACt_C3WX@IHPpBgF1px%no$+1?gp_Q8>0_d!r~ ztvZ$kUZ3U7?-z>|Gw+~hf?T>j$Y11XzI_%nV;%BQEsuJMSyVip5fm1u Kv(`IEyY5Dt7^y2$IJH-hxw0{G ~-0C#5A-s(KxUz#SgvgYW0F zj72lwS6&5KW#m7(EW0QNkbws6=|e<`-hhMj7*8iSPVxQ+A`sRy_Hp59c!T92fZS@_ z1y3U*_7r^l<2i18=B+O $bKZuDp79+8Fz8Hmk(_%u0eJ6e z9&99;v6w%_54)5uuN| g~{Fz)}F7m#EU#owrzj;kK2= z-A`vWCV0id<{J)w+ZelX`Xi7uh|4F3h*5z;$%vrYP%tYtj5$_r#G=+#&e0)pE1S~^ z!))M7Q{O-FksN0?r)Ipmf>iZMH0~gMLw60Jt<%2vBQQmc^c?ieN88MAl>WemeKkhK zr}Sa(Jad_bacHmR^nB(8Yb*j&6!gbJS3xXkzd#O3i`Q-gzFb`h0yJq3p|-pThM=bB z5oU)l{S9gZhMCY+9Dx^)D9swliKD>hS$VB&{h1}a@ yG9AHwn z%kI1a3e(G1fsf0nV;`A*%H6|AG~HqjlL^m|Q#i>S&EW1zfL1uG(*lsKO`+AT{Xi)K z+^XwnCfJV>TeQ4MuCwi&+ywU-@T>e4dw_yJ 6N{YmFK!ikWuynW@NrxC9Vi z=K}rE?2|x!117yjzfXF>?ELmw%=qZ1*KL0THIvrO@AI2CxWfjZc*|b{>Jm~_&fGtP zUI3gUXdg~t_U$ZO<~Jj0ee agmg%vi=fuLXb* z`MS|zlP6*jClux8RJL5JK~9iRJVL=w;0H<_XH90Ip~Bs0XUoJ{)A;g?N-$8Xoi0J* z$68~&ej50|gEJLC_OdtZ&W!7Qdi~&y(k3LJ>^` oG7r-kSSoJ>LYF1ZN7j}9HQniE>gNVo&t{KEa 0RXOe)u@$Z&<%VO&?%fpaXGi- v&R#x&Ie-c*h-c1Ujg*d%6T<-wph=5w z#JIq#5a4_xs9E(E%K{9{oCumW$xR|A&+gLO53GLwubYTv1{LUp?VHVn5E)aT5x5{| z{%z2XX0?<8S0uMf=uU8te+k+h3QOE+sz}A)r3hfvTDi7&{+#(&jWUrYso`di1O^}L zr-$`FfTw}MJnOI v;D*FRpt1y|kZ$#N*AKYyE72J^ZPtDqWrivWm^?23lAa6q%>h?%w}X|K zCJMO6VPb^2WFhB5{Ilm-Na+afXCTAC3m6a@X5xs}%Z&B|pfw5Bn5!tT - \ No newline at end of file diff --git a/src/profile-v2/assets/professional-certificate.svg b/src/profile-v2/assets/professional-certificate.svg deleted file mode 100644 index 2940d10..0000000 --- a/src/profile-v2/assets/professional-certificate.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/profile-v2/assets/verified-certificate.svg b/src/profile-v2/assets/verified-certificate.svg deleted file mode 100644 index 2940d10..0000000 --- a/src/profile-v2/assets/verified-certificate.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/profile-v2/data/actions.js b/src/profile-v2/data/actions.js deleted file mode 100644 index b960400..0000000 --- a/src/profile-v2/data/actions.js +++ /dev/null @@ -1,137 +0,0 @@ -import { AsyncActionType } from '../utils'; - -export const FETCH_PROFILE = new AsyncActionType('PROFILE', 'FETCH_PROFILE'); -export const SAVE_PROFILE = new AsyncActionType('PROFILE', 'SAVE_PROFILE'); -export const SAVE_PROFILE_PHOTO = new AsyncActionType('PROFILE', 'SAVE_PROFILE_PHOTO'); -export const DELETE_PROFILE_PHOTO = new AsyncActionType('PROFILE', 'DELETE_PROFILE_PHOTO'); -export const OPEN_FORM = 'OPEN_FORM'; -export const CLOSE_FORM = 'CLOSE_FORM'; -export const UPDATE_DRAFT = 'UPDATE_DRAFT'; -export const RESET_DRAFTS = 'RESET_DRAFTS'; - -export const fetchProfile = username => ({ - type: FETCH_PROFILE.BASE, - payload: { username }, -}); - -export const fetchProfileBegin = () => ({ - type: FETCH_PROFILE.BEGIN, -}); - -export const fetchProfileSuccess = ( - account, - preferences, - courseCertificates, - isAuthenticatedUserProfile, -) => ({ - type: FETCH_PROFILE.SUCCESS, - account, - preferences, - courseCertificates, - isAuthenticatedUserProfile, -}); - -export const fetchProfileReset = () => ({ - type: FETCH_PROFILE.RESET, -}); - -export const saveProfile = (formId, username) => ({ - type: SAVE_PROFILE.BASE, - payload: { - formId, - username, - }, -}); - -export const saveProfileBegin = () => ({ - type: SAVE_PROFILE.BEGIN, -}); - -export const saveProfileSuccess = (account, preferences) => ({ - type: SAVE_PROFILE.SUCCESS, - payload: { - account, - preferences, - }, -}); - -export const saveProfileReset = () => ({ - type: SAVE_PROFILE.RESET, -}); - -export const saveProfileFailure = errors => ({ - type: SAVE_PROFILE.FAILURE, - payload: { errors }, -}); - -export const saveProfilePhoto = (username, formData) => ({ - type: SAVE_PROFILE_PHOTO.BASE, - payload: { - username, - formData, - }, -}); - -export const saveProfilePhotoBegin = () => ({ - type: SAVE_PROFILE_PHOTO.BEGIN, -}); - -export const saveProfilePhotoSuccess = profileImage => ({ - type: SAVE_PROFILE_PHOTO.SUCCESS, - payload: { profileImage }, -}); - -export const saveProfilePhotoReset = () => ({ - type: SAVE_PROFILE_PHOTO.RESET, -}); - -export const saveProfilePhotoFailure = error => ({ - type: SAVE_PROFILE_PHOTO.FAILURE, - payload: { error }, -}); - -export const deleteProfilePhoto = username => ({ - type: DELETE_PROFILE_PHOTO.BASE, - payload: { - username, - }, -}); - -export const deleteProfilePhotoBegin = () => ({ - type: DELETE_PROFILE_PHOTO.BEGIN, -}); - -export const deleteProfilePhotoSuccess = profileImage => ({ - type: DELETE_PROFILE_PHOTO.SUCCESS, - payload: { profileImage }, -}); - -export const deleteProfilePhotoReset = () => ({ - type: DELETE_PROFILE_PHOTO.RESET, -}); - -export const openForm = formId => ({ - type: OPEN_FORM, - payload: { - formId, - }, -}); - -export const closeForm = formId => ({ - type: CLOSE_FORM, - payload: { - formId, - }, -}); - -export const updateDraft = (name, value) => ({ - type: UPDATE_DRAFT, - payload: { - name, - value, - }, -}); - -export const resetDrafts = () => ({ - type: RESET_DRAFTS, -}); diff --git a/src/profile-v2/data/actions.test.js b/src/profile-v2/data/actions.test.js deleted file mode 100644 index 275d695..0000000 --- a/src/profile-v2/data/actions.test.js +++ /dev/null @@ -1,98 +0,0 @@ -import { - SAVE_PROFILE_PHOTO, - saveProfilePhotoBegin, - saveProfilePhotoSuccess, - saveProfilePhotoFailure, - saveProfilePhotoReset, - saveProfilePhoto, - DELETE_PROFILE_PHOTO, - deleteProfilePhotoBegin, - deleteProfilePhotoSuccess, - deleteProfilePhotoReset, - deleteProfilePhoto, -} from './actions'; - -describe('SAVE profile photo actions', () => { - it('should create an action to signal the start of a profile photo save', () => { - const formData = 'multipart form data'; - const expectedAction = { - type: SAVE_PROFILE_PHOTO.BASE, - payload: { - username: 'myusername', - formData, - }, - }; - expect(saveProfilePhoto('myusername', formData)).toEqual(expectedAction); - }); - - it('should create an action to signal user profile photo save beginning', () => { - const expectedAction = { - type: SAVE_PROFILE_PHOTO.BEGIN, - }; - expect(saveProfilePhotoBegin()).toEqual(expectedAction); - }); - - it('should create an action to signal user profile photo save success', () => { - const newPhotoData = { hasImage: true }; - const expectedAction = { - type: SAVE_PROFILE_PHOTO.SUCCESS, - payload: { - profileImage: newPhotoData, - }, - }; - expect(saveProfilePhotoSuccess(newPhotoData)).toEqual(expectedAction); - }); - - it('should create an action to signal user profile photo save reset', () => { - const expectedAction = { - type: SAVE_PROFILE_PHOTO.RESET, - }; - expect(saveProfilePhotoReset()).toEqual(expectedAction); - }); - - it('should create an action to signal user profile photo save failure', () => { - const error = 'Test failure'; - const expectedAction = { - type: SAVE_PROFILE_PHOTO.FAILURE, - payload: { error }, - }; - expect(saveProfilePhotoFailure(error)).toEqual(expectedAction); - }); -}); - -describe('DELETE profile photo actions', () => { - it('should create an action to signal the start of a profile photo deletion', () => { - const expectedAction = { - type: DELETE_PROFILE_PHOTO.BASE, - payload: { - username: 'myusername', - }, - }; - expect(deleteProfilePhoto('myusername')).toEqual(expectedAction); - }); - - it('should create an action to signal user profile photo deletion beginning', () => { - const expectedAction = { - type: DELETE_PROFILE_PHOTO.BEGIN, - }; - expect(deleteProfilePhotoBegin()).toEqual(expectedAction); - }); - - it('should create an action to signal user profile photo deletion success', () => { - const defaultPhotoData = { hasImage: false }; - const expectedAction = { - type: DELETE_PROFILE_PHOTO.SUCCESS, - payload: { - profileImage: defaultPhotoData, - }, - }; - expect(deleteProfilePhotoSuccess(defaultPhotoData)).toEqual(expectedAction); - }); - - it('should create an action to signal user profile photo deletion reset', () => { - const expectedAction = { - type: DELETE_PROFILE_PHOTO.RESET, - }; - expect(deleteProfilePhotoReset()).toEqual(expectedAction); - }); -}); diff --git a/src/profile-v2/data/constants.js b/src/profile-v2/data/constants.js deleted file mode 100644 index de97069..0000000 --- a/src/profile-v2/data/constants.js +++ /dev/null @@ -1,33 +0,0 @@ -const EDUCATION_LEVELS = [ - 'p', - 'm', - 'b', - 'a', - 'hs', - 'jhs', - 'el', - 'none', - 'other', -]; - -const SOCIAL = { - linkedin: { - title: 'LinkedIn', - }, - twitter: { - title: 'Twitter', - }, - facebook: { - title: 'Facebook', - }, -}; - -const FIELD_LABELS = { - COUNTRY: 'country', -}; - -export { - EDUCATION_LEVELS, - SOCIAL, - FIELD_LABELS, -}; diff --git a/src/profile-v2/data/mock_data.js b/src/profile-v2/data/mock_data.js deleted file mode 100644 index c43ed98..0000000 --- a/src/profile-v2/data/mock_data.js +++ /dev/null @@ -1,7 +0,0 @@ -const mockData = { - learningGoal: 'advance_career', - editMode: 'static', - visibilityLearningGoal: 'private', -}; - -export default mockData; diff --git a/src/profile-v2/data/pact-profile.test.js b/src/profile-v2/data/pact-profile.test.js deleted file mode 100644 index abd14e6..0000000 --- a/src/profile-v2/data/pact-profile.test.js +++ /dev/null @@ -1,84 +0,0 @@ -// This test file simply creates a contract that defines -// expectations and correct responses from the Pact stub server. - -import path from 'path'; - -import { PactV3, MatchersV3 } from '@pact-foundation/pact'; - -import { initializeMockApp, getConfig, setConfig } from '@edx/frontend-platform'; -import { getAccount } from './services'; - -const expectedUserInfo200 = { - username: 'staff', - email: 'staff@example.com', - bio: 'This is my bio', - name: 'Lemon Seltzer', - country: 'ME', - dateJoined: '2017-06-07T00:44:23Z', - isActive: true, - yearOfBirth: 1901, - languageProficiencies: [], - levelOfEducation: null, - profileImage: {}, - socialLinks: [], -}; - -const provider = new PactV3({ - log: path.resolve(process.cwd(), 'src/pact-logs/pact.log'), - dir: path.resolve(process.cwd(), 'src/pacts'), - consumer: 'frontend-app-profile', - provider: 'edx-platform', -}); - -describe('getAccount for one username', () => { - beforeAll(async () => { - initializeMockApp(); - }); - it('returns a HTTP 200 and user information', async () => { - const username200 = 'staff'; - await provider.addInteraction({ - states: [{ description: "I have a user's basic information" }], - uponReceiving: "A request for user's basic information", - withRequest: { - method: 'GET', - path: `/api/user/v1/accounts/${username200}`, - headers: {}, - }, - willRespondWith: { - status: 200, - headers: {}, - body: MatchersV3.like(expectedUserInfo200), - }, - }); - return provider.executeTest(async (mockserver) => { - setConfig({ - ...getConfig(), - LMS_BASE_URL: mockserver.url, - }); - const response = await getAccount(username200); - expect(response).toEqual(expectedUserInfo200); - }); - }); - - it('Account does not exist', async () => { - const username404 = 'staff_not_found'; - await provider.addInteraction({ - states: [{ description: "Account and user's information does not exist" }], - uponReceiving: "A request for user's basic information", - withRequest: { - method: 'GET', - path: `/api/user/v1/accounts/${username404}`, - }, - willRespondWith: { - status: 404, - }, - }); - await provider.executeTest(async (mockserver) => { - setConfig({ - ...getConfig(), - LMS_BASE_URL: mockserver.url, - }); - await expect(getAccount(username404).then((response) => response.data)).rejects.toThrow('Request failed with status code 404'); - }); - }); -}); diff --git a/src/profile-v2/data/reducers.js b/src/profile-v2/data/reducers.js deleted file mode 100644 index 3e4760c..0000000 --- a/src/profile-v2/data/reducers.js +++ /dev/null @@ -1,181 +0,0 @@ -import { - SAVE_PROFILE, - SAVE_PROFILE_PHOTO, - DELETE_PROFILE_PHOTO, - CLOSE_FORM, - OPEN_FORM, - FETCH_PROFILE, - UPDATE_DRAFT, - RESET_DRAFTS, -} from './actions'; - -export const initialState = { - errors: {}, - saveState: null, - savePhotoState: null, - currentlyEditingField: null, - account: { - socialLinks: [], - languageProficiencies: [], - name: '', - bio: '', - country: '', - levelOfEducation: '', - profileImage: {}, - yearOfBirth: '', - }, - preferences: { - visibilityName: '', - visibilityBio: '', - visibilityCountry: '', - visibilityLevelOfEducation: '', - visibilitySocialLinks: '', - visibilityLanguageProficiencies: '', - }, - courseCertificates: [], - drafts: {}, - isLoadingProfile: true, - isAuthenticatedUserProfile: false, - disabledCountries: ['RU'], - countriesCodesList: [], -}; - -const profilePage = (state = initialState, action = {}) => { - switch (action.type) { - case FETCH_PROFILE.BEGIN: - return { - ...state, - // TODO: uncomment this line after ARCH-438 Image Post API returns the url - // is complete. Right now we refetch the whole profile causing us to show a full reload - // instead of a partial one. - // isLoadingProfile: true, - }; - case FETCH_PROFILE.SUCCESS: - return { - ...state, - account: { - ...state.account, - ...action.account, - socialLinks: action.account.socialLinks || [], - languageProficiencies: action.account.languageProficiencies || [], - }, - preferences: action.preferences, - courseCertificates: action.courseCertificates || [], - isLoadingProfile: false, - isAuthenticatedUserProfile: action.isAuthenticatedUserProfile, - countriesCodesList: action.countriesCodesList || [], - }; - case SAVE_PROFILE.BEGIN: - return { - ...state, - saveState: 'pending', - errors: {}, - }; - case SAVE_PROFILE.SUCCESS: - return { - ...state, - saveState: 'complete', - errors: {}, - account: action.payload.account !== null ? { - ...state.account, - ...action.payload.account, - socialLinks: action.payload.account.socialLinks || [], - languageProficiencies: action.payload.account.languageProficiencies || [], - } : state.account, - preferences: { ...state.preferences, ...action.payload.preferences }, - }; - case SAVE_PROFILE.FAILURE: - return { - ...state, - saveState: 'error', - isLoadingProfile: false, - errors: { ...state.errors, ...action.payload.errors }, - }; - case SAVE_PROFILE.RESET: - return { - ...state, - saveState: null, - isLoadingProfile: false, - errors: {}, - }; - case SAVE_PROFILE_PHOTO.BEGIN: - return { - ...state, - savePhotoState: 'pending', - errors: {}, - }; - case SAVE_PROFILE_PHOTO.SUCCESS: - return { - ...state, - account: { ...state.account, profileImage: action.payload.profileImage }, - savePhotoState: 'complete', - errors: {}, - }; - case SAVE_PROFILE_PHOTO.FAILURE: - return { - ...state, - savePhotoState: 'error', - errors: { ...state.errors, photo: action.payload.error }, - }; - case SAVE_PROFILE_PHOTO.RESET: - return { - ...state, - savePhotoState: null, - errors: {}, - }; - case DELETE_PROFILE_PHOTO.BEGIN: - return { - ...state, - savePhotoState: 'pending', - errors: {}, - }; - case DELETE_PROFILE_PHOTO.SUCCESS: - return { - ...state, - account: { ...state.account, profileImage: action.payload.profileImage }, - savePhotoState: 'complete', - errors: {}, - }; - case DELETE_PROFILE_PHOTO.FAILURE: - return { - ...state, - savePhotoState: 'error', - errors: { ...state.errors, ...action.payload.errors }, - }; - case DELETE_PROFILE_PHOTO.RESET: - return { - ...state, - savePhotoState: null, - errors: {}, - }; - case UPDATE_DRAFT: - return { - ...state, - drafts: { ...state.drafts, [action.payload.name]: action.payload.value }, - }; - case RESET_DRAFTS: - return { - ...state, - drafts: {}, - }; - case OPEN_FORM: - return { - ...state, - currentlyEditingField: action.payload.formId, - drafts: {}, - }; - case CLOSE_FORM: - if (action.payload.formId === state.currentlyEditingField) { - return { - ...state, - currentlyEditingField: null, - drafts: {}, - }; - } - return state; - default: - return state; - } -}; - -export default profilePage; diff --git a/src/profile-v2/data/sagas.js b/src/profile-v2/data/sagas.js deleted file mode 100644 index b64c3fe..0000000 --- a/src/profile-v2/data/sagas.js +++ /dev/null @@ -1,191 +0,0 @@ -import { history } from '@edx/frontend-platform'; -import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; -import pick from 'lodash.pick'; -import { - all, - call, - delay, - put, - select, - takeEvery, -} from 'redux-saga/effects'; -import { - closeForm, - deleteProfilePhotoBegin, - deleteProfilePhotoReset, - deleteProfilePhotoSuccess, - DELETE_PROFILE_PHOTO, - fetchProfileBegin, - fetchProfileReset, - fetchProfileSuccess, - FETCH_PROFILE, - resetDrafts, - saveProfileBegin, - saveProfileFailure, - saveProfileReset, - saveProfileSuccess, - SAVE_PROFILE, - saveProfilePhotoBegin, - saveProfilePhotoReset, - saveProfilePhotoSuccess, - SAVE_PROFILE_PHOTO, -} from './actions'; -import { handleSaveProfileSelector, userAccountSelector } from './selectors'; -import * as ProfileApiService from './services'; - -export function* handleFetchProfile(action) { - const { username } = action.payload; - const userAccount = yield select(userAccountSelector); - const isAuthenticatedUserProfile = username === getAuthenticatedUser().username; - let preferences = {}; - let account = userAccount; - let courseCertificates = null; - let countriesCodesList = []; - - try { - yield put(fetchProfileBegin()); - - const calls = [ - call(ProfileApiService.getAccount, username), - call(ProfileApiService.getCourseCertificates, username), - call(ProfileApiService.getCountryList), - ]; - - if (isAuthenticatedUserProfile) { - calls.push(call(ProfileApiService.getPreferences, username)); - } - - const result = yield all(calls); - - if (isAuthenticatedUserProfile) { - [account, courseCertificates, countriesCodesList, preferences] = result; - } else { - [account, courseCertificates, countriesCodesList] = result; - } - - if (isAuthenticatedUserProfile && result[0].accountPrivacy === 'all_users') { - yield call(ProfileApiService.patchPreferences, action.payload.username, { - account_privacy: 'custom', - 'visibility.name': 'all_users', - 'visibility.bio': 'all_users', - 'visibility.course_certificates': 'all_users', - 'visibility.country': 'all_users', - 'visibility.date_joined': 'all_users', - 'visibility.level_of_education': 'all_users', - 'visibility.language_proficiencies': 'all_users', - 'visibility.social_links': 'all_users', - 'visibility.time_zone': 'all_users', - }); - } - - yield put(fetchProfileSuccess( - account, - preferences, - courseCertificates, - isAuthenticatedUserProfile, - countriesCodesList, - )); - - yield put(fetchProfileReset()); - } catch (e) { - if (e.response.status === 404) { - history.push('/notfound'); - } else { - throw e; - } - } -} - -export function* handleSaveProfile(action) { - try { - const { drafts, preferences } = yield select(handleSaveProfileSelector); - - const accountDrafts = pick(drafts, [ - 'bio', - 'country', - 'levelOfEducation', - 'languageProficiencies', - 'name', - 'socialLinks', - ]); - - const preferencesDrafts = pick(drafts, [ - 'visibilityBio', - 'visibilityCountry', - 'visibilityLevelOfEducation', - 'visibilityLanguageProficiencies', - 'visibilityName', - 'visibilitySocialLinks', - ]); - - if (Object.keys(preferencesDrafts).length > 0) { - preferencesDrafts.accountPrivacy = 'custom'; - } - - yield put(saveProfileBegin()); - let accountResult = null; - - if (Object.keys(accountDrafts).length > 0) { - accountResult = yield call( - ProfileApiService.patchProfile, - action.payload.username, - accountDrafts, - ); - } - - let preferencesResult = preferences; - if (Object.keys(preferencesDrafts).length > 0) { - yield call(ProfileApiService.patchPreferences, action.payload.username, preferencesDrafts); - // TODO: Temporary deoptimization since the patchPreferences call doesn't return anything. - - preferencesResult = yield call(ProfileApiService.getPreferences, action.payload.username); - } - - yield put(saveProfileSuccess(accountResult, preferencesResult)); - yield delay(1000); - yield put(closeForm(action.payload.formId)); - yield delay(300); - yield put(saveProfileReset()); - yield put(resetDrafts()); - } catch (e) { - if (e.processedData && e.processedData.fieldErrors) { - yield put(saveProfileFailure(e.processedData.fieldErrors)); - } else { - yield put(saveProfileReset()); - throw e; - } - } -} - -export function* handleSaveProfilePhoto(action) { - const { username, formData } = action.payload; - - try { - yield put(saveProfilePhotoBegin()); - const photoResult = yield call(ProfileApiService.postProfilePhoto, username, formData); - yield put(saveProfilePhotoSuccess(photoResult)); - yield put(saveProfilePhotoReset()); - } catch (e) { - yield put(saveProfilePhotoReset()); - } -} - -export function* handleDeleteProfilePhoto(action) { - const { username } = action.payload; - - try { - yield put(deleteProfilePhotoBegin()); - const photoResult = yield call(ProfileApiService.deleteProfilePhoto, username); - yield put(deleteProfilePhotoSuccess(photoResult)); - yield put(deleteProfilePhotoReset()); - } catch (e) { - yield put(deleteProfilePhotoReset()); - } -} - -export default function* profileSaga() { - yield takeEvery(FETCH_PROFILE.BASE, handleFetchProfile); - yield takeEvery(SAVE_PROFILE.BASE, handleSaveProfile); - yield takeEvery(SAVE_PROFILE_PHOTO.BASE, handleSaveProfilePhoto); - yield takeEvery(DELETE_PROFILE_PHOTO.BASE, handleDeleteProfilePhoto); -} diff --git a/src/profile-v2/data/sagas.test.js b/src/profile-v2/data/sagas.test.js deleted file mode 100644 index 2da09b3..0000000 --- a/src/profile-v2/data/sagas.test.js +++ /dev/null @@ -1,167 +0,0 @@ -import { - takeEvery, - put, - call, - delay, - select, - all, -} from 'redux-saga/effects'; -import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; - -import * as profileActions from './actions'; -import { handleSaveProfileSelector, userAccountSelector } from './selectors'; - -jest.mock('./services', () => ({ - getProfile: jest.fn(), - patchProfile: jest.fn(), - postProfilePhoto: jest.fn(), - deleteProfilePhoto: jest.fn(), - getPreferences: jest.fn(), - getAccount: jest.fn(), - getCourseCertificates: jest.fn(), - getCountryList: jest.fn(), -})); - -jest.mock('@edx/frontend-platform/auth', () => ({ - getAuthenticatedUser: jest.fn(), -})); - -/* eslint-disable import/first */ -import profileSaga, { - handleFetchProfile, - handleSaveProfile, - handleSaveProfilePhoto, - handleDeleteProfilePhoto, -} from './sagas'; -import * as ProfileApiService from './services'; -/* eslint-enable import/first */ - -describe('RootSaga', () => { - describe('profileSaga', () => { - it('should pass actions to the correct sagas', () => { - const gen = profileSaga(); - - expect(gen.next().value) - .toEqual(takeEvery(profileActions.FETCH_PROFILE.BASE, handleFetchProfile)); - expect(gen.next().value) - .toEqual(takeEvery(profileActions.SAVE_PROFILE.BASE, handleSaveProfile)); - expect(gen.next().value) - .toEqual(takeEvery(profileActions.SAVE_PROFILE_PHOTO.BASE, handleSaveProfilePhoto)); - expect(gen.next().value) - .toEqual(takeEvery(profileActions.DELETE_PROFILE_PHOTO.BASE, handleDeleteProfilePhoto)); - - expect(gen.next().value).toBeUndefined(); - }); - }); - - describe('handleFetchProfile', () => { - it('should fetch certificates and preferences for the current user profile', () => { - const userAccount = { - username: 'gonzo', - other: 'data', - }; - getAuthenticatedUser.mockReturnValue(userAccount); - const selectorData = { - userAccount, - }; - - const action = profileActions.fetchProfile('gonzo'); - const gen = handleFetchProfile(action); - - const result = [userAccount, [1, 2, 3], [], { preferences: 'stuff' }]; - - expect(gen.next().value).toEqual(select(userAccountSelector)); - expect(gen.next(selectorData).value).toEqual(put(profileActions.fetchProfileBegin())); - expect(gen.next().value).toEqual(all([ - call(ProfileApiService.getAccount, 'gonzo'), - call(ProfileApiService.getCourseCertificates, 'gonzo'), - call(ProfileApiService.getCountryList), - call(ProfileApiService.getPreferences, 'gonzo'), - ])); - expect(gen.next(result).value) - .toEqual(put(profileActions.fetchProfileSuccess(userAccount, result[3], result[1], true, []))); - expect(gen.next().value).toEqual(put(profileActions.fetchProfileReset())); - expect(gen.next().value).toBeUndefined(); - }); - - it('should fetch certificates and profile for some other user profile', () => { - const userAccount = { - username: 'gonzo', - other: 'data', - }; - const countriesCodesList = [{ code: 'AX' }, { code: 'AL' }]; - getAuthenticatedUser.mockReturnValue(userAccount); - const selectorData = { - userAccount, - }; - - const action = profileActions.fetchProfile('booyah'); - const gen = handleFetchProfile(action); - - const result = [{}, [1, 2, 3], countriesCodesList]; - - expect(gen.next().value).toEqual(select(userAccountSelector)); - expect(gen.next(selectorData).value).toEqual(put(profileActions.fetchProfileBegin())); - expect(gen.next().value).toEqual(all([ - call(ProfileApiService.getAccount, 'booyah'), - call(ProfileApiService.getCourseCertificates, 'booyah'), - call(ProfileApiService.getCountryList), - ])); - expect(gen.next(result).value) - .toEqual(put(profileActions.fetchProfileSuccess(result[0], {}, result[1], false, countriesCodesList))); - expect(gen.next().value).toEqual(put(profileActions.fetchProfileReset())); - expect(gen.next().value).toBeUndefined(); - }); - }); - - describe('handleSaveProfile', () => { - const selectorData = { - username: 'my username', - drafts: { - name: 'Full Name', - }, - preferences: {}, - }; - - it('should successfully process a saveProfile request if there are no exceptions', () => { - const action = profileActions.saveProfile('ze form id', 'my username'); - const gen = handleSaveProfile(action); - const profile = { - name: 'Full Name', - levelOfEducation: 'b', - }; - expect(gen.next().value).toEqual(select(handleSaveProfileSelector)); - expect(gen.next(selectorData).value).toEqual(put(profileActions.saveProfileBegin())); - expect(gen.next().value).toEqual(call(ProfileApiService.patchProfile, 'my username', { - name: 'Full Name', - })); - expect(gen.next(profile).value).toEqual(put(profileActions.saveProfileSuccess(profile, {}))); - expect(gen.next().value).toEqual(delay(1000)); - expect(gen.next().value).toEqual(put(profileActions.closeForm('ze form id'))); - expect(gen.next().value).toEqual(delay(300)); - expect(gen.next().value).toEqual(put(profileActions.saveProfileReset())); - expect(gen.next().value).toEqual(put(profileActions.resetDrafts())); - expect(gen.next().value).toBeUndefined(); - }); - - it('should successfully publish a failure action on exception', () => { - const error = new Error('uhoh'); - error.processedData = { - fieldErrors: { - uhoh: 'not good', - }, - }; - const action = profileActions.saveProfile( - 'ze form id', - 'my username', - ); - const gen = handleSaveProfile(action); - - expect(gen.next().value).toEqual(select(handleSaveProfileSelector)); - expect(gen.next(selectorData).value).toEqual(put(profileActions.saveProfileBegin())); - const result = gen.throw(error); - expect(result.value).toEqual(put(profileActions.saveProfileFailure({ uhoh: 'not good' }))); - expect(gen.next().value).toBeUndefined(); - }); - }); -}); diff --git a/src/profile-v2/data/selectors.js b/src/profile-v2/data/selectors.js deleted file mode 100644 index d398295..0000000 --- a/src/profile-v2/data/selectors.js +++ /dev/null @@ -1,338 +0,0 @@ -import { createSelector } from 'reselect'; -import { - getLocale, - getLanguageList, - getCountryList, - getCountryMessages, - getLanguageMessages, -} from '@edx/frontend-platform/i18n'; - -export const formIdSelector = (state, props) => props.formId; -export const userAccountSelector = state => state.userAccount; -export const profileAccountSelector = state => state.profilePage.account; -export const profileDraftsSelector = state => state.profilePage.drafts; -export const accountPrivacySelector = state => state.profilePage.preferences.accountPrivacy; -export const profilePreferencesSelector = state => state.profilePage.preferences; -export const profileCourseCertificatesSelector = state => state.profilePage.courseCertificates; -export const saveStateSelector = state => state.profilePage.saveState; -export const savePhotoStateSelector = state => state.profilePage.savePhotoState; -export const isLoadingProfileSelector = state => state.profilePage.isLoadingProfile; -export const currentlyEditingFieldSelector = state => state.profilePage.currentlyEditingField; -export const accountErrorsSelector = state => state.profilePage.errors; -export const isAuthenticatedUserProfileSelector = state => state.profilePage.isAuthenticatedUserProfile; -export const countriesCodesListSelector = state => state.profilePage.countriesCodesList; - -export const editableFormModeSelector = createSelector( - profileAccountSelector, - isAuthenticatedUserProfileSelector, - profileCourseCertificatesSelector, - formIdSelector, - currentlyEditingFieldSelector, - (account, isAuthenticatedUserProfile, certificates, formId, currentlyEditingField) => { - let propExists = account[formId] != null && account[formId].length > 0; - propExists = formId === 'certificates' ? certificates.length > 0 : propExists; - if (!isAuthenticatedUserProfile) { - return 'static'; - } - if (formId === currentlyEditingField) { - return 'editing'; - } - - if (!propExists) { - return 'empty'; - } - - return 'editable'; - }, -); - -export const accountDraftsFieldSelector = createSelector( - formIdSelector, - profileDraftsSelector, - (formId, drafts) => drafts[formId], -); - -export const visibilityDraftsFieldSelector = createSelector( - formIdSelector, - profileDraftsSelector, - (formId, drafts) => drafts[`visibility${formId.charAt(0).toUpperCase() + formId.slice(1)}`], -); - -export const formErrorSelector = createSelector( - accountErrorsSelector, - formIdSelector, - (errors, formId) => (errors[formId] ? errors[formId].userMessage : null), -); - -export const editableFormSelector = createSelector( - editableFormModeSelector, - formErrorSelector, - saveStateSelector, - (editMode, error, saveState) => ({ - editMode, - error, - saveState, - }), -); - -export const localeSelector = () => getLocale(); -export const countryMessagesSelector = createSelector( - localeSelector, - locale => getCountryMessages(locale), -); -export const languageMessagesSelector = createSelector( - localeSelector, - locale => getLanguageMessages(locale), -); - -export const sortedLanguagesSelector = createSelector( - localeSelector, - locale => getLanguageList(locale), -); - -export const sortedCountriesSelector = createSelector( - localeSelector, - countriesCodesListSelector, - profileAccountSelector, - (locale, countriesCodesList, profileAccount) => { - const countryList = getCountryList(locale); - const userCountry = profileAccount.country; - - return countryList.filter(({ code }) => code === userCountry || countriesCodesList.find(x => x === code)); - }, -); - -export const preferredLanguageSelector = createSelector( - editableFormSelector, - sortedLanguagesSelector, - languageMessagesSelector, - (editableForm, sortedLanguages, languageMessages) => ({ - ...editableForm, - sortedLanguages, - languageMessages, - }), -); - -export const countrySelector = createSelector( - editableFormSelector, - sortedCountriesSelector, - countryMessagesSelector, - countriesCodesListSelector, - profileAccountSelector, - (editableForm, translatedCountries, countryMessages, countriesCodesList, account) => ({ - ...editableForm, - translatedCountries, - countryMessages, - countriesCodesList, - committedCountry: account.country, - }), -); - -export const certificatesSelector = createSelector( - editableFormSelector, - profileCourseCertificatesSelector, - (editableForm, certificates) => ({ - ...editableForm, - certificates, - value: certificates, - }), -); - -export const profileImageSelector = createSelector( - profileAccountSelector, - account => (account.profileImage != null - ? { - src: account.profileImage.imageUrlFull, - isDefault: !account.profileImage.hasImage, - } - : {}), -); - -export const handleSaveProfileSelector = createSelector( - profileDraftsSelector, - profilePreferencesSelector, - (drafts, preferences) => ({ - drafts, - preferences, - }), -); - -const socialLinksByPlatformSelector = createSelector( - profileAccountSelector, - (account) => { - const linksByPlatform = {}; - if (Array.isArray(account.socialLinks)) { - account.socialLinks.forEach((socialLink) => { - linksByPlatform[socialLink.platform] = socialLink; - }); - } - return linksByPlatform; - }, -); - -const draftSocialLinksByPlatformSelector = createSelector( - profileDraftsSelector, - (drafts) => { - const linksByPlatform = {}; - if (Array.isArray(drafts.socialLinks)) { - drafts.socialLinks.forEach((socialLink) => { - linksByPlatform[socialLink.platform] = socialLink; - }); - } - return linksByPlatform; - }, -); - -export const formSocialLinksSelector = createSelector( - socialLinksByPlatformSelector, - draftSocialLinksByPlatformSelector, - (linksByPlatform, draftLinksByPlatform) => { - const knownPlatforms = ['twitter', 'facebook', 'linkedin']; - const socialLinks = []; - knownPlatforms.forEach((platform) => { - if (draftLinksByPlatform[platform] !== undefined) { - socialLinks.push(draftLinksByPlatform[platform]); - } else if (linksByPlatform[platform] !== undefined) { - socialLinks.push(linksByPlatform[platform]); - } else { - socialLinks.push({ - platform, - socialLink: null, - }); - } - }); - return socialLinks; - }, -); - -export const visibilitiesSelector = createSelector( - profilePreferencesSelector, - accountPrivacySelector, - (preferences, accountPrivacy) => { - switch (accountPrivacy) { - case 'custom': - return { - visibilityBio: preferences.visibilityBio || 'all_users', - visibilityCountry: preferences.visibilityCountry || 'all_users', - visibilityLevelOfEducation: preferences.visibilityLevelOfEducation || 'all_users', - visibilityLanguageProficiencies: preferences.visibilityLanguageProficiencies || 'all_users', - visibilityName: preferences.visibilityName || 'all_users', - visibilitySocialLinks: preferences.visibilitySocialLinks || 'all_users', - }; - case 'private': - return { - visibilityBio: 'private', - visibilityCountry: 'private', - visibilityLevelOfEducation: 'private', - visibilityLanguageProficiencies: 'private', - visibilityName: 'private', - visibilitySocialLinks: 'private', - }; - case 'all_users': - default: - return { - visibilityBio: 'all_users', - visibilityCountry: 'all_users', - visibilityLevelOfEducation: 'all_users', - visibilityLanguageProficiencies: 'all_users', - visibilityName: 'all_users', - visibilitySocialLinks: 'all_users', - }; - } - }, -); - -function chooseFormValue(draft, committed) { - return draft !== undefined ? draft : committed; -} - -export const formValuesSelector = createSelector( - profileAccountSelector, - visibilitiesSelector, - profileDraftsSelector, - profileCourseCertificatesSelector, - formSocialLinksSelector, - (account, visibilities, drafts, courseCertificates, socialLinks) => ({ - bio: chooseFormValue(drafts.bio, account.bio), - visibilityBio: chooseFormValue(drafts.visibilityBio, visibilities.visibilityBio), - courseCertificates, - country: chooseFormValue(drafts.country, account.country), - visibilityCountry: chooseFormValue(drafts.visibilityCountry, visibilities.visibilityCountry), - levelOfEducation: chooseFormValue(drafts.levelOfEducation, account.levelOfEducation), - visibilityLevelOfEducation: chooseFormValue( - drafts.visibilityLevelOfEducation, - visibilities.visibilityLevelOfEducation, - ), - languageProficiencies: chooseFormValue( - drafts.languageProficiencies, - account.languageProficiencies, - ), - visibilityLanguageProficiencies: chooseFormValue( - drafts.visibilityLanguageProficiencies, - visibilities.visibilityLanguageProficiencies, - ), - name: chooseFormValue(drafts.name, account.name), - visibilityName: chooseFormValue(drafts.visibilityName, visibilities.visibilityName), - socialLinks, - visibilitySocialLinks: chooseFormValue( - drafts.visibilitySocialLinks, - visibilities.visibilitySocialLinks, - ), - }), -); - -export const profilePageSelector = createSelector( - profileAccountSelector, - formValuesSelector, - profileImageSelector, - saveStateSelector, - savePhotoStateSelector, - isLoadingProfileSelector, - draftSocialLinksByPlatformSelector, - accountErrorsSelector, - isAuthenticatedUserProfileSelector, - ( - account, - formValues, - profileImage, - saveState, - savePhotoState, - isLoadingProfile, - draftSocialLinksByPlatform, - errors, - isAuthenticatedUserProfile, - ) => ({ - username: account.username, - profileImage, - requiresParentalConsent: account.requiresParentalConsent, - dateJoined: account.dateJoined, - yearOfBirth: account.yearOfBirth, - - bio: formValues.bio, - visibilityBio: formValues.visibilityBio, - - courseCertificates: formValues.courseCertificates, - - country: formValues.country, - visibilityCountry: formValues.visibilityCountry, - - levelOfEducation: formValues.levelOfEducation, - visibilityLevelOfEducation: formValues.visibilityLevelOfEducation, - - languageProficiencies: formValues.languageProficiencies, - visibilityLanguageProficiencies: formValues.visibilityLanguageProficiencies, - - name: formValues.name, - visibilityName: formValues.visibilityName, - - socialLinks: formValues.socialLinks, - visibilitySocialLinks: formValues.visibilitySocialLinks, - draftSocialLinksByPlatform, - - saveState, - savePhotoState, - isLoadingProfile, - photoUploadError: errors.photo || null, - isAuthenticatedUserProfile, - }), -); diff --git a/src/profile-v2/data/services.js b/src/profile-v2/data/services.js deleted file mode 100644 index e6104df..0000000 --- a/src/profile-v2/data/services.js +++ /dev/null @@ -1,168 +0,0 @@ -import { ensureConfig, getConfig } from '@edx/frontend-platform'; -import { getAuthenticatedHttpClient as getHttpClient } from '@edx/frontend-platform/auth'; -import { logError } from '@edx/frontend-platform/logging'; -import { camelCaseObject, convertKeyNames, snakeCaseObject } from '../utils'; -import { FIELD_LABELS } from './constants'; - -ensureConfig(['LMS_BASE_URL'], 'Profile API service'); - -function processAccountData(data) { - const processedData = camelCaseObject(data); - return { - ...processedData, - socialLinks: Array.isArray(processedData.socialLinks) ? processedData.socialLinks : [], - languageProficiencies: Array.isArray(processedData.languageProficiencies) - ? processedData.languageProficiencies : [], - name: processedData.name || null, - bio: processedData.bio || null, - country: processedData.country || null, - levelOfEducation: processedData.levelOfEducation || null, - profileImage: processedData.profileImage || {}, - yearOfBirth: processedData.yearOfBirth || null, - }; -} - -function processAndThrowError(error, errorDataProcessor) { - const processedError = Object.create(error); - if (error.response && error.response.data && typeof error.response.data === 'object') { - processedError.processedData = errorDataProcessor(error.response.data); - throw processedError; - } else { - throw error; - } -} - -export async function getAccount(username) { - const { data } = await getHttpClient().get(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`); - - return processAccountData(data); -} - -export async function patchProfile(username, params) { - const processedParams = snakeCaseObject(params); - - const { data } = await getHttpClient() - .patch(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`, processedParams, { - headers: { - 'Content-Type': 'application/merge-patch+json', - }, - }) - .catch((error) => { - processAndThrowError(error, processAccountData); - }); - - return processAccountData(data); -} - -export async function postProfilePhoto(username, formData) { - // eslint-disable-next-line no-unused-vars - const { data } = await getHttpClient().post( - `${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}/image`, - formData, - { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }, - ).catch((error) => { - processAndThrowError(error, camelCaseObject); - }); - - // TODO: Someday in the future the POST photo endpoint - // will return the new values. At that time we should - // use the commented line below instead of the separate - // getAccount request that follows. - // return camelCaseObject(data); - const updatedData = await getAccount(username); - return updatedData.profileImage; -} - -export async function deleteProfilePhoto(username) { - // eslint-disable-next-line no-unused-vars - const { data } = await getHttpClient().delete(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}/image`); - - // TODO: Someday in the future the POST photo endpoint - // will return the new values. At that time we should - // use the commented line below instead of the separate - // getAccount request that follows. - // return camelCaseObject(data); - const updatedData = await getAccount(username); - return updatedData.profileImage; -} - -export async function getPreferences(username) { - const { data } = await getHttpClient().get(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`); - - return camelCaseObject(data); -} - -export async function patchPreferences(username, params) { - let processedParams = snakeCaseObject(params); - processedParams = convertKeyNames(processedParams, { - visibility_bio: 'visibility.bio', - visibility_course_certificates: 'visibility.course_certificates', - visibility_country: 'visibility.country', - visibility_date_joined: 'visibility.date_joined', - visibility_level_of_education: 'visibility.level_of_education', - visibility_language_proficiencies: 'visibility.language_proficiencies', - visibility_name: 'visibility.name', - visibility_social_links: 'visibility.social_links', - visibility_time_zone: 'visibility.time_zone', - }); - - await getHttpClient().patch(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`, processedParams, { - headers: { 'Content-Type': 'application/merge-patch+json' }, - }); - - return params; // TODO: Once the server returns the updated preferences object, return that. -} - -function transformCertificateData(data) { - const transformedData = []; - data.forEach((cert) => { - // download_url may be full url or absolute path. - // note: using the URL() api breaks in ie 11 - const urlIsPath = typeof cert.download_url === 'string' - && cert.download_url.search(/http[s]?:\/\//) !== 0; - - const downloadUrl = urlIsPath - ? `${getConfig().LMS_BASE_URL}${cert.download_url}` - : cert.download_url; - - transformedData.push({ - ...camelCaseObject(cert), - certificateType: cert.certificate_type, - downloadUrl, - }); - }); - return transformedData; -} - -export async function getCourseCertificates(username) { - const url = `${getConfig().LMS_BASE_URL}/api/certificates/v0/certificates/${username}/`; - try { - const { data } = await getHttpClient().get(url); - return transformCertificateData(data); - } catch (e) { - logError(e); - return []; - } -} - -function extractCountryList(data) { - return data?.fields - .find(({ name }) => name === FIELD_LABELS.COUNTRY) - ?.options?.map(({ value }) => (value)) || []; -} - -export async function getCountryList() { - const url = `${getConfig().LMS_BASE_URL}/user_api/v1/account/registration/`; - - try { - const { data } = await getHttpClient().get(url); - return extractCountryList(data); - } catch (e) { - logError(e); - return []; - } -} diff --git a/src/profile-v2/forms/Bio.jsx b/src/profile-v2/forms/Bio.jsx deleted file mode 100644 index 797bbb6..0000000 --- a/src/profile-v2/forms/Bio.jsx +++ /dev/null @@ -1,151 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; -import { Form } from '@openedx/paragon'; - -import classNames from 'classnames'; -import messages from './Bio.messages'; - -import FormControls from './elements/FormControls'; -import EditableItemHeader from './elements/EditableItemHeader'; -import EmptyContent from './elements/EmptyContent'; -import SwitchContent from './elements/SwitchContent'; - -import { editableFormSelector } from '../data/selectors'; -import { - useCloseOpenHandler, - useHandleChange, - useHandleSubmit, - useIsOnMobileScreen, - useIsVisibilityEnabled, -} from '../data/hooks'; - -const Bio = ({ - formId, - bio, - visibilityBio, - editMode, - saveState, - error, - changeHandler, - submitHandler, - closeHandler, - openHandler, -}) => { - const isMobileView = useIsOnMobileScreen(); - const isVisibilityEnabled = useIsVisibilityEnabled(); - const intl = useIntl(); - - const handleChange = useHandleChange(changeHandler); - const handleSubmit = useHandleSubmit(submitHandler, formId); - const handleOpen = useCloseOpenHandler(openHandler, formId); - const handleClose = useCloseOpenHandler(closeHandler, formId); - - return ( - - - -- {intl.formatMessage(messages['profile.bio.about.me'])} -
- - {error !== null && ( -- {error} - - )} -- - - ), - editable: ( - <> - - {intl.formatMessage(messages['profile.bio.about.me'])} -
-- > - ), - empty: ( - <> - - {intl.formatMessage(messages['profile.bio.about.me'])} -
-- - > - ), - static: ( - <> -- - {intl.formatMessage(messages['profile.bio.about.me'])} -
-- > - ), - }} - /> - ); -}; - -Bio.propTypes = { - formId: PropTypes.string.isRequired, - bio: PropTypes.string, - visibilityBio: PropTypes.oneOf(['private', 'all_users']), - editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']), - saveState: PropTypes.string, - error: PropTypes.string, - changeHandler: PropTypes.func.isRequired, - submitHandler: PropTypes.func.isRequired, - closeHandler: PropTypes.func.isRequired, - openHandler: PropTypes.func.isRequired, -}; - -Bio.defaultProps = { - editMode: 'static', - saveState: null, - bio: null, - visibilityBio: 'private', - error: null, -}; - -export default connect( - editableFormSelector, - {}, -)(Bio); diff --git a/src/profile-v2/forms/Bio.messages.jsx b/src/profile-v2/forms/Bio.messages.jsx deleted file mode 100644 index 0860acd..0000000 --- a/src/profile-v2/forms/Bio.messages.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - 'profile.bio.about.me': { - id: 'profile.bio.about.me', - defaultMessage: 'Bio', - description: 'A section of a user profile', - }, -}); - -export default messages; diff --git a/src/profile-v2/forms/Country.jsx b/src/profile-v2/forms/Country.jsx deleted file mode 100644 index 86cd039..0000000 --- a/src/profile-v2/forms/Country.jsx +++ /dev/null @@ -1,163 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { useIntl } from '@edx/frontend-platform/i18n'; -import { Form } from '@openedx/paragon'; - -import messages from './Country.messages'; - -import FormControls from './elements/FormControls'; -import EditableItemHeader from './elements/EditableItemHeader'; -import EmptyContent from './elements/EmptyContent'; -import SwitchContent from './elements/SwitchContent'; - -import { countrySelector } from '../data/selectors'; -import { - useCloseOpenHandler, - useHandleChange, - useHandleSubmit, - useIsVisibilityEnabled, -} from '../data/hooks'; - -const Country = ({ - formId, - country, - visibilityCountry, - editMode, - saveState, - error, - translatedCountries, - countriesCodesList, - countryMessages, - changeHandler, - submitHandler, - closeHandler, - openHandler, -}) => { - const isVisibilityEnabled = useIsVisibilityEnabled(); - const intl = useIntl(); - - const handleChange = useHandleChange(changeHandler); - const handleSubmit = useHandleSubmit(submitHandler, formId); - const handleOpen = useCloseOpenHandler(openHandler, formId); - const handleClose = useCloseOpenHandler(closeHandler, formId); - - const isDisabledCountry = (countryCode) => countriesCodesList.length > 0 - && !countriesCodesList.find(code => code === countryCode); - - return ( - - - -- {intl.formatMessage(messages['profile.country.label'])} -
- - {error !== null && ( -- {error} - - )} -- - - ), - editable: ( - <> - - {intl.formatMessage(messages['profile.country.label'])} -
-- > - ), - empty: ( - <> - - {intl.formatMessage(messages['profile.country.label'])} -
-- {intl.formatMessage(messages['profile.country.empty'])} - - > - ), - static: ( - <> -- {intl.formatMessage(messages['profile.country.label'])} -
-- > - ), - }} - /> - ); -}; - -Country.propTypes = { - formId: PropTypes.string.isRequired, - country: PropTypes.string, - visibilityCountry: PropTypes.oneOf(['private', 'all_users']), - editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']), - saveState: PropTypes.string, - error: PropTypes.string, - translatedCountries: PropTypes.arrayOf(PropTypes.shape({ - code: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - })).isRequired, - countriesCodesList: PropTypes.arrayOf(PropTypes.string).isRequired, - countryMessages: PropTypes.objectOf(PropTypes.string).isRequired, - changeHandler: PropTypes.func.isRequired, - submitHandler: PropTypes.func.isRequired, - closeHandler: PropTypes.func.isRequired, - openHandler: PropTypes.func.isRequired, -}; - -Country.defaultProps = { - editMode: 'static', - saveState: null, - country: null, - visibilityCountry: 'private', - error: null, -}; - -export default connect( - countrySelector, - {}, -)(Country); diff --git a/src/profile-v2/forms/Country.messages.jsx b/src/profile-v2/forms/Country.messages.jsx deleted file mode 100644 index 137bff6..0000000 --- a/src/profile-v2/forms/Country.messages.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - 'profile.country.label': { - id: 'profile.country.label', - defaultMessage: 'Country', - description: 'The label for a country in a user profile.', - }, - 'profile.country.empty': { - id: 'profile.country.empty', - defaultMessage: 'Add country', - description: 'The affordance to add country location to a user’s profile.', - }, -}); - -export default messages; diff --git a/src/profile-v2/forms/Education.jsx b/src/profile-v2/forms/Education.jsx deleted file mode 100644 index 5b877fe..0000000 --- a/src/profile-v2/forms/Education.jsx +++ /dev/null @@ -1,171 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; -import get from 'lodash.get'; -import { Form } from '@openedx/paragon'; - -import messages from './Education.messages'; - -import FormControls from './elements/FormControls'; -import EditableItemHeader from './elements/EditableItemHeader'; -import EmptyContent from './elements/EmptyContent'; -import SwitchContent from './elements/SwitchContent'; - -import { EDUCATION_LEVELS } from '../data/constants'; - -import { editableFormSelector } from '../data/selectors'; -import { - useCloseOpenHandler, - useHandleChange, - useHandleSubmit, - useIsVisibilityEnabled, -} from '../data/hooks'; - -const Education = ({ - formId, - levelOfEducation, - visibilityLevelOfEducation, - editMode, - saveState, - error, - changeHandler, - submitHandler, - closeHandler, - openHandler, -}) => { - const isVisibilityEnabled = useIsVisibilityEnabled(); - const intl = useIntl(); - - const handleChange = useHandleChange(changeHandler); - const handleSubmit = useHandleSubmit(submitHandler, formId); - const handleOpen = useCloseOpenHandler(openHandler, formId); - const handleClose = useCloseOpenHandler(closeHandler, formId); - - return ( - - - -- {intl.formatMessage(messages['profile.education.education'])} -
- - {error !== null && ( -- {error} - - )} -- - - ), - editable: ( - <> - - {intl.formatMessage(messages['profile.education.education'])} -
-- > - ), - empty: ( - <> - - {intl.formatMessage(messages['profile.education.education'])} -
-- - > - ), - static: ( - <> -- - {intl.formatMessage(messages['profile.education.education'])} -
-- > - ), - }} - /> - ); -}; - -Education.propTypes = { - formId: PropTypes.string.isRequired, - levelOfEducation: PropTypes.string, - visibilityLevelOfEducation: PropTypes.oneOf(['private', 'all_users']), - editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']), - saveState: PropTypes.string, - error: PropTypes.string, - changeHandler: PropTypes.func.isRequired, - submitHandler: PropTypes.func.isRequired, - closeHandler: PropTypes.func.isRequired, - openHandler: PropTypes.func.isRequired, -}; - -Education.defaultProps = { - editMode: 'static', - saveState: null, - levelOfEducation: null, - visibilityLevelOfEducation: 'private', - error: null, -}; - -export default connect( - editableFormSelector, - {}, -)(Education); diff --git a/src/profile-v2/forms/Education.messages.jsx b/src/profile-v2/forms/Education.messages.jsx deleted file mode 100644 index dd7f422..0000000 --- a/src/profile-v2/forms/Education.messages.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - 'profile.education.education': { - id: 'profile.education.education', - defaultMessage: 'Education', - description: 'A section of a user profile', - }, - 'profile.education.levels.p': { - id: 'profile.education.levels.p', - defaultMessage: 'Doctorate', - description: 'Selected by the user if their highest level of education is a doctorate degree.', - }, - 'profile.education.levels.m': { - id: 'profile.education.levels.m', - defaultMessage: "Master's or professional degree", - description: "Selected by the user if their highest level of education is a master's or professional degree from a college or university.", - }, - 'profile.education.levels.b': { - id: 'profile.education.levels.b', - defaultMessage: "Bachelor's Degree", - description: "Selected by the user if their highest level of education is a four year college or university bachelor's degree.", - }, - 'profile.education.levels.a': { - id: 'profile.education.levels.a', - defaultMessage: "Associate's degree", - description: "Selected by the user if their highest level of education is an associate's degree. 1-2 years of college or university.", - }, - 'profile.education.levels.hs': { - id: 'profile.education.levels.hs', - defaultMessage: 'Secondary/high school', - description: 'Selected by the user if their highest level of education is secondary or high school. 9-12 years of education.', - }, - 'profile.education.levels.jhs': { - id: 'profile.education.levels.jhs', - defaultMessage: 'Junior secondary/junior high/middle school', - description: 'Selected by the user if their highest level of education is junior or middle school. 6-8 years of education.', - }, - 'profile.education.levels.el': { - id: 'profile.education.levels.el', - defaultMessage: 'Elementary/primary school', - description: 'Selected by the user if their highest level of education is elementary or primary school. 1-5 years of education.', - }, - 'profile.education.levels.none': { - id: 'profile.education.levels.none', - defaultMessage: 'No formal education', - description: 'Selected by the user to describe their education.', - }, - 'profile.education.levels.o': { - id: 'profile.education.levels.o', - defaultMessage: 'Other education', - description: 'Selected by the user if they have a type of education not described by the other choices.', - }, -}); - -export default messages; diff --git a/src/profile-v2/forms/Name.jsx b/src/profile-v2/forms/Name.jsx deleted file mode 100644 index f3e2993..0000000 --- a/src/profile-v2/forms/Name.jsx +++ /dev/null @@ -1,192 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { useIntl } from '@edx/frontend-platform/i18n'; - -import { InfoOutline } from '@openedx/paragon/icons'; -import { Hyperlink, OverlayTrigger, Tooltip } from '@openedx/paragon'; -import messages from './Name.messages'; - -import FormControls from './elements/FormControls'; -import EditableItemHeader from './elements/EditableItemHeader'; -import EmptyContent from './elements/EmptyContent'; -import SwitchContent from './elements/SwitchContent'; - -import { editableFormSelector } from '../data/selectors'; -import { - useCloseOpenHandler, - useHandleChange, - useHandleSubmit, - useIsVisibilityEnabled, -} from '../data/hooks'; - -const Name = ({ - formId, - name, - visibilityName, - editMode, - saveState, - changeHandler, - submitHandler, - closeHandler, - openHandler, - accountSettingsUrl, -}) => { - const isVisibilityEnabled = useIsVisibilityEnabled(); - const intl = useIntl(); - - const handleChange = useHandleChange(changeHandler); - const handleSubmit = useHandleSubmit(submitHandler, formId); - const handleOpen = useCloseOpenHandler(openHandler, formId); - const handleClose = useCloseOpenHandler(closeHandler, formId); - - return ( - - - - ), - editable: ( - <> - --- {intl.formatMessage(messages['profile.name.full.name'])} -
-- -- {intl.formatMessage(messages['profile.name.tooltip'])} -
- - )} - > -- - > - ), - empty: ( - <> - --- {intl.formatMessage(messages['profile.name.full.name'])} -
-- -- {intl.formatMessage(messages['profile.name.tooltip'])} -
- - )} - > -- - {intl.formatMessage(messages['profile.name.empty'])} - - > - ), - static: ( - <> ---- {intl.formatMessage(messages['profile.name.full.name'])} -
-- -- {intl.formatMessage(messages['profile.name.tooltip'])} -
- - )} - > -- - > - ), - }} - /> - ); -}; - -Name.propTypes = { - formId: PropTypes.string.isRequired, - name: PropTypes.string, - visibilityName: PropTypes.oneOf(['private', 'all_users']), - editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']), - saveState: PropTypes.string, - changeHandler: PropTypes.func.isRequired, - submitHandler: PropTypes.func.isRequired, - closeHandler: PropTypes.func.isRequired, - openHandler: PropTypes.func.isRequired, - accountSettingsUrl: PropTypes.string.isRequired, -}; - -Name.defaultProps = { - editMode: 'static', - saveState: null, - name: null, - visibilityName: 'private', -}; - -export default connect( - editableFormSelector, - {}, -)(Name); diff --git a/src/profile-v2/forms/Name.messages.jsx b/src/profile-v2/forms/Name.messages.jsx deleted file mode 100644 index e9e9317..0000000 --- a/src/profile-v2/forms/Name.messages.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - 'profile.name.full.name': { - id: 'profile.name.full.name', - defaultMessage: 'Full name', - description: 'A section of a user profile', - }, - 'profile.name.empty': { - id: 'profile.name.empty', - defaultMessage: 'Add full name', - description: 'The affordance to add a name to a user’s profile.', - }, - 'profile.name.tooltip': { - id: 'profile.name.tooltip', - defaultMessage: 'The name that is used for ID verification and that appears on your certificates', - description: 'Tooltip for the full name field.', - }, - 'profile.name.redirect': { - id: 'profile.name.redirect', - defaultMessage: 'Edit full name from the Accounts page', - description: 'Redirect message for editing the name from the Accounts page.', - }, -}); - -export default messages; diff --git a/src/profile-v2/forms/PreferredLanguage.jsx b/src/profile-v2/forms/PreferredLanguage.jsx deleted file mode 100644 index 54e0e17..0000000 --- a/src/profile-v2/forms/PreferredLanguage.jsx +++ /dev/null @@ -1,166 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { useIntl } from '@edx/frontend-platform/i18n'; -import { Form } from '@openedx/paragon'; - -import messages from './PreferredLanguage.messages'; - -import FormControls from './elements/FormControls'; -import EditableItemHeader from './elements/EditableItemHeader'; -import EmptyContent from './elements/EmptyContent'; -import SwitchContent from './elements/SwitchContent'; - -import { preferredLanguageSelector } from '../data/selectors'; -import { - useCloseOpenHandler, - useHandleSubmit, - useIsVisibilityEnabled, -} from '../data/hooks'; - -const PreferredLanguage = ({ - formId, - languageProficiencies, - visibilityLanguageProficiencies, - editMode, - saveState, - error, - sortedLanguages, - languageMessages, - changeHandler, - submitHandler, - closeHandler, - openHandler, -}) => { - const isVisibilityEnabled = useIsVisibilityEnabled(); - const intl = useIntl(); - - const handleChange = ({ target: { name, value } }) => { - let newValue = value; - if (name === formId) { - newValue = value ? [{ code: value }] : []; - } - changeHandler(name, newValue); - }; - - const handleSubmit = useHandleSubmit(submitHandler, formId); - const handleOpen = useCloseOpenHandler(openHandler, formId); - const handleClose = useCloseOpenHandler(closeHandler, formId); - - const value = languageProficiencies.length ? languageProficiencies[0].code : ''; - - return ( - - - -- {intl.formatMessage(messages['profile.preferredlanguage.label'])} -
- - {error !== null && ( -- {error} - - )} -- - - ), - editable: ( - <> - - {intl.formatMessage(messages['profile.preferredlanguage.label'])} -
-- > - ), - empty: ( - <> - - {intl.formatMessage(messages['profile.preferredlanguage.label'])} -
-- {intl.formatMessage(messages['profile.preferredlanguage.empty'])} - - > - ), - static: ( - <> -- {intl.formatMessage(messages['profile.preferredlanguage.label'])} -
-- > - ), - }} - /> - ); -}; - -PreferredLanguage.propTypes = { - formId: PropTypes.string.isRequired, - languageProficiencies: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.shape({ code: PropTypes.string })), - PropTypes.oneOf(['']), - ]), - visibilityLanguageProficiencies: PropTypes.oneOf(['private', 'all_users']), - editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']), - saveState: PropTypes.string, - error: PropTypes.string, - sortedLanguages: PropTypes.arrayOf(PropTypes.shape({ - code: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - })).isRequired, - languageMessages: PropTypes.objectOf(PropTypes.string).isRequired, - changeHandler: PropTypes.func.isRequired, - submitHandler: PropTypes.func.isRequired, - closeHandler: PropTypes.func.isRequired, - openHandler: PropTypes.func.isRequired, -}; - -PreferredLanguage.defaultProps = { - editMode: 'static', - saveState: null, - languageProficiencies: [], - visibilityLanguageProficiencies: 'private', - error: null, -}; - -export default connect( - preferredLanguageSelector, - {}, -)(PreferredLanguage); diff --git a/src/profile-v2/forms/PreferredLanguage.messages.jsx b/src/profile-v2/forms/PreferredLanguage.messages.jsx deleted file mode 100644 index f24ed1b..0000000 --- a/src/profile-v2/forms/PreferredLanguage.messages.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - 'profile.preferredlanguage.empty': { - id: 'profile.preferredlanguage.empty', - defaultMessage: 'Add language', - description: 'Instructions when the user doesn’t have a preferred language set.', - }, - 'profile.preferredlanguage.label': { - id: 'profile.preferredlanguage.label', - defaultMessage: 'Primary language spoken', - description: 'The label for a user’s primary spoken language.', - }, -}); - -export default messages; diff --git a/src/profile-v2/forms/ProfileAvatar.jsx b/src/profile-v2/forms/ProfileAvatar.jsx deleted file mode 100644 index 0b0d968..0000000 --- a/src/profile-v2/forms/ProfileAvatar.jsx +++ /dev/null @@ -1,170 +0,0 @@ -import React, { useRef } from 'react'; -import PropTypes from 'prop-types'; -import { - Dropdown, - IconButton, - Icon, - Tooltip, - OverlayTrigger, -} from '@openedx/paragon'; -import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; - -import { PhotoCamera } from '@openedx/paragon/icons'; -import { ReactComponent as DefaultAvatar } from '../assets/avatar.svg'; -import messages from './ProfileAvatar.messages'; - -const ProfileAvatar = ({ - src, - isDefault, - onSave, - onDelete, - savePhotoState, - isEditable, -}) => { - const intl = useIntl(); - const fileInput = useRef(null); - const form = useRef(null); - - const onClickUpload = () => { - fileInput.current.click(); - }; - - const onClickDelete = () => { - onDelete(); - }; - - const onSubmit = (e) => { - if (e) { - e.preventDefault(); - } - onSave(new FormData(form.current)); - form.current.reset(); - }; - - const onChangeInput = () => { - onSubmit(); - }; - - const renderPending = () => ( - - -- ); - - const renderEditButton = () => { - if (!isEditable) { - return null; - } - - return ( --- ); - }; - - const renderAvatar = () => ( - isDefault ? ( -- -- {!isDefault ? ( - -- {intl.formatMessage(messages['profile.profileavatar.tooltip.edit'])} -
- ) : ( -- {intl.formatMessage(messages['profile.profileavatar.tooltip.upload'])} -
- )} - - )} - > -- - -- - {!isDefault && ( -- - - )} -- - ) : ( - - ) - ); - - return ( -
-- ); -}; - -ProfileAvatar.propTypes = { - src: PropTypes.string, - isDefault: PropTypes.bool, - onSave: PropTypes.func.isRequired, - onDelete: PropTypes.func.isRequired, - savePhotoState: PropTypes.oneOf([null, 'pending', 'complete', 'error']), - isEditable: PropTypes.bool, -}; - -ProfileAvatar.defaultProps = { - src: null, - isDefault: true, - savePhotoState: null, - isEditable: false, -}; - -export default ProfileAvatar; diff --git a/src/profile-v2/forms/ProfileAvatar.messages.jsx b/src/profile-v2/forms/ProfileAvatar.messages.jsx deleted file mode 100644 index 5076ad1..0000000 --- a/src/profile-v2/forms/ProfileAvatar.messages.jsx +++ /dev/null @@ -1,26 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - 'profile.image.alt.attribute': { - id: 'profile.image.alt.attribute', - defaultMessage: 'profile avatar', - description: 'Alt attribute for a profile photo', - }, - 'profile.profileavatar.change-button': { - id: 'profile.profileavatar.change-button', - defaultMessage: 'Change', - description: 'Change photo button', - }, - 'profile.profileavatar.tooltip.edit': { - id: 'profile.profileavatar.tooltip.edit', - defaultMessage: 'Edit photo', - description: 'Tooltip for edit photo button', - }, - 'profile.profileavatar.tooltip.upload': { - id: 'profile.profileavatar.tooltip.upload', - defaultMessage: 'Upload photo', - description: 'Tooltip for upload photo button', - }, -}); - -export default messages; diff --git a/src/profile-v2/forms/SocialLinks.jsx b/src/profile-v2/forms/SocialLinks.jsx deleted file mode 100644 index 60677fc..0000000 --- a/src/profile-v2/forms/SocialLinks.jsx +++ /dev/null @@ -1,258 +0,0 @@ -import React, { useState } from 'react'; -import PropTypes from 'prop-types'; -import { Alert } from '@openedx/paragon'; -import { connect } from 'react-redux'; -import { faTwitter, faFacebook, faLinkedin } from '@fortawesome/free-brands-svg-icons'; -import { FormattedMessage } from '@edx/frontend-platform/i18n'; -import classNames from 'classnames'; - -import FormControls from './elements/FormControls'; -import EditableItemHeader from './elements/EditableItemHeader'; -import EmptyContent from './elements/EmptyContent'; -import SwitchContent from './elements/SwitchContent'; - -import { editableFormSelector } from '../data/selectors'; -import { useIsVisibilityEnabled } from '../data/hooks'; - -const platformDisplayInfo = { - facebook: { - icon: faFacebook, - name: 'Facebook', - }, - twitter: { - icon: faTwitter, - name: 'X', - }, - linkedin: { - icon: faLinkedin, - name: 'LinkedIn', - }, -}; - -const SocialLinks = ({ - formId, - socialLinks, - draftSocialLinksByPlatform, - visibilitySocialLinks, - editMode, - saveState, - error, - changeHandler, - submitHandler, - closeHandler, - openHandler, -}) => { - const isVisibilityEnabled = useIsVisibilityEnabled(); - const [activePlatform, setActivePlatform] = useState(null); - - const mergeWithDrafts = (newSocialLink) => { - const knownPlatforms = ['twitter', 'facebook', 'linkedin']; - const updated = []; - knownPlatforms.forEach((platform) => { - if (newSocialLink.platform === platform) { - updated.push(newSocialLink); - } else if (draftSocialLinksByPlatform[platform] !== undefined) { - updated.push(draftSocialLinksByPlatform[platform]); - } - }); - return updated; - }; - - const handleChange = (e) => { - const { name, value } = e.target; - if (name !== 'visibilitySocialLinks') { - changeHandler( - 'socialLinks', - mergeWithDrafts({ - platform: name, - socialLink: value, - }), - ); - } else { - changeHandler(name, value); - } - }; - - const handleSubmit = (e) => { - e.preventDefault(); - submitHandler(formId); - setActivePlatform(null); - }; - - const handleClose = () => { - closeHandler(formId); - setActivePlatform(null); - }; - - const handleOpen = (platform) => { - openHandler(formId); - setActivePlatform(platform); - }; - - const renderPlatformContent = (platform, socialLink, isEditing) => { - if (isEditing) { - return ( - - ); - } - if (socialLink) { - return ( -- {savePhotoState === 'pending' && renderPending()} - {renderAvatar()} -- {renderEditButton()} - --- ); - } - return ( -handleOpen(platform)} - showVisibility={visibilitySocialLinks !== null && isVisibilityEnabled} - visibility={visibilitySocialLinks} - /> - handleOpen(platform)}> - Add {platformDisplayInfo[platform].name} - - ); - }; - - return ( -- - {socialLinks.map(({ platform }) => ( -- - ), - static: ( --- ))} -- {platformDisplayInfo[platform].name} -
-handleOpen(platform)}> - -- -- ), - editable: ( -- {socialLinks - .filter(({ socialLink }) => Boolean(socialLink)) - .map(({ platform, socialLink }) => ( ---- ))} -- {platformDisplayInfo[platform].name} -
-- -- ), - editing: ( -- {socialLinks.map(({ platform, socialLink }) => ( ---- ))} -- {platformDisplayInfo[platform].name} -
- {renderPlatformContent(platform, socialLink, activePlatform === platform)} --- ), - }} - /> - ); -}; - -SocialLinks.propTypes = { - formId: PropTypes.string.isRequired, - socialLinks: PropTypes.arrayOf(PropTypes.shape({ - platform: PropTypes.string, - socialLink: PropTypes.string, - })).isRequired, - draftSocialLinksByPlatform: PropTypes.objectOf(PropTypes.shape({ - platform: PropTypes.string, - socialLink: PropTypes.string, - })), - visibilitySocialLinks: PropTypes.oneOf(['private', 'all_users']), - editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']), - saveState: PropTypes.string, - error: PropTypes.string, - changeHandler: PropTypes.func.isRequired, - submitHandler: PropTypes.func.isRequired, - closeHandler: PropTypes.func.isRequired, - openHandler: PropTypes.func.isRequired, -}; - -SocialLinks.defaultProps = { - editMode: 'static', - saveState: null, - draftSocialLinksByPlatform: {}, - visibilitySocialLinks: 'private', - error: null, -}; - -export default connect( - editableFormSelector, - {}, -)(SocialLinks); diff --git a/src/profile-v2/forms/SocialLinks.messages.jsx b/src/profile-v2/forms/SocialLinks.messages.jsx deleted file mode 100644 index 51bf873..0000000 --- a/src/profile-v2/forms/SocialLinks.messages.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - 'profile.sociallinks.social.links': { - id: 'profile.sociallinks.social.links', - defaultMessage: 'Social Links', - description: 'A section of a user profile', - }, -}); - -export default messages; diff --git a/src/profile-v2/forms/elements/EditButton.jsx b/src/profile-v2/forms/elements/EditButton.jsx deleted file mode 100644 index 80cb479..0000000 --- a/src/profile-v2/forms/elements/EditButton.jsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { EditOutline } from '@openedx/paragon/icons'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { Button, OverlayTrigger, Tooltip } from '@openedx/paragon'; -import messages from './EditButton.messages'; - -const EditButton = ({ - onClick, className, style, intl, -}) => ( -- {socialLinks.map(({ platform, socialLink }) => ( ---- ))} -- {platformDisplayInfo[platform].name} -
- {renderPlatformContent(platform, socialLink, activePlatform === platform)} -- -); - -export default injectIntl(EditButton); - -EditButton.propTypes = { - onClick: PropTypes.func.isRequired, - className: PropTypes.string, - style: PropTypes.object, // eslint-disable-line - intl: intlShape.isRequired, -}; - -EditButton.defaultProps = { - className: null, - style: null, -}; diff --git a/src/profile-v2/forms/elements/EditButton.messages.jsx b/src/profile-v2/forms/elements/EditButton.messages.jsx deleted file mode 100644 index ed1ac7b..0000000 --- a/src/profile-v2/forms/elements/EditButton.messages.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - 'profile.editbutton.edit': { - id: 'profile.editbutton.edit', - defaultMessage: 'Edit', - description: 'A button label', - }, -}); - -export default messages; diff --git a/src/profile-v2/forms/elements/EditableItemHeader.jsx b/src/profile-v2/forms/elements/EditableItemHeader.jsx deleted file mode 100644 index 5381e3a..0000000 --- a/src/profile-v2/forms/elements/EditableItemHeader.jsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -import classNames from 'classnames'; -import EditButton from './EditButton'; -import { Visibility } from './Visibility'; -import { useIsOnMobileScreen } from '../../data/hooks'; - -const EditableItemHeader = ({ - content, - showVisibility, - visibility, - showEditButton, - onClickEdit, - headingId, -}) => { - const isMobileView = useIsOnMobileScreen(); - return ( - <> -- {intl.formatMessage(messages['profile.editbutton.edit'])} -
- - )} - > - ------ {content} -
-- {showEditButton ?-: null} - - {showVisibility ?- > - ); -}; - -export default EditableItemHeader; - -EditableItemHeader.propTypes = { - onClickEdit: PropTypes.func, - showVisibility: PropTypes.bool, - showEditButton: PropTypes.bool, - content: PropTypes.node, - visibility: PropTypes.oneOf(['private', 'all_users']), - headingId: PropTypes.string, -}; - -EditableItemHeader.defaultProps = { - onClickEdit: () => { - }, - showVisibility: false, - showEditButton: false, - content: '', - visibility: 'private', - headingId: null, -}; diff --git a/src/profile-v2/forms/elements/EmptyContent.jsx b/src/profile-v2/forms/elements/EmptyContent.jsx deleted file mode 100644 index 3978d80..0000000 --- a/src/profile-v2/forms/elements/EmptyContent.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faPlus } from '@fortawesome/free-solid-svg-icons'; - -const EmptyContent = ({ children, onClick, showPlusIcon }) => ( -: null} -
- {onClick ? ( - - ) : children} --); - -export default EmptyContent; - -EmptyContent.propTypes = { - onClick: PropTypes.func, - children: PropTypes.node, - showPlusIcon: PropTypes.bool, -}; - -EmptyContent.defaultProps = { - onClick: null, - children: null, - showPlusIcon: true, -}; diff --git a/src/profile-v2/forms/elements/FormControls.jsx b/src/profile-v2/forms/elements/FormControls.jsx deleted file mode 100644 index ddb2866..0000000 --- a/src/profile-v2/forms/elements/FormControls.jsx +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Button, StatefulButton } from '@openedx/paragon'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; - -import messages from './FormControls.messages'; - -import { VisibilitySelect } from './Visibility'; -import { useIsVisibilityEnabled } from '../../data/hooks'; - -const FormControls = ({ - cancelHandler, changeHandler, visibility, visibilityId, saveState, intl, -}) => { - const buttonState = saveState === 'error' ? null : saveState; - const isVisibilityEnabled = useIsVisibilityEnabled(); - - return ( -- {isVisibilityEnabled && ( -- ); -}; - -export default injectIntl(FormControls); - -FormControls.propTypes = { - saveState: PropTypes.oneOf([null, 'pending', 'complete', 'error']), - visibility: PropTypes.oneOf(['private', 'all_users']), - visibilityId: PropTypes.string.isRequired, - cancelHandler: PropTypes.func.isRequired, - changeHandler: PropTypes.func.isRequired, - - intl: intlShape.isRequired, -}; - -FormControls.defaultProps = { - visibility: 'private', - saveState: null, -}; diff --git a/src/profile-v2/forms/elements/FormControls.messages.jsx b/src/profile-v2/forms/elements/FormControls.messages.jsx deleted file mode 100644 index f4f5953..0000000 --- a/src/profile-v2/forms/elements/FormControls.messages.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - 'profile.formcontrols.who.can.see': { - id: 'profile.formcontrols.who.can.see', - defaultMessage: 'Who can see this:', - description: 'What users can see this area?', - }, - 'profile.formcontrols.button.cancel': { - id: 'profile.formcontrols.button.cancel', - defaultMessage: 'Cancel', - description: 'A button label', - }, - 'profile.formcontrols.button.save': { - id: 'profile.formcontrols.button.save', - defaultMessage: 'Save', - description: 'A button label', - }, - 'profile.formcontrols.button.saving': { - id: 'profile.formcontrols.button.saving', - defaultMessage: 'Saving', - description: 'A button label', - }, - 'profile.formcontrols.button.saved': { - id: 'profile.formcontrols.button.saved', - defaultMessage: 'Saved', - description: 'A button label', - }, -}); - -export default messages; diff --git a/src/profile-v2/forms/elements/SwitchContent.jsx b/src/profile-v2/forms/elements/SwitchContent.jsx deleted file mode 100644 index 3bb1de5..0000000 --- a/src/profile-v2/forms/elements/SwitchContent.jsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { TransitionReplace } from '@openedx/paragon'; - -const onChildExit = (htmlNode) => { - if (htmlNode.contains(document.activeElement)) { - const enteringChild = htmlNode.previousSibling || htmlNode.nextSibling; - - if (!enteringChild) { - return; - } - - const focusableElements = enteringChild.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); - if (focusableElements.length) { - focusableElements[0].focus(); - } - } -}; - -const SwitchContent = ({ expression, cases, className }) => { - const getContent = (caseKey) => { - if (cases[caseKey]) { - if (typeof cases[caseKey] === 'string') { - return getContent(cases[caseKey]); - } - return React.cloneElement(cases[caseKey], { key: caseKey }); - } - if (cases.default) { - if (typeof cases.default === 'string') { - return getContent(cases.default); - } - React.cloneElement(cases.default, { key: 'default' }); - } - - return null; - }; - - return ( -- -- )} -- --- ----{ - // Swallow clicks if the state is pending. - // We do this instead of disabling the button to prevent - // it from losing focus (disabled elements cannot have focus). - // Disabling it would causes upstream issues in focus management. - // Swallowing the onSubmit event on the form would be better, but - // we would have to add that logic for every field given our - // current structure of the application. - if (buttonState === 'pending') { - e.preventDefault(); - } - }} - disabledStates={[]} - /> - - {getContent(expression)} - - ); -}; - -SwitchContent.propTypes = { - expression: PropTypes.string, - cases: PropTypes.objectOf(PropTypes.node).isRequired, - className: PropTypes.string, -}; - -SwitchContent.defaultProps = { - expression: null, - className: null, -}; - -export default SwitchContent; diff --git a/src/profile-v2/forms/elements/Visibility.jsx b/src/profile-v2/forms/elements/Visibility.jsx deleted file mode 100644 index cfe67a2..0000000 --- a/src/profile-v2/forms/elements/Visibility.jsx +++ /dev/null @@ -1,76 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { getConfig } from '@edx/frontend-platform'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faEyeSlash, faEye } from '@fortawesome/free-regular-svg-icons'; - -import messages from './Visibility.messages'; - -const Visibility = ({ to, intl }) => { - const icon = to === 'private' ? faEyeSlash : faEye; - const label = to === 'private' - ? intl.formatMessage(messages['profile.visibility.who.just.me']) - : intl.formatMessage(messages['profile.visibility.who.everyone'], { siteName: getConfig().SITE_NAME }); - - return ( - -{label} - - ); -}; - -Visibility.propTypes = { - to: PropTypes.oneOf(['private', 'all_users']), - - intl: intlShape.isRequired, -}; -Visibility.defaultProps = { - to: 'private', -}; - -const VisibilitySelect = ({ intl, className, ...props }) => { - const { value } = props; - const icon = value === 'private' ? faEyeSlash : faEye; - - return ( - - - - - - - ); -}; - -VisibilitySelect.propTypes = { - id: PropTypes.string, - className: PropTypes.string, - name: PropTypes.string, - value: PropTypes.oneOf(['private', 'all_users']), - onChange: PropTypes.func, - - intl: intlShape.isRequired, -}; -VisibilitySelect.defaultProps = { - id: null, - className: null, - name: 'visibility', - value: null, - onChange: null, -}; - -const intlVisibility = injectIntl(Visibility); -const intlVisibilitySelect = injectIntl(VisibilitySelect); - -export { - intlVisibility as Visibility, - intlVisibilitySelect as VisibilitySelect, -}; diff --git a/src/profile-v2/forms/elements/Visibility.messages.jsx b/src/profile-v2/forms/elements/Visibility.messages.jsx deleted file mode 100644 index 06a4450..0000000 --- a/src/profile-v2/forms/elements/Visibility.messages.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - 'profile.visibility.who.just.me': { - id: 'profile.visibility.who.just.me', - defaultMessage: 'Just me', - description: 'What users can see this area?', - }, - 'profile.visibility.who.everyone': { - id: 'profile.visibility.who.everyone', - defaultMessage: 'Everyone on {siteName}', - description: 'What users can see this area?', - }, -}); - -export default messages; diff --git a/src/profile-v2/index.js b/src/profile-v2/index.js deleted file mode 100644 index 4cb72e8..0000000 --- a/src/profile-v2/index.js +++ /dev/null @@ -1,5 +0,0 @@ -export { default as reducer } from './data/reducers'; -export { default as saga } from './data/sagas'; -export { default as ProfilePage } from './ProfilePage'; -export { default as NotFoundPage } from './NotFoundPage'; -export { default as messages } from './ProfilePage.messages'; diff --git a/src/profile-v2/index.scss b/src/profile-v2/index.scss deleted file mode 100644 index 87907c2..0000000 --- a/src/profile-v2/index.scss +++ /dev/null @@ -1,265 +0,0 @@ -.word-break-all { - word-break: break-all !important; -} - -// TODO: Update edx-bootstrap theme to incorporate these edits. -.btn, a.btn { - text-decoration: none; - &:hover { - text-decoration: none; - } -} -.btn-link { - text-decoration: underline; - &:hover { - text-decoration: underline; - } -} - -.profile-page-bg-banner { - height: 298px; - width: 100%; - background-image: url('./assets/dot-pattern-light.png'); - background-repeat: repeat-x; - background-size: auto 85%; -} - -.icon-visibility-off { - height: 1rem; - color: var(--pgn-color-gray-500); -} - -.profile-page { - .edit-section-header { - font-size: var(--pgn-typography-font-size-h4-base); - display: block; - font-weight: 400; - letter-spacing: 0; - margin: 0; - line-height: 2.25rem; - } - - label.edit-section-header { - margin-bottom: calc(var(--pgn-spacing-spacer-base) * .5); - } - - .profile-avatar-wrap { - @media (--pgn-size-breakpoint-min-width-md) { - max-width: 12rem; - margin-right: 0; - height: auto; - } - } - - .profile-avatar-button { - position: absolute; - left: 76px; - top: 76px; - } - - .profile-avatar-menu-container { - background: rgba(0,0,0,.65); - position: absolute; - width: 100%; - height: 100%; - display: flex; - justify-content: center; - align-items: center; - border-radius: 50%; - - @media (--pgn-size-breakpoint-min-width-md) { - background: linear-gradient(to top, rgba(0,0,0,.65) 4rem, rgba(0,0,0,0) 4rem); - align-items: flex-end; - } - - .btn { - text-decoration: none; - @media (--pgn-size-breakpoint-min-width-md) { - margin-bottom: 1.2rem; - } - } - - .dropdown { - @media (--pgn-size-breakpoint-min-width-md) { - margin-bottom: 1.2rem; - } - - .btn { - color:var(--pgn-color-white);; - background: transparent; - border-color: transparent; - margin: 0; - } - } - } - - .profile-avatar { - width: 7.5rem; - height: 7.5rem; - position: relative; - - @media (--pgn-size-breakpoint-min-width-md) { - width: 7.5rem; - height: 7.5rem; - } - - .profile-avatar-edit-button { - border: none; - position: absolute; - height: 100%; - left: 0; - width: 100%; - bottom: 0; - display: flex; - justify-content: center; - padding-top: .1rem; - font-weight: 600; - background: rgba(0,0,0,.5); - border-radius:0; - transition: opacity 200ms ease; - - @media (--pgn-size-breakpoint-min-width-md) { - height: 4rem; - } - - &:focus, &:hover, &:active, &.active { - opacity: 1; - } - } - } - - .certificate { - background-color: #F3F1ED; - border-radius: 0.75rem; - overflow: hidden; - border: 1px #E7E4DB solid; - - .certificate-type-illustration { - position: absolute; - top: 1rem; - right: 1rem; - bottom: 0; - width: 15.15rem; - opacity: .06; - background-size: 90%; - background-repeat: no-repeat; - background-position: right top; - } - } -} - -.info-icon { - width: 1.5rem; - height: 1.5rem; - padding-left: 0.125rem; -} - -.max-width-32em { - max-width: 32em; -} - -.height-50vh { - height: 50vh; -} - -// Todo: Move the following to edx-paragon - -.btn-rounded { - border-radius: 100px; -} - -.min-width-179px { - min-width: 179px; -} - -.max-width-304px{ - max-width: 304px; -} - -.width-314px { - width: 314px; -} - -.w-90{ - max-width: 90%; -} - -.width-24px{ - width: 24px; -} - -.height-42px { - height: 42px; -} - -.rounded-75 { - border-radius: 0.75rem; -} - -.pt-40px{ - padding-top: 40px; -} - -.pl-40px { - padding-left: 40px; -} - -.py-10px{ - padding-top: 10px; - padding-bottom: 10px; -} - -.py-36px { - padding-top: 36px; - padding-bottom: 36px; -} - -.px-120px { - padding-left: 120px; - padding-right: 120px; -} - -.px-40px { - padding-left: 40px; - padding-right: 40px; -} - -.g-15rem { - gap: 1.5rem; -} - -.g-5rem { - gap: 0.5rem; -} - -.g-1rem { - gap: 1rem; -} - -.g-3rem { - gap: 3rem; -} - -.color-black { - color: #000; -} - -.bg-color-grey-FBFAF9 { - background-color: #FBFAF9; -} - -.background-black-65 { - background-color: rgba(0,0,0,.65) -} - -.object-fit-cover { - object-fit: cover; -} - -.lh-36px { - line-height: 36px; -} - -.overflowWrap-breakWord { - overflow-wrap: break-word; -} diff --git a/src/profile-v2/utils.js b/src/profile-v2/utils.js deleted file mode 100644 index 4a68d4d..0000000 --- a/src/profile-v2/utils.js +++ /dev/null @@ -1,69 +0,0 @@ -import camelCase from 'lodash.camelcase'; -import snakeCase from 'lodash.snakecase'; - -export function modifyObjectKeys(object, modify) { - if ( - object === undefined - || object === null - || (typeof object !== 'object' && !Array.isArray(object)) - ) { - return object; - } - - if (Array.isArray(object)) { - return object.map(value => modifyObjectKeys(value, modify)); - } - - const result = {}; - Object.entries(object).forEach(([key, value]) => { - result[modify(key)] = modifyObjectKeys(value, modify); - }); - return result; -} - -export function camelCaseObject(object) { - return modifyObjectKeys(object, camelCase); -} - -export function snakeCaseObject(object) { - return modifyObjectKeys(object, snakeCase); -} - -export function convertKeyNames(object, nameMap) { - const transformer = key => (nameMap[key] === undefined ? key : nameMap[key]); - - return modifyObjectKeys(object, transformer); -} - -/** - * Helper class to save time when writing out action types for asynchronous methods. Also helps - * ensure that actions are namespaced. - * - * TODO: Put somewhere common to it can be used by other MFEs. - */ -export class AsyncActionType { - constructor(topic, name) { - this.topic = topic; - this.name = name; - } - - get BASE() { - return `${this.topic}__${this.name}`; - } - - get BEGIN() { - return `${this.topic}__${this.name}__BEGIN`; - } - - get SUCCESS() { - return `${this.topic}__${this.name}__SUCCESS`; - } - - get FAILURE() { - return `${this.topic}__${this.name}__FAILURE`; - } - - get RESET() { - return `${this.topic}__${this.name}__RESET`; - } -} diff --git a/src/profile-v2/utils.test.js b/src/profile-v2/utils.test.js deleted file mode 100644 index c015e0e..0000000 --- a/src/profile-v2/utils.test.js +++ /dev/null @@ -1,103 +0,0 @@ -import { - AsyncActionType, - modifyObjectKeys, - camelCaseObject, - snakeCaseObject, - convertKeyNames, -} from './utils'; - -describe('modifyObjectKeys', () => { - it('should use the provided modify function to change all keys in and object and its children', () => { - function meowKeys(key) { - return `${key}Meow`; - } - - const result = modifyObjectKeys( - { - one: undefined, - two: null, - three: '', - four: 0, - five: NaN, - six: [1, 2, { seven: 'woof' }], - eight: { nine: { ten: 'bark' }, eleven: true }, - }, - meowKeys, - ); - - expect(result).toEqual({ - oneMeow: undefined, - twoMeow: null, - threeMeow: '', - fourMeow: 0, - fiveMeow: NaN, - sixMeow: [1, 2, { sevenMeow: 'woof' }], - eightMeow: { nineMeow: { tenMeow: 'bark' }, elevenMeow: true }, - }); - }); -}); - -describe('camelCaseObject', () => { - it('should make everything camelCase', () => { - const result = camelCaseObject({ - what_now: 'brown cow', - but_who: { says_you_people: 'okay then', but_how: { will_we_even_know: 'the song is over' } }, - 'dot.dot.dot': 123, - }); - - expect(result).toEqual({ - whatNow: 'brown cow', - butWho: { saysYouPeople: 'okay then', butHow: { willWeEvenKnow: 'the song is over' } }, - dotDotDot: 123, - }); - }); -}); - -describe('snakeCaseObject', () => { - it('should make everything snake_case', () => { - const result = snakeCaseObject({ - whatNow: 'brown cow', - butWho: { saysYouPeople: 'okay then', butHow: { willWeEvenKnow: 'the song is over' } }, - 'dot.dot.dot': 123, - }); - - expect(result).toEqual({ - what_now: 'brown cow', - but_who: { says_you_people: 'okay then', but_how: { will_we_even_know: 'the song is over' } }, - dot_dot_dot: 123, - }); - }); -}); - -describe('convertKeyNames', () => { - it('should replace the specified keynames', () => { - const result = convertKeyNames( - { - one: { two: { three: 'four' } }, - five: 'six', - }, - { - two: 'blue', - five: 'alive', - seven: 'heaven', - }, - ); - - expect(result).toEqual({ - one: { blue: { three: 'four' } }, - alive: 'six', - }); - }); -}); - -describe('AsyncActionType', () => { - it('should return well formatted action strings', () => { - const actionType = new AsyncActionType('HOUSE_CATS', 'START_THE_RACE'); - - expect(actionType.BASE).toBe('HOUSE_CATS__START_THE_RACE'); - expect(actionType.BEGIN).toBe('HOUSE_CATS__START_THE_RACE__BEGIN'); - expect(actionType.SUCCESS).toBe('HOUSE_CATS__START_THE_RACE__SUCCESS'); - expect(actionType.FAILURE).toBe('HOUSE_CATS__START_THE_RACE__FAILURE'); - expect(actionType.RESET).toBe('HOUSE_CATS__START_THE_RACE__RESET'); - }); -}); diff --git a/src/profile/AgeMessage.jsx b/src/profile/AgeMessage.jsx deleted file mode 100644 index c7f90fb..0000000 --- a/src/profile/AgeMessage.jsx +++ /dev/null @@ -1,43 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Alert } from '@openedx/paragon'; -import { FormattedMessage } from '@edx/frontend-platform/i18n'; -import { getConfig } from '@edx/frontend-platform'; - -const AgeMessage = ({ accountSettingsUrl }) => ( - - -); - -AgeMessage.propTypes = { - accountSettingsUrl: PropTypes.string.isRequired, -}; - -export default AgeMessage; diff --git a/src/profile/Banner.jsx b/src/profile/Banner.jsx deleted file mode 100644 index de8ff9d..0000000 --- a/src/profile/Banner.jsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -const Banner = () => ; - -export default Banner; diff --git a/src/profile-v2/CertificateCard.jsx b/src/profile/CertificateCard.jsx similarity index 100% rename from src/profile-v2/CertificateCard.jsx rename to src/profile/CertificateCard.jsx diff --git a/src/profile-v2/Certificates.jsx b/src/profile/Certificates.jsx similarity index 100% rename from src/profile-v2/Certificates.jsx rename to src/profile/Certificates.jsx diff --git a/src/profile-v2/Certificates.messages.jsx b/src/profile/Certificates.messages.jsx similarity index 100% rename from src/profile-v2/Certificates.messages.jsx rename to src/profile/Certificates.messages.jsx diff --git a/src/profile/DateJoined.jsx b/src/profile/DateJoined.jsx index 05ce2d6..5b02d4b 100644 --- a/src/profile/DateJoined.jsx +++ b/src/profile/DateJoined.jsx @@ -1,23 +1,21 @@ -import React from 'react'; +import React, { memo } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n'; const DateJoined = ({ date }) => { - if (date == null) { - return null; - } + if (!date) { return null; } return ( -- -- - - -- +
+ ); }; @@ -28,4 +26,4 @@ DateJoined.defaultProps = { date: null, }; -export default DateJoined; +export default memo(DateJoined); diff --git a/src/profile/NotFoundPage.jsx b/src/profile/NotFoundPage.jsx index 0963b15..b33f0db 100644 --- a/src/profile/NotFoundPage.jsx +++ b/src/profile/NotFoundPage.jsx @@ -3,7 +3,7 @@ import { FormattedMessage } from '@edx/frontend-platform/i18n'; const NotFoundPage = () => (, + year: , }} /> - -+
- {this.props.srMessage} - - ); - } - - render() { - return ( - -+ ), + editable: ( + <> +-+const PageLoading = ({ srMessage }) => ( +- {this.renderSrMessage()} --++); PageLoading.propTypes = { srMessage: PropTypes.string.isRequired, }; + +export default PageLoading; diff --git a/src/profile/ProfilePage.jsx b/src/profile/ProfilePage.jsx index 2dfe72d..2411755 100644 --- a/src/profile/ProfilePage.jsx +++ b/src/profile/ProfilePage.jsx @@ -1,14 +1,20 @@ -import React from 'react'; +import React, { + useEffect, useState, useContext, useCallback, +} from 'react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics'; -import { ensureConfig, getConfig } from '@edx/frontend-platform'; +import { ensureConfig } from '@edx/frontend-platform'; import { AppContext } from '@edx/frontend-platform/react'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { Alert, Hyperlink } from '@openedx/paragon'; +import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; +import { + Alert, Hyperlink, OverlayTrigger, Tooltip, +} from '@openedx/paragon'; +import { InfoOutline } from '@openedx/paragon/icons'; +import classNames from 'classnames'; -// Actions import { fetchProfile, saveProfile, @@ -19,7 +25,6 @@ import { updateDraft, } from './data/actions'; -// Components import ProfileAvatar from './forms/ProfileAvatar'; import Name from './forms/Name'; import Country from './forms/Country'; @@ -27,120 +32,122 @@ import PreferredLanguage from './forms/PreferredLanguage'; import Education from './forms/Education'; import SocialLinks from './forms/SocialLinks'; import Bio from './forms/Bio'; -import Certificates from './forms/Certificates'; -import AgeMessage from './AgeMessage'; import DateJoined from './DateJoined'; -import UsernameDescription from './UsernameDescription'; +import UserCertificateSummary from './UserCertificateSummary'; import PageLoading from './PageLoading'; -import Banner from './Banner'; -import LearningGoal from './forms/LearningGoal'; +import Certificates from './Certificates'; -// Selectors import { profilePageSelector } from './data/selectors'; - -// i18n import messages from './ProfilePage.messages'; - import withParams from '../utils/hoc'; +import { useIsOnMobileScreen, useIsOnTabletScreen } from './data/hooks'; -ensureConfig(['CREDENTIALS_BASE_URL', 'LMS_BASE_URL'], 'ProfilePage'); +ensureConfig(['CREDENTIALS_BASE_URL', 'LMS_BASE_URL', 'ACCOUNT_SETTINGS_URL'], 'ProfilePage'); -class ProfilePage extends React.Component { - constructor(props, context) { - super(props, context); +const ProfilePage = ({ params }) => { + const dispatch = useDispatch(); + const intl = useIntl(); + const context = useContext(AppContext); + const { + dateJoined, + courseCertificates, + name, + visibilityName, + profileImage, + savePhotoState, + isLoadingProfile, + photoUploadError, + country, + visibilityCountry, + levelOfEducation, + visibilityLevelOfEducation, + socialLinks, + draftSocialLinksByPlatform, + visibilitySocialLinks, + languageProficiencies, + visibilityLanguageProficiencies, + bio, + visibilityBio, + saveState, + username, + } = useSelector(profilePageSelector); - const credentialsBaseUrl = context.config.CREDENTIALS_BASE_URL; - this.state = { - viewMyRecordsUrl: credentialsBaseUrl ? `${credentialsBaseUrl}/records` : null, - accountSettingsUrl: context.config.ACCOUNT_SETTINGS_URL, - }; + const navigate = useNavigate(); + const [viewMyRecordsUrl, setViewMyRecordsUrl] = useState(null); + const isMobileView = useIsOnMobileScreen(); + const isTabletView = useIsOnTabletScreen(); - this.handleSaveProfilePhoto = this.handleSaveProfilePhoto.bind(this); - this.handleDeleteProfilePhoto = this.handleDeleteProfilePhoto.bind(this); - this.handleClose = this.handleClose.bind(this); - this.handleOpen = this.handleOpen.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.handleChange = this.handleChange.bind(this); - } + useEffect(() => { + const { CREDENTIALS_BASE_URL } = context.config; + if (CREDENTIALS_BASE_URL) { + setViewMyRecordsUrl(`${CREDENTIALS_BASE_URL}/records`); + } - componentDidMount() { - this.props.fetchProfile(this.props.params.username); + dispatch(fetchProfile(params.username)); sendTrackingLogEvent('edx.profile.viewed', { - username: this.props.params.username, + username: params.username, }); - } + }, [dispatch, params.username, context.config]); - handleSaveProfilePhoto(formData) { - this.props.saveProfilePhoto(this.context.authenticatedUser.username, formData); - } + useEffect(() => { + if (!username && saveState === 'error' && navigate) { + navigate('/notfound'); + } + }, [username, saveState, navigate]); - handleDeleteProfilePhoto() { - this.props.deleteProfilePhoto(this.context.authenticatedUser.username); - } + const authenticatedUserName = context.authenticatedUser.username; - handleClose(formId) { - this.props.closeForm(formId); - } + const handleSaveProfilePhoto = useCallback((formData) => { + dispatch(saveProfilePhoto(authenticatedUserName, formData)); + }, [dispatch, authenticatedUserName]); - handleOpen(formId) { - this.props.openForm(formId); - } + const handleDeleteProfilePhoto = useCallback(() => { + dispatch(deleteProfilePhoto(authenticatedUserName)); + }, [dispatch, authenticatedUserName]); - handleSubmit(formId) { - this.props.saveProfile(formId, this.context.authenticatedUser.username); - } + const handleClose = useCallback((formId) => { + dispatch(closeForm(formId)); + }, [dispatch]); - handleChange(name, value) { - this.props.updateDraft(name, value); - } + const handleOpen = useCallback((formId) => { + dispatch(openForm(formId)); + }, [dispatch]); - isYOBDisabled() { - const { yearOfBirth } = this.props; - const currentYear = new Date().getFullYear(); - const isAgeOrNotCompliant = !yearOfBirth || ((currentYear - yearOfBirth) < 13); + const handleSubmit = useCallback((formId) => { + dispatch(saveProfile(formId, authenticatedUserName)); + }, [dispatch, authenticatedUserName]); - return isAgeOrNotCompliant && getConfig().COLLECT_YEAR_OF_BIRTH !== 'true'; - } + const handleChange = useCallback((fieldName, value) => { + dispatch(updateDraft(fieldName, value)); + }, [dispatch]); - isAuthenticatedUserProfile() { - return this.props.params.username === this.context.authenticatedUser.username; - } + const isAuthenticatedUserProfile = () => params.username === authenticatedUserName; - // Inserted into the DOM in two places (for responsive layout) - renderViewMyRecordsButton() { - if (!(this.state.viewMyRecordsUrl && this.isAuthenticatedUserProfile())) { + const isBlockVisible = (blockInfo) => isAuthenticatedUserProfile() + || (!isAuthenticatedUserProfile() && Boolean(blockInfo)); + + const renderViewMyRecordsButton = () => { + if (!(viewMyRecordsUrl && isAuthenticatedUserProfile())) { return null; } return ( -+++ {srMessage && {srMessage}}- ); - } -} +- {this.props.intl.formatMessage(messages['profile.viewMyRecords'])} + + {intl.formatMessage(messages['profile.viewMyRecords'])} ); - } + }; - // Inserted into the DOM in two places (for responsive layout) - renderHeadingLockup() { - const { dateJoined } = this.props; - - return ( - -{this.props.params.username}
-- {this.isYOBDisabled() && } -
- - ); - } - - renderPhotoUploadErrorMessage() { - const { photoUploadError } = this.props; - - if (photoUploadError === null) { - return null; - } - - return ( + const renderPhotoUploadErrorMessage = () => ( + photoUploadError && (- ); - } + ) + ); - renderAgeMessage() { - const { requiresParentalConsent } = this.props; - const shouldShowAgeMessage = requiresParentalConsent && this.isAuthenticatedUserProfile(); + const commonFormProps = { + openHandler: handleOpen, + closeHandler: handleClose, + submitHandler: handleSubmit, + changeHandler: handleChange, + }; - if (!shouldShowAgeMessage) { - return null; - } - return@@ -148,227 +155,267 @@ class ProfilePage extends React.Component { ; - } - - renderContent() { - const { - profileImage, - name, - visibilityName, - country, - visibilityCountry, - levelOfEducation, - visibilityLevelOfEducation, - socialLinks, - draftSocialLinksByPlatform, - visibilitySocialLinks, - learningGoal, - visibilityLearningGoal, - languageProficiencies, - visibilityLanguageProficiencies, - courseCertificates, - visibilityCourseCertificates, - bio, - visibilityBio, - requiresParentalConsent, - isLoadingProfile, - username, - saveState, - navigate, - } = this.props; - - if (isLoadingProfile) { - return ; - } - - if (!username && saveState === 'error' && navigate) { - navigate('/notfound'); - } - - const commonFormProps = { - openHandler: this.handleOpen, - closeHandler: this.handleClose, - submitHandler: this.handleSubmit, - changeHandler: this.handleChange, - }; - - const isBlockVisible = (blockInfo) => this.isAuthenticatedUserProfile() - || (!this.isAuthenticatedUserProfile() && Boolean(blockInfo)); - - const isLanguageBlockVisible = isBlockVisible(languageProficiencies.length); - const isEducationBlockVisible = isBlockVisible(levelOfEducation); - const isSocialLinksBLockVisible = isBlockVisible(socialLinks.some((link) => link.socialLink !== null)); - const isBioBlockVisible = isBlockVisible(bio); - const isCertificatesBlockVisible = isBlockVisible(courseCertificates.length); - const isNameBlockVisible = isBlockVisible(name); - const isLocationBlockVisible = isBlockVisible(country); - - return ( - -- ), - editable: ( - <> --+ ), + editable: ( + <> +-+-++ return ( + + {isLoadingProfile ? ( +`; @@ -6653,19 +547,16 @@ exports[`+ ) : ( + <> + +-++++++ +++ {params.username} +
+ {isBlockVisible(name) && ( ++ {name} +
+ )} ++++ + + {renderViewMyRecordsButton()} +++ {renderPhotoUploadErrorMessage()} +-+ ); +}; ProfilePage.propTypes = { - // Account data + params: PropTypes.shape({ + username: PropTypes.string.isRequired, + }).isRequired, requiresParentalConsent: PropTypes.bool, dateJoined: PropTypes.string, username: PropTypes.string, - - // Bio form data bio: PropTypes.string, - yearOfBirth: PropTypes.number, - visibilityBio: PropTypes.string.isRequired, - - // Certificates form data + visibilityBio: PropTypes.string, courseCertificates: PropTypes.arrayOf(PropTypes.shape({ title: PropTypes.string, })), - visibilityCourseCertificates: PropTypes.string.isRequired, - - // Country form data country: PropTypes.string, - visibilityCountry: PropTypes.string.isRequired, - - // Education form data + visibilityCountry: PropTypes.string, levelOfEducation: PropTypes.string, - visibilityLevelOfEducation: PropTypes.string.isRequired, - - // Language proficiency form data + visibilityLevelOfEducation: PropTypes.string, languageProficiencies: PropTypes.arrayOf(PropTypes.shape({ code: PropTypes.string.isRequired, })), - visibilityLanguageProficiencies: PropTypes.string.isRequired, - - // Name form data + visibilityLanguageProficiencies: PropTypes.string, name: PropTypes.string, - visibilityName: PropTypes.string.isRequired, - - // Social links form data + visibilityName: PropTypes.string, socialLinks: PropTypes.arrayOf(PropTypes.shape({ platform: PropTypes.string, socialLink: PropTypes.string, @@ -377,41 +424,15 @@ ProfilePage.propTypes = { platform: PropTypes.string, socialLink: PropTypes.string, })), - visibilitySocialLinks: PropTypes.string.isRequired, - - // Learning Goal form data - learningGoal: PropTypes.string, - visibilityLearningGoal: PropTypes.string.isRequired, - - // Other data we need + visibilitySocialLinks: PropTypes.string, profileImage: PropTypes.shape({ src: PropTypes.string, isDefault: PropTypes.bool, }), saveState: PropTypes.oneOf([null, 'pending', 'complete', 'error']), savePhotoState: PropTypes.oneOf([null, 'pending', 'complete', 'error']), - isLoadingProfile: PropTypes.bool.isRequired, - - // Page state helpers + isLoadingProfile: PropTypes.bool, photoUploadError: PropTypes.objectOf(PropTypes.string), - - // Actions - fetchProfile: PropTypes.func.isRequired, - saveProfile: PropTypes.func.isRequired, - saveProfilePhoto: PropTypes.func.isRequired, - deleteProfilePhoto: PropTypes.func.isRequired, - openForm: PropTypes.func.isRequired, - closeForm: PropTypes.func.isRequired, - updateDraft: PropTypes.func.isRequired, - navigate: PropTypes.func.isRequired, - - // Router - params: PropTypes.shape({ - username: PropTypes.string.isRequired, - }).isRequired, - - // i18n - intl: intlShape.isRequired, }; ProfilePage.defaultProps = { @@ -421,28 +442,22 @@ ProfilePage.defaultProps = { photoUploadError: {}, profileImage: {}, name: null, - yearOfBirth: null, levelOfEducation: null, country: null, socialLinks: [], draftSocialLinksByPlatform: {}, bio: null, - learningGoal: null, languageProficiencies: [], - courseCertificates: null, + courseCertificates: [], requiresParentalConsent: null, dateJoined: null, + visibilityName: null, + visibilityCountry: null, + visibilityLevelOfEducation: null, + visibilitySocialLinks: null, + visibilityLanguageProficiencies: null, + visibilityBio: null, + isLoadingProfile: false, }; -export default connect( - profilePageSelector, - { - fetchProfile, - saveProfilePhoto, - deleteProfilePhoto, - saveProfile, - openForm, - closeForm, - updateDraft, - }, -)(injectIntl(withParams(ProfilePage))); +export default withParams(ProfilePage); diff --git a/src/profile/ProfilePage.messages.jsx b/src/profile/ProfilePage.messages.jsx index 4dfeef6..c7cec79 100644 --- a/src/profile/ProfilePage.messages.jsx +++ b/src/profile/ProfilePage.messages.jsx @@ -11,6 +11,16 @@ const messages = defineMessages({ defaultMessage: 'Profile loading...', description: 'Message displayed when the profile data is loading.', }, + 'profile.username': { + id: 'profile.username', + defaultMessage: 'Username', + description: 'Label for the username field.', + }, + 'profile.username.tooltip': { + id: 'profile.username.tooltip', + defaultMessage: 'The name that identifies you on edX. You cannot change your username.', + description: 'Tooltip for the username field.', + }, }); export default messages; diff --git a/src/profile/ProfilePage.test.jsx b/src/profile/ProfilePage.test.jsx index 7f1a771..d186c48 100644 --- a/src/profile/ProfilePage.test.jsx +++ b/src/profile/ProfilePage.test.jsx @@ -1,4 +1,3 @@ -/* eslint-disable global-require */ import { getConfig } from '@edx/frontend-platform'; import * as analytics from '@edx/frontend-platform/analytics'; import { AppContext } from '@edx/frontend-platform/react'; @@ -9,31 +8,38 @@ import PropTypes from 'prop-types'; import { Provider } from 'react-redux'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import { BrowserRouter, useNavigate } from 'react-router-dom'; +import { + MemoryRouter, + Routes, + Route, + useNavigate, +} from 'react-router-dom'; import messages from '../i18n'; import ProfilePage from './ProfilePage'; +import loadingApp from './__mocks__/loadingApp.mockStore'; +import viewOwnProfile from './__mocks__/viewOwnProfile.mockStore'; +import viewOtherProfile from './__mocks__/viewOtherProfile.mockStore'; +import invalidUser from './__mocks__/invalidUser.mockStore'; + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: jest.fn(), +})); const mockStore = configureMockStore([thunk]); + const storeMocks = { - loadingApp: require('./__mocks__/loadingApp.mockStore'), - invalidUser: require('./__mocks__/invalidUser.mockStore'), - viewOwnProfile: require('./__mocks__/viewOwnProfile.mockStore'), - viewOtherProfile: require('./__mocks__/viewOtherProfile.mockStore'), - savingEditedBio: require('./__mocks__/savingEditedBio.mockStore'), + loadingApp, + viewOwnProfile, + viewOtherProfile, + invalidUser, }; + const requiredProfilePageProps = { - fetchUserAccount: () => {}, - fetchProfile: () => {}, - saveProfile: () => {}, - saveProfilePhoto: () => {}, - deleteProfilePhoto: () => {}, - openField: () => {}, - closeField: () => {}, params: { username: 'staff' }, }; -// Mock language cookie Object.defineProperty(global.document, 'cookie', { writable: true, value: `${getConfig().LANGUAGE_PREFERENCE_COOKIE_NAME}=en`, @@ -65,54 +71,39 @@ configureI18n({ beforeEach(() => { analytics.sendTrackingLogEvent.mockReset(); + useNavigate.mockReset(); }); -const ProfileWrapper = ({ params, requiresParentalConsent }) => { - const navigate = useNavigate(); - return ( -- {this.renderHeadingLockup()} --- {this.renderViewMyRecordsButton()} +- {this.renderPhotoUploadErrorMessage()} -+-++++++ {isMobileView ? ( +
++ ) + : ( + + )} + +++++ {isBlockVisible(name) && ( ++++ {intl.formatMessage(messages['profile.username'])} +
++ ++ {intl.formatMessage(messages['profile.username.tooltip'])} +
+ + )} + > ++ + {params.username} +
++ )} + {isBlockVisible(country) && ( + + )} + {isBlockVisible((languageProficiencies || []).length) && ( + + )} + {isBlockVisible(levelOfEducation) && ( + + )} + + {isBlockVisible(bio) && ( +++ )} + {isBlockVisible((socialLinks || []).some((link) => link?.socialLink !== null)) && ( + + )} + -- ); - } - - render() { - return ( ---- {this.renderHeadingLockup()} --- {this.renderViewMyRecordsButton()} -- {isNameBlockVisible && ( -- )} - {isLocationBlockVisible && ( - - )} - {isLanguageBlockVisible && ( - - )} - {isEducationBlockVisible && ( - - )} - {isSocialLinksBLockVisible && ( - + + {isBlockVisible((courseCertificates || []).length) && ( +-)} - {!this.isYOBDisabled() && this.renderAgeMessage()} - {isBioBlockVisible && ( --- )} - {getConfig().ENABLE_SKILLS_BUILDER_PROFILE && ( - - )} - {isCertificatesBlockVisible && ( - - )} - -- ); - } -} - -ProfilePage.contextType = AppContext; + > + )} +- {this.renderContent()} - - ); -}; - -ProfileWrapper.propTypes = { - params: PropTypes.shape({}).isRequired, - requiresParentalConsent: PropTypes.bool.isRequired, -}; - const ProfilePageWrapper = ({ - contextValue, store, params, requiresParentalConsent, + contextValue, store, params, }) => ( - + ); ProfilePageWrapper.defaultProps = { + // eslint-disable-next-line react/default-props-match-prop-types params: { username: 'staff' }, - requiresParentalConsent: null, }; ProfilePageWrapper.propTypes = { contextValue: PropTypes.shape({}).isRequired, store: PropTypes.shape({}).isRequired, - params: PropTypes.shape({}), - requiresParentalConsent: PropTypes.bool, + params: PropTypes.shape({ + username: PropTypes.string.isRequired, + }).isRequired, }; describe(' - - +- + + +} + /> + ', () => { @@ -122,17 +113,12 @@ describe(' ', () => { authenticatedUser: { userId: null, username: null, administrator: false }, config: getConfig(), }; - const component = ; - const { container: tree } = render(component); - expect(tree).toMatchSnapshot(); - }); - - it('successfully redirected to not found page.', () => { - const contextValue = { - authenticatedUser: { userId: 123, username: 'staff', administrator: true }, - config: getConfig(), - }; - const component = ; + const component = ( + + ); const { container: tree } = render(component); expect(tree).toMatchSnapshot(); }); @@ -142,7 +128,12 @@ describe(' ', () => { authenticatedUser: { userId: 123, username: 'staff', administrator: true }, config: getConfig(), }; - const component = ; + const component = ( + + ); const { container: tree } = render(component); expect(tree).toMatchSnapshot(); }); @@ -152,7 +143,6 @@ describe(' ', () => { authenticatedUser: { userId: 123, username: 'staff', administrator: true }, config: getConfig(), }; - const component = ( ', () => { ...storeMocks.viewOtherProfile.profilePage, account: { ...storeMocks.viewOtherProfile.profilePage.account, - name: 'user', - country: 'EN', - bio: 'bio', - courseCertificates: ['course certificates'], - levelOfEducation: 'some level', - languageProficiencies: ['some lang'], - socialLinks: ['twitter'], - timeZone: 'time zone', - accountPrivacy: 'all_users', + name: 'Verified User', + country: 'US', + bio: 'About me', + courseCertificates: [{ title: 'Course 1' }], + levelOfEducation: 'bachelors', + languageProficiencies: [{ code: 'en' }], + socialLinks: [{ platform: 'twitter', socialLink: 'https://twitter.com/user' }], + }, + preferences: { + ...storeMocks.viewOtherProfile.profilePage.preferences, + visibilityName: 'all_users', + visibilityCountry: 'all_users', + visibilityLevelOfEducation: 'all_users', + visibilityLanguageProficiencies: 'all_users', + visibilitySocialLinks: 'all_users', + visibilityBio: 'all_users', }, }, })} - match={{ params: { username: 'verified' } }} // Override default match - /> - ); - const { container: tree } = render(component); - expect(tree).toMatchSnapshot(); - }); - - it('while saving an edited bio', () => { - const contextValue = { - authenticatedUser: { userId: 123, username: 'staff', administrator: true }, - config: getConfig(), - }; - const component = ( - - ); - const { container: tree } = render(component); - expect(tree).toMatchSnapshot(); - }); - - it('while saving an edited bio with error', () => { - const storeData = JSON.parse(JSON.stringify(storeMocks.savingEditedBio)); - storeData.profilePage.errors.bio = { userMessage: 'bio error' }; - const contextValue = { - authenticatedUser: { userId: 123, username: 'staff', administrator: true }, - config: getConfig(), - }; - const component = ( - - ); - const { container: tree } = render(component); - expect(tree).toMatchSnapshot(); - }); - - it('test country edit with error', () => { - const storeData = JSON.parse(JSON.stringify(storeMocks.savingEditedBio)); - storeData.profilePage.errors.country = { userMessage: 'country error' }; - storeData.profilePage.currentlyEditingField = 'country'; - const contextValue = { - authenticatedUser: { userId: 123, username: 'staff', administrator: true }, - config: getConfig(), - }; - const component = ( - - ); - const { container: tree } = render(component); - expect(tree).toMatchSnapshot(); - }); - - it('test education edit with error', () => { - const storeData = JSON.parse(JSON.stringify(storeMocks.savingEditedBio)); - storeData.profilePage.errors.levelOfEducation = { userMessage: 'education error' }; - storeData.profilePage.currentlyEditingField = 'levelOfEducation'; - const contextValue = { - authenticatedUser: { userId: 123, username: 'staff', administrator: true }, - config: getConfig(), - }; - const component = ( - - ); - const { container: tree } = render(component); - expect(tree).toMatchSnapshot(); - }); - - it('test preferreded language edit with error', () => { - const storeData = JSON.parse(JSON.stringify(storeMocks.savingEditedBio)); - storeData.profilePage.errors.languageProficiencies = { userMessage: 'preferred language error' }; - storeData.profilePage.currentlyEditingField = 'languageProficiencies'; - const contextValue = { - authenticatedUser: { userId: 123, username: 'staff', administrator: true }, - config: getConfig(), - }; - const component = ( - ); const { container: tree } = render(component); @@ -284,63 +195,24 @@ describe(' ', () => { const { container: tree } = render(component); expect(tree).toMatchSnapshot(); }); - it('test age message alert', () => { - const storeData = JSON.parse(JSON.stringify(storeMocks.viewOwnProfile)); - storeData.userAccount.requiresParentalConsent = true; - storeData.profilePage.account.requiresParentalConsent = true; - const contextValue = { - authenticatedUser: { userId: 123, username: 'staff', administrator: true }, - config: { ...getConfig(), COLLECT_YEAR_OF_BIRTH: true }, - }; - const { container } = render( - , - ); - expect(container.querySelector('.alert-info')).toHaveClass('show'); - }); - it('test photo error alert', () => { - const storeData = JSON.parse(JSON.stringify(storeMocks.viewOwnProfile)); - storeData.profilePage.errors.photo = { userMessage: 'error' }; - const contextValue = { - authenticatedUser: { userId: 123, username: 'staff', administrator: true }, - config: { ...getConfig(), COLLECT_YEAR_OF_BIRTH: true }, - }; - const { container } = render( - , - ); - - expect(container.querySelector('.alert-danger')).toHaveClass('show'); - }); - - it.each([ - ['test user with non-disabled country', 'PK'], - ['test user with disabled country', 'RU'], - ])('test user with %s', (_, accountCountry) => { - const storeData = JSON.parse(JSON.stringify(storeMocks.savingEditedBio)); - storeData.profilePage.errors.country = {}; - storeData.profilePage.currentlyEditingField = 'country'; - storeData.profilePage.disabledCountries = ['RU']; - storeData.profilePage.account.country = accountCountry; + it('successfully redirected to not found page', () => { const contextValue = { authenticatedUser: { userId: 123, username: 'staff', administrator: true }, config: getConfig(), }; + const navigate = jest.fn(); + useNavigate.mockReturnValue(navigate); const component = ( ); const { container: tree } = render(component); expect(tree).toMatchSnapshot(); + expect(navigate).toHaveBeenCalledWith('/notfound'); }); }); @@ -358,11 +230,30 @@ describe(' ', () => { />, ); - expect(analytics.sendTrackingLogEvent.mock.calls.length).toBe(1); - expect(analytics.sendTrackingLogEvent.mock.calls[0][0]).toEqual('edx.profile.viewed'); - expect(analytics.sendTrackingLogEvent.mock.calls[0][1]).toEqual({ + expect(analytics.sendTrackingLogEvent).toHaveBeenCalledTimes(1); + expect(analytics.sendTrackingLogEvent).toHaveBeenCalledWith('edx.profile.viewed', { username: 'test-username', }); }); }); + + describe('handles navigation', () => { + it('navigates to notfound on save error with no username', () => { + const contextValue = { + authenticatedUser: { userId: 123, username: 'staff', administrator: true }, + config: getConfig(), + }; + const navigate = jest.fn(); + useNavigate.mockReturnValue(navigate); + render( + , + ); + + expect(navigate).toHaveBeenCalledWith('/notfound'); + }); + }); }); diff --git a/src/profile-v2/UserCertificateSummary.jsx b/src/profile/UserCertificateSummary.jsx similarity index 100% rename from src/profile-v2/UserCertificateSummary.jsx rename to src/profile/UserCertificateSummary.jsx diff --git a/src/profile/UsernameDescription.jsx b/src/profile/UsernameDescription.jsx deleted file mode 100644 index 684eb71..0000000 --- a/src/profile/UsernameDescription.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import { FormattedMessage } from '@edx/frontend-platform/i18n'; -import { VisibilityOff } from '@openedx/paragon/icons'; -import { Icon } from '@openedx/paragon'; -import { getConfig } from '@edx/frontend-platform'; - -const UsernameDescription = () => ( - --); - -export default UsernameDescription; diff --git a/src/profile/__mocks__/invalidUser.mockStore.js b/src/profile/__mocks__/invalidUser.mockStore.js index 8643a21..253ef87 100644 --- a/src/profile/__mocks__/invalidUser.mockStore.js +++ b/src/profile/__mocks__/invalidUser.mockStore.js @@ -29,7 +29,7 @@ module.exports = { drafts: {}, isLoadingProfile: false, isAuthenticatedUserProfile: true, - countriesCodesList: [], + countriesCodesList: ['US', 'CA', 'GB', 'ME'] }, router: { location: { diff --git a/src/profile/__mocks__/loadingApp.mockStore.js b/src/profile/__mocks__/loadingApp.mockStore.js index dfef507..aaf1f63 100644 --- a/src/profile/__mocks__/loadingApp.mockStore.js +++ b/src/profile/__mocks__/loadingApp.mockStore.js @@ -29,6 +29,7 @@ module.exports = { drafts: {}, isLoadingProfile: true, isAuthenticatedUserProfile: true, + countriesCodesList: ['US', 'CA', 'GB', 'ME'] }, router: { location: { diff --git a/src/profile/__mocks__/savingEditedBio.mockStore.js b/src/profile/__mocks__/savingEditedBio.mockStore.js index 7ba9a52..a104762 100644 --- a/src/profile/__mocks__/savingEditedBio.mockStore.js +++ b/src/profile/__mocks__/savingEditedBio.mockStore.js @@ -126,7 +126,7 @@ module.exports = { ], drafts: {}, isLoadingProfile: false, - countriesCodesList: [], + disabledCountries: [], }, router: { location: { diff --git a/src/profile/__mocks__/viewOtherProfile.mockStore.js b/src/profile/__mocks__/viewOtherProfile.mockStore.js index b35f61d..7afdac1 100644 --- a/src/profile/__mocks__/viewOtherProfile.mockStore.js +++ b/src/profile/__mocks__/viewOtherProfile.mockStore.js @@ -81,12 +81,18 @@ module.exports = { gender: null, accountPrivacy: 'private' }, - preferences: {}, + preferences: { + visibilityName: 'all_users', + visibilityCountry: 'all_users', + visibilityLevelOfEducation: 'all_users', + visibilityLanguageProficiencies: 'all_users', + visibilitySocialLinks: 'all_users', + visibilityBio: 'all_users' + }, courseCertificates: [], drafts: {}, isLoadingProfile: false, - learningGoal: 'advance_career', - countriesCodesList: [], + countriesCodesList: ['US', 'CA', 'GB', 'ME'] }, router: { location: { diff --git a/src/profile/__mocks__/viewOwnProfile.mockStore.js b/src/profile/__mocks__/viewOwnProfile.mockStore.js index b40d8f0..4fb2510 100644 --- a/src/profile/__mocks__/viewOwnProfile.mockStore.js +++ b/src/profile/__mocks__/viewOwnProfile.mockStore.js @@ -124,9 +124,9 @@ module.exports = { createdDate: '2019-03-04T19:31:39.896806Z' } ], - countriesCodesList:[{code:"AX"},{code:"AL"}], drafts: {}, - isLoadingProfile: false + isLoadingProfile: false, + countriesCodesList: ['US', 'CA', 'GB', 'ME'] }, router: { location: { diff --git a/src/profile/__snapshots__/ProfilePage.test.jsx.snap b/src/profile/__snapshots__/ProfilePage.test.jsx.snap index 455ae82..367f0f8 100644 --- a/src/profile/__snapshots__/ProfilePage.test.jsx.snap +++ b/src/profile/__snapshots__/ProfilePage.test.jsx.snap @@ -5,13 +5,9 @@ exports[`- --- Renders correctly in various states app loading 1`] = ` -+Renders correctly in various states app loading 1`] = ``; -exports[`Renders correctly in various states successfully redirected to not found page. 1`] = ` +exports[` Renders correctly in various states successfully redirected to not found page 1`] = ` -`; - -exports[`-----Renders correctly in various states successfully redire-- Renders correctly in various states successfully redire />---- -- -- staff -
+ staffTest +- - - --- Your profile information is only visible to you. Only your username is visible to others on localhost. --
- ---- -- -- staff -
-- - - --- Your profile information is only visible to you. Only your username is visible to others on localhost. --
- -------- Full Name -
-- -- - This is the name that appears in your account and on your certificates. ------- Location -
-- --------- Primary Language Spoken -
-- --------- Education -
-- ----+ class="p-0 col-auto" + />--- Social Links -
--
-- -
-- --- -
-- --- -
-- ---+ class="ml-auto" + />------- About Me -
-- -------- You don't have any certificates yet. -- My Certificates - -
-- - - - Everyone on localhost - -
-Renders correctly in various states test country edit with error 1`] = ` - --`; - -exports[`----++++ Profile information +
++---- -- --
--- -- -- staff -
-- Member since - 2017 -
-
- ------ -- -- staff -
-- Member since - 2017 -
-
- ------ Full Name - -
-- - - - Just me - + Username
+- Lemon Seltzer -
- - This is the name that appears in your account and on your certificates. - + staffTest +----- --------- Primary Language Spoken - -
-- - - - Everyone on localhost - -
-- Yoruba -
-------- Education - -
-- - - - Just me - -
-- Elementary/primary school -
-------- Social Links - -
-- - - - Everyone on localhost - -
--
-- - - - Twitter - -
-- - - - Facebook - -
-- -
-- ----------- About Me - -
-- - - - Everyone on localhost - -
-- This is my bio -
-------- My Certificates - -
-- - - - Everyone on localhost - -
------ ------- Verified Certificate -
-- edX Demonstration Course -
-- From -
-- edX -
- -- Completed on - 3/4/2019 -
- -Renders correctly in various states test education edit with error 1`] = ` - --`; - -exports[`- ------------- -- --
--- -- -- staff -
-- Member since - 2017 -
-
- ------ -- -- staff -
-- Member since - 2017 -
-
- -------- Full Name - -
-- - - - Just me - -
-- Lemon Seltzer -
- - This is the name that appears in your account and on your certificates. - -------- Location - -
-- - - - Everyone on localhost - -
-- Montenegro -
-------- Primary Language Spoken - -
-- - - - Everyone on localhost - -
-- Yoruba -
------ --------- Social Links - -
-- - - - Everyone on localhost - -
--
-- - - - Twitter - -
-- - - - Facebook - -
-- -
-- ----------- About Me - -
-- - - - Everyone on localhost - -
-- This is my bio -
-------- My Certificates - -
-- - - - Everyone on localhost - -
------ ------- Verified Certificate -
-- edX Demonstration Course -
-- From -
-- edX -
- -- Completed on - 3/4/2019 -
- -Renders correctly in various states test preferreded language edit with error 1`] = ` - --`; - -exports[`- ------------- -- --
--- -- -- staff -
-- Member since - 2017 -
-
- ------ -- -- staff -
-- Member since - 2017 -
-
- -------- Full Name - -
-- - - - Just me - -
-- Lemon Seltzer -
- - This is the name that appears in your account and on your certificates. - -------- Location - -
-- - - - Everyone on localhost - -
-- Montenegro -
------ --------- Education - -
-- - - - Just me - -
-- Elementary/primary school -
-------- Social Links - -
-- - - - Everyone on localhost - -
--
-- - - - Twitter - -
-- - - - Facebook - -
-- -
-- ----------- About Me - -
-- - - - Everyone on localhost - -
-- This is my bio -
-------- My Certificates - -
-- - - - Everyone on localhost - -
------ ------- Verified Certificate -
-- edX Demonstration Course -
-- From -
-- edX -
- -- Completed on - 3/4/2019 -
- -Renders correctly in various states test user with test user with disabled country 1`] = ` - --`; - -exports[`- ------------- -- --
-- --- staff -
-- Member since - 2017 -
-
- ----- -- -- staff -
-- Member since - 2017 -
-
- -------- Full Name - -
-- - - - Just me - -
-- Lemon Seltzer -
- - This is the name that appears in your account and on your certificates. - ------ --------- Primary Language Spoken - -
-- - - - Everyone on localhost - -
-- Yoruba -
-------- Education - -
-- - - - Just me - -
-- Elementary/primary school -
-------- Social Links - -
-- - - - Everyone on localhost - -
--
-- - - - Twitter - -
-- - - - Facebook - -
-- -
-- ----------- About Me - -
-- - - - Everyone on localhost - -
-- This is my bio -
-------- My Certificates - -
-- - - - Everyone on localhost - -
------ ------- Verified Certificate -
-- edX Demonstration Course -
-- From -
-- edX -
- -- Completed on - 3/4/2019 -
- -Renders correctly in various states test user with test user with non-disabled country 1`] = ` - -`; @@ -6247,19 +168,16 @@ exports[`----------- -- --
--- -- -- staff -
-- Member since - 2017 -
-
- ------ -- -- staff -
-- Member since - 2017 -
-
- -------- Full Name - -
-- - - - Just me - -
-- Lemon Seltzer -
- - This is the name that appears in your account and on your certificates. - ------ --------- Primary Language Spoken - -
-- - - - Everyone on localhost - -
-- Yoruba -
-------- Education - -
-- - - - Just me - -
-- Elementary/primary school -
-------- Social Links - -
-- - - - Everyone on localhost - -
--
-- - - - Twitter - -
-- - - - Facebook - -
-- -
-- ----------- About Me - -
-- - - - Everyone on localhost - -
-- This is my bio -
-------- My Certificates - -
-- - - - Everyone on localhost - -
------ ------- Verified Certificate -
-- edX Demonstration Course -
-- From -
-- edX -
- -- Completed on - 3/4/2019 -
- -Renders correctly in various states viewing other profi class="profile-page" > - +Renders correctly in various states viewing other profi />++++ verified +
++ Verified User +
++ + Member since + + + 2017 + + + ++-+ class="ml-auto" + />- -- -- staff -
-- Member since - 2017 -
-- - - --- Your profile information is only visible to you. Only your username is visible to others on localhost. --
- -+- -- -- staff -
-- Member since - 2017 -
-- - - --- Your profile information is only visible to you. Only your username is visible to others on localhost. --
- -------- Full Name -
-- user -
------- -- Location -
------- -- Primary Language Spoken -
-------- Education -
-- Other education -
---+ Profile information +--- Social Links -
--
+--- About Me -
+ Username + +- bio -
+ verified + ++++++++ Full name +
+ +++ +++ ++ Verified User +
++++++ Country +
+++ +++ ++ United States of America +
++++++ Primary language spoken +
+++ +++ ++ English +
++++ Education +
+++ +++ ++ Other education +
++-+- My Certificates -
+ Bio + +++ +++ ++ About me +
+++- You don't have any certificates yet.++++++ X +
+++ +++ ++ https://twitter.com/user +
+Renders correctly in various states viewing own profile class="profile-page" > - ---Renders correctly in various states viewing own profile--+
--- -- -- staff -
-- Member since - 2017 -
-
- --+-- -- -- staff -
-- Member since - 2017 -
-
- ------ Full Name - -
-- - - - Just me - -
-+ staff +
+Lemon Seltzer
- - This is the name that appears in your account and on your certificates. - ------- Location - -
-+ Member since - - - Everyone on localhost - -
-- Montenegro -
-------- Primary Language Spoken - -
-- - - - Everyone on localhost - -
-- Yoruba -
-------- Education - -
-- - - - Just me - -
-- Elementary/primary school -
--+-+--- Social Links - -
-- - - - Everyone on localhost - -
--
+ 1 + + certifications + +- - - - Twitter - -
-- + + - - - Facebook - -
-- -
-- --++ ), + editable: ( + <> ++-`; - -exports[`++++ Profile information +
++-+--- About Me - -
- - - - Everyone on localhost - + Username
+- This is my bio -
+ staff + ++++++++ Full name +
+ ++++++ Lemon Seltzer +
++ +++++ + + + Just me + +
++++++ Country +
++++++ Montenegro +
++ +++++ + + + Everyone on localhost + +
++++++ Primary language spoken +
++++++ Yoruba +
++ +++++ + + + Everyone on localhost + +
++++ Education +
++++++ Elementary/primary school +
++ +++++ + + + Just me + +
+---- My Certificates - -
- - - - Everyone on localhost - + Bio
-++- -+-++++++ + + +++ + + + Everyone on localhost + +
+++@@ -7520,1889 +1363,98 @@ exports[`+++ +++++ + +
++ + + Everyone on localhost - - + +++ Facebook +
++++++++ https://www.facebook.com/aloha +
++ +++++ + + + Everyone on localhost + +
+++ LinkedIn +
++Renders correctly in various states viewing own profile Renders correctly in various states while saving an edited bio 1`] = ` - -- ), - editable: ( - <> ---`; - -exports[`--+---+-+ Your certificates + +- -- --
++ Your learner records information is only visible to you. Only your username and profile image are visible to others on localhost. +
-- -- -- staff -
-- Member since - 2017 -
-
- ------ -- -- staff -
-- Member since - 2017 -
-
- ------ Full Name - -
-- - - - Just me - -
-- Lemon Seltzer -
- - This is the name that appears in your account and on your certificates. - -------- Location - -
-- - - - Everyone on localhost - -
-- Montenegro -
-------- Primary Language Spoken - -
-- - - - Everyone on localhost - -
-- Yoruba -
-------- Education - -
-- - - - Just me - -
-- Elementary/primary school -
-------- Social Links - -
-- - - - Everyone on localhost - -
--
-- - - - Twitter - -
-- - - - Facebook - -
-- -
-- --------- --------- My Certificates - -
-- - - - Everyone on localhost - -
----- ------- Verified Certificate -
-- edX Demonstration Course -
-- From -
-- edX -
- -- Completed on - 3/4/2019 -
- -Renders correctly in various states while saving an edited bio with error 1`] = ` - -+ ), + editable: ( + <> +- -- ), - editable: ( - <> --- ), - editable: ( - <> ---------- -- --
--- -- -- staff -
-- Member since - 2017 -
-
- --+ ), + editable: ( + <> +--- -- -- staff -
-- Member since - 2017 -
-
- -------- Full Name - -
-- - - - Just me - -
-- Lemon Seltzer -
- - This is the name that appears in your account and on your certificates. - -------- Location - -
-- - - - Everyone on localhost - -
-- Montenegro -
-------- Primary Language Spoken - -
-- - - - Everyone on localhost - -
-- Yoruba -
-------- Education - -
-- - - - Just me - -
-- Elementary/primary school -
-------- Social Links - -
-- - - - Everyone on localhost - -
--
-- - - - Twitter - -
-- - - - Facebook - -
-- -
-- ---+----- ---+-@@ -9420,19 +1472,16 @@ exports[`--- My Certificates - -
-- - - - Everyone on localhost - -
-- -+ +-+ Verified Certificate + +--- Verified Certificate -
-- edX Demonstration Course -
-- From -
-- edX -
- -- Completed on - 3/4/2019 -
- -+ edX Demonstration Course +
++ From +
++ edX +
++ Completed on + 3/4/2019 +
+ Credential ID +
Renders correctly in various states without credentials class="profile-page" > - ---Renders correctly in various states without credentials--+
--- -- -- staff -
-- Member since - 2017 -
-
- --+-- -- -- staff -
-- Member since - 2017 -
-
- ------ Full Name - -
-- - - - Just me - -
-+ staff +
+Lemon Seltzer
- - This is the name that appears in your account and on your certificates. - ------- Location - -
-+ Member since - - - Everyone on localhost - -
-- Montenegro -
-------- Primary Language Spoken - -
-- - - - Everyone on localhost - -
-- Yoruba -
-------- Education - -
-- - - - Just me - -
-- Elementary/primary school -
--+---- Social Links - -
-- - - - Everyone on localhost - -
--
+ 1 + + certifications + +- - - - Twitter - -
-- + + - - - Facebook - -
-- -
-- --+- ), - editable: ( - <> -+`; diff --git a/src/profile/data/actions.js b/src/profile/data/actions.js index 85edb5b..b960400 100644 --- a/src/profile/data/actions.js +++ b/src/profile/data/actions.js @@ -9,8 +9,6 @@ export const CLOSE_FORM = 'CLOSE_FORM'; export const UPDATE_DRAFT = 'UPDATE_DRAFT'; export const RESET_DRAFTS = 'RESET_DRAFTS'; -// FETCH PROFILE ACTIONS - export const fetchProfile = username => ({ type: FETCH_PROFILE.BASE, payload: { username }, @@ -25,22 +23,18 @@ export const fetchProfileSuccess = ( preferences, courseCertificates, isAuthenticatedUserProfile, - countriesCodesList, ) => ({ type: FETCH_PROFILE.SUCCESS, account, preferences, courseCertificates, isAuthenticatedUserProfile, - countriesCodesList, }); export const fetchProfileReset = () => ({ type: FETCH_PROFILE.RESET, }); -// SAVE PROFILE ACTIONS - export const saveProfile = (formId, username) => ({ type: SAVE_PROFILE.BASE, payload: { @@ -70,8 +64,6 @@ export const saveProfileFailure = errors => ({ payload: { errors }, }); -// SAVE PROFILE PHOTO ACTIONS - export const saveProfilePhoto = (username, formData) => ({ type: SAVE_PROFILE_PHOTO.BASE, payload: { @@ -98,8 +90,6 @@ export const saveProfilePhotoFailure = error => ({ payload: { error }, }); -// DELETE PROFILE PHOTO ACTIONS - export const deleteProfilePhoto = username => ({ type: DELETE_PROFILE_PHOTO.BASE, payload: { @@ -120,8 +110,6 @@ export const deleteProfilePhotoReset = () => ({ type: DELETE_PROFILE_PHOTO.RESET, }); -// FIELD STATE ACTIONS - export const openForm = formId => ({ type: OPEN_FORM, payload: { @@ -136,8 +124,6 @@ export const closeForm = formId => ({ }, }); -// FORM STATE ACTIONS - export const updateDraft = (name, value) => ({ type: UPDATE_DRAFT, payload: { diff --git a/src/profile/data/actions.test.js b/src/profile/data/actions.test.js index 6268888..275d695 100644 --- a/src/profile/data/actions.test.js +++ b/src/profile/data/actions.test.js @@ -1,14 +1,4 @@ import { - openForm, - closeForm, - OPEN_FORM, - CLOSE_FORM, - SAVE_PROFILE, - saveProfileBegin, - saveProfileSuccess, - saveProfileFailure, - saveProfileReset, - saveProfile, SAVE_PROFILE_PHOTO, saveProfilePhotoBegin, saveProfilePhotoSuccess, @@ -22,76 +12,6 @@ import { deleteProfilePhoto, } from './actions'; -describe('editable field actions', () => { - it('should create an open action', () => { - const expectedAction = { - type: OPEN_FORM, - payload: { - formId: 'name', - }, - }; - expect(openForm('name')).toEqual(expectedAction); - }); - - it('should create a closed action', () => { - const expectedAction = { - type: CLOSE_FORM, - payload: { - formId: 'name', - }, - }; - expect(closeForm('name')).toEqual(expectedAction); - }); -}); - -describe('SAVE profile actions', () => { - it('should create an action to signal the start of a profile save', () => { - const expectedAction = { - type: SAVE_PROFILE.BASE, - payload: { - formId: 'name', - }, - }; - expect(saveProfile('name')).toEqual(expectedAction); - }); - - it('should create an action to signal user profile save success', () => { - const accountData = { name: 'Full Name' }; - const preferencesData = { visibility: { name: 'private' } }; - const expectedAction = { - type: SAVE_PROFILE.SUCCESS, - payload: { - account: accountData, - preferences: preferencesData, - }, - }; - expect(saveProfileSuccess(accountData, preferencesData)).toEqual(expectedAction); - }); - - it('should create an action to signal user profile save beginning', () => { - const expectedAction = { - type: SAVE_PROFILE.BEGIN, - }; - expect(saveProfileBegin()).toEqual(expectedAction); - }); - - it('should create an action to signal user profile save success', () => { - const expectedAction = { - type: SAVE_PROFILE.RESET, - }; - expect(saveProfileReset()).toEqual(expectedAction); - }); - - it('should create an action to signal user account save failure', () => { - const errors = ['Test failure']; - const expectedAction = { - type: SAVE_PROFILE.FAILURE, - payload: { errors }, - }; - expect(saveProfileFailure(errors)).toEqual(expectedAction); - }); -}); - describe('SAVE profile photo actions', () => { it('should create an action to signal the start of a profile photo save', () => { const formData = 'multipart form data'; @@ -123,7 +43,7 @@ describe('SAVE profile photo actions', () => { expect(saveProfilePhotoSuccess(newPhotoData)).toEqual(expectedAction); }); - it('should create an action to signal user profile photo save success', () => { + it('should create an action to signal user profile photo save reset', () => { const expectedAction = { type: SAVE_PROFILE_PHOTO.RESET, }; @@ -169,34 +89,10 @@ describe('DELETE profile photo actions', () => { expect(deleteProfilePhotoSuccess(defaultPhotoData)).toEqual(expectedAction); }); - it('should create an action to signal user profile photo deletion success', () => { + it('should create an action to signal user profile photo deletion reset', () => { const expectedAction = { type: DELETE_PROFILE_PHOTO.RESET, }; expect(deleteProfilePhotoReset()).toEqual(expectedAction); }); }); - -describe('Editable field opening and closing actions', () => { - const formId = 'name'; - - it('should create an action to signal the opening a field', () => { - const expectedAction = { - type: OPEN_FORM, - payload: { - formId, - }, - }; - expect(openForm(formId)).toEqual(expectedAction); - }); - - it('should create an action to signal the closing a field', () => { - const expectedAction = { - type: CLOSE_FORM, - payload: { - formId, - }, - }; - expect(closeForm(formId)).toEqual(expectedAction); - }); -}); diff --git a/src/profile-v2/data/hooks.js b/src/profile/data/hooks.js similarity index 93% rename from src/profile-v2/data/hooks.js rename to src/profile/data/hooks.js index 161a436..abf37c5 100644 --- a/src/profile-v2/data/hooks.js +++ b/src/profile/data/hooks.js @@ -12,7 +12,7 @@ export function useIsOnMobileScreen() { } export function useIsVisibilityEnabled() { - return getConfig().DISABLE_VISIBILITY_EDITING === 'true'; + return getConfig().DISABLE_VISIBILITY_EDITING !== 'true'; } export function useHandleChange(changeHandler) { diff --git a/src/profile/data/pact-profile.test.js b/src/profile/data/pact-profile.test.js index 3addaa4..abd14e6 100644 --- a/src/profile/data/pact-profile.test.js +++ b/src/profile/data/pact-profile.test.js @@ -17,6 +17,10 @@ const expectedUserInfo200 = { dateJoined: '2017-06-07T00:44:23Z', isActive: true, yearOfBirth: 1901, + languageProficiencies: [], + levelOfEducation: null, + profileImage: {}, + socialLinks: [], }; const provider = new PactV3({ diff --git a/src/profile/data/reducers.js b/src/profile/data/reducers.js index 0d374c6..3e4760c 100644 --- a/src/profile/data/reducers.js +++ b/src/profile/data/reducers.js @@ -16,12 +16,27 @@ export const initialState = { currentlyEditingField: null, account: { socialLinks: [], + languageProficiencies: [], + name: '', + bio: '', + country: '', + levelOfEducation: '', + profileImage: {}, + yearOfBirth: '', + }, + preferences: { + visibilityName: '', + visibilityBio: '', + visibilityCountry: '', + visibilityLevelOfEducation: '', + visibilitySocialLinks: '', + visibilityLanguageProficiencies: '', }, - preferences: {}, courseCertificates: [], drafts: {}, isLoadingProfile: true, isAuthenticatedUserProfile: false, + disabledCountries: ['RU'], countriesCodesList: [], }; @@ -38,12 +53,17 @@ const profilePage = (state = initialState, action = {}) => { case FETCH_PROFILE.SUCCESS: return { ...state, - account: action.account, + account: { + ...state.account, + ...action.account, + socialLinks: action.account.socialLinks || [], + languageProficiencies: action.account.languageProficiencies || [], + }, preferences: action.preferences, - courseCertificates: action.courseCertificates, + courseCertificates: action.courseCertificates || [], isLoadingProfile: false, isAuthenticatedUserProfile: action.isAuthenticatedUserProfile, - countriesCodesList: action.countriesCodesList, + countriesCodesList: action.countriesCodesList || [], }; case SAVE_PROFILE.BEGIN: return { @@ -56,9 +76,12 @@ const profilePage = (state = initialState, action = {}) => { ...state, saveState: 'complete', errors: {}, - // Account is always replaced completely. - account: action.payload.account !== null ? action.payload.account : state.account, - // Preferences changes get merged in. + account: action.payload.account !== null ? { + ...state.account, + ...action.payload.account, + socialLinks: action.payload.account.socialLinks || [], + languageProficiencies: action.payload.account.languageProficiencies || [], + } : state.account, preferences: { ...state.preferences, ...action.payload.preferences }, }; case SAVE_PROFILE.FAILURE: @@ -75,7 +98,6 @@ const profilePage = (state = initialState, action = {}) => { isLoadingProfile: false, errors: {}, }; - case SAVE_PROFILE_PHOTO.BEGIN: return { ...state, @@ -85,7 +107,6 @@ const profilePage = (state = initialState, action = {}) => { case SAVE_PROFILE_PHOTO.SUCCESS: return { ...state, - // Merge in new profile image data account: { ...state.account, profileImage: action.payload.profileImage }, savePhotoState: 'complete', errors: {}, @@ -102,7 +123,6 @@ const profilePage = (state = initialState, action = {}) => { savePhotoState: null, errors: {}, }; - case DELETE_PROFILE_PHOTO.BEGIN: return { ...state, @@ -112,7 +132,6 @@ const profilePage = (state = initialState, action = {}) => { case DELETE_PROFILE_PHOTO.SUCCESS: return { ...state, - // Merge in new profile image data (should be empty or default image) account: { ...state.account, profileImage: action.payload.profileImage }, savePhotoState: 'complete', errors: {}, @@ -129,13 +148,11 @@ const profilePage = (state = initialState, action = {}) => { savePhotoState: null, errors: {}, }; - case UPDATE_DRAFT: return { ...state, drafts: { ...state.drafts, [action.payload.name]: action.payload.value }, }; - case RESET_DRAFTS: return { ...state, @@ -148,7 +165,6 @@ const profilePage = (state = initialState, action = {}) => { drafts: {}, }; case CLOSE_FORM: - // Only close if the field to close is undefined or matches the field that is currently open if (action.payload.formId === state.currentlyEditingField) { return { ...state, diff --git a/src/profile-v2/data/reducers.test.js b/src/profile/data/reducers.test.js similarity index 100% rename from src/profile-v2/data/reducers.test.js rename to src/profile/data/reducers.test.js diff --git a/src/profile/data/sagas.js b/src/profile/data/sagas.js index e2ebba5..b64c3fe 100644 --- a/src/profile/data/sagas.js +++ b/src/profile/data/sagas.js @@ -1,3 +1,4 @@ +import { history } from '@edx/frontend-platform'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import pick from 'lodash.pick'; import { @@ -21,13 +22,12 @@ import { resetDrafts, saveProfileBegin, saveProfileFailure, - saveProfilePhotoBegin, - saveProfilePhotoFailure, - saveProfilePhotoReset, - saveProfilePhotoSuccess, saveProfileReset, saveProfileSuccess, SAVE_PROFILE, + saveProfilePhotoBegin, + saveProfilePhotoReset, + saveProfilePhotoSuccess, SAVE_PROFILE_PHOTO, } from './actions'; import { handleSaveProfileSelector, userAccountSelector } from './selectors'; @@ -37,7 +37,6 @@ export function* handleFetchProfile(action) { const { username } = action.payload; const userAccount = yield select(userAccountSelector); const isAuthenticatedUserProfile = username === getAuthenticatedUser().username; - // Default our data assuming the account is the current user's account. let preferences = {}; let account = userAccount; let courseCertificates = null; @@ -46,7 +45,6 @@ export function* handleFetchProfile(action) { try { yield put(fetchProfileBegin()); - // Depending on which profile we're loading, we need to make different calls. const calls = [ call(ProfileApiService.getAccount, username), call(ProfileApiService.getCourseCertificates, username), @@ -54,12 +52,9 @@ export function* handleFetchProfile(action) { ]; if (isAuthenticatedUserProfile) { - // If the profile is for the current user, get their preferences. - // We don't need them for other users. calls.push(call(ProfileApiService.getPreferences, username)); } - // Make all the calls in parallel. const result = yield all(calls); if (isAuthenticatedUserProfile) { @@ -68,9 +63,6 @@ export function* handleFetchProfile(action) { [account, courseCertificates, countriesCodesList] = result; } - // Set initial visibility values for account - // Set account_privacy as custom is necessary so that when viewing another user's profile, - // their full name is displayed and change visibility forms are worked correctly if (isAuthenticatedUserProfile && result[0].accountPrivacy === 'all_users') { yield call(ProfileApiService.patchPreferences, action.payload.username, { account_privacy: 'custom', @@ -97,11 +89,7 @@ export function* handleFetchProfile(action) { yield put(fetchProfileReset()); } catch (e) { if (e.response.status === 404) { - if (e.processedData && e.processedData.fieldErrors) { - yield put(saveProfileFailure(e.processedData.fieldErrors)); - } else { - yield put(saveProfileFailure(e.customAttributes)); - } + history.push('/notfound'); } else { throw e; } @@ -114,7 +102,6 @@ export function* handleSaveProfile(action) { const accountDrafts = pick(drafts, [ 'bio', - 'courseCertificates', 'country', 'levelOfEducation', 'languageProficiencies', @@ -124,7 +111,6 @@ export function* handleSaveProfile(action) { const preferencesDrafts = pick(drafts, [ 'visibilityBio', - 'visibilityCourseCertificates', 'visibilityCountry', 'visibilityLevelOfEducation', 'visibilityLanguageProficiencies', @@ -138,7 +124,6 @@ export function* handleSaveProfile(action) { yield put(saveProfileBegin()); let accountResult = null; - // Build the visibility drafts into a structure the API expects. if (Object.keys(accountDrafts).length > 0) { accountResult = yield call( @@ -148,17 +133,14 @@ export function* handleSaveProfile(action) { ); } - let preferencesResult = preferences; // assume it hasn't changed. + let preferencesResult = preferences; if (Object.keys(preferencesDrafts).length > 0) { yield call(ProfileApiService.patchPreferences, action.payload.username, preferencesDrafts); // TODO: Temporary deoptimization since the patchPreferences call doesn't return anything. - // Remove this second call once we can get a result from the one above. + preferencesResult = yield call(ProfileApiService.getPreferences, action.payload.username); } - // The account result is returned from the server. - // The preferences draft is valid if the server didn't complain, so - // pass it through directly. yield put(saveProfileSuccess(accountResult, preferencesResult)); yield delay(1000); yield put(closeForm(action.payload.formId)); @@ -184,12 +166,7 @@ export function* handleSaveProfilePhoto(action) { yield put(saveProfilePhotoSuccess(photoResult)); yield put(saveProfilePhotoReset()); } catch (e) { - if (e.processedData) { - yield put(saveProfilePhotoFailure(e.processedData)); - } else { - yield put(saveProfilePhotoReset()); - throw e; - } + yield put(saveProfilePhotoReset()); } } @@ -203,7 +180,6 @@ export function* handleDeleteProfilePhoto(action) { yield put(deleteProfilePhotoReset()); } catch (e) { yield put(deleteProfilePhotoReset()); - throw e; } } diff --git a/src/profile/data/sagas.test.js b/src/profile/data/sagas.test.js index 291ab1b..2da09b3 100644 --- a/src/profile/data/sagas.test.js +++ b/src/profile/data/sagas.test.js @@ -26,7 +26,6 @@ jest.mock('@edx/frontend-platform/auth', () => ({ getAuthenticatedUser: jest.fn(), })); -// RootSaga and ProfileApiService must be imported AFTER the mock above. /* eslint-disable import/first */ import profileSaga, { handleFetchProfile, @@ -78,7 +77,6 @@ describe('RootSaga', () => { call(ProfileApiService.getCourseCertificates, 'gonzo'), call(ProfileApiService.getCountryList), call(ProfileApiService.getPreferences, 'gonzo'), - ])); expect(gen.next(result).value) .toEqual(put(profileActions.fetchProfileSuccess(userAccount, result[3], result[1], true, []))); @@ -137,8 +135,6 @@ describe('RootSaga', () => { expect(gen.next().value).toEqual(call(ProfileApiService.patchProfile, 'my username', { name: 'Full Name', })); - // The library would supply the result of the above call - // as the parameter to the NEXT yield. Here: expect(gen.next(profile).value).toEqual(put(profileActions.saveProfileSuccess(profile, {}))); expect(gen.next().value).toEqual(delay(1000)); expect(gen.next().value).toEqual(put(profileActions.closeForm('ze form id'))); diff --git a/src/profile/data/selectors.js b/src/profile/data/selectors.js index 41d69fa..d398295 100644 --- a/src/profile/data/selectors.js +++ b/src/profile/data/selectors.js @@ -5,18 +5,15 @@ import { getCountryList, getCountryMessages, getLanguageMessages, -} from '@edx/frontend-platform/i18n'; // eslint-disable-line +} from '@edx/frontend-platform/i18n'; export const formIdSelector = (state, props) => props.formId; export const userAccountSelector = state => state.userAccount; - export const profileAccountSelector = state => state.profilePage.account; export const profileDraftsSelector = state => state.profilePage.drafts; export const accountPrivacySelector = state => state.profilePage.preferences.accountPrivacy; export const profilePreferencesSelector = state => state.profilePage.preferences; export const profileCourseCertificatesSelector = state => state.profilePage.courseCertificates; -export const profileAccountDraftsSelector = state => state.profilePage.accountDrafts; -export const profileVisibilityDraftsSelector = state => state.profilePage.visibilityDrafts; export const saveStateSelector = state => state.profilePage.saveState; export const savePhotoStateSelector = state => state.profilePage.savePhotoState; export const isLoadingProfileSelector = state => state.profilePage.isLoadingProfile; @@ -32,22 +29,11 @@ export const editableFormModeSelector = createSelector( formIdSelector, currentlyEditingFieldSelector, (account, isAuthenticatedUserProfile, certificates, formId, currentlyEditingField) => { - // If the prop doesn't exist, that means it hasn't been set (for the current user's profile) - // or is being hidden from us (for other users' profiles) let propExists = account[formId] != null && account[formId].length > 0; - propExists = formId === 'certificates' ? certificates.length > 0 : propExists; // overwrite for certificates - // If this isn't the current user's profile + propExists = formId === 'certificates' ? certificates.length > 0 : propExists; if (!isAuthenticatedUserProfile) { return 'static'; } - // the current user has no age set / under 13 ... - if (account.requiresParentalConsent) { - // then there are only two options: static or nothing. - // We use 'null' as a return value because the consumers of - // getMode render nothing at all on a mode of null. - return propExists ? 'static' : null; - } - // Otherwise, if this is the current user's profile... if (formId === currentlyEditingField) { return 'editing'; } @@ -68,12 +54,10 @@ export const accountDraftsFieldSelector = createSelector( export const visibilityDraftsFieldSelector = createSelector( formIdSelector, - profileVisibilityDraftsSelector, - (formId, visibilityDrafts) => visibilityDrafts[formId], + profileDraftsSelector, + (formId, drafts) => drafts[`visibility${formId.charAt(0).toUpperCase() + formId.slice(1)}`], ); -// Note: Error messages are delivered from the server -// localized according to a user's account settings export const formErrorSelector = createSelector( accountErrorsSelector, formIdSelector, @@ -91,11 +75,6 @@ export const editableFormSelector = createSelector( }), ); -// Because this selector has no input selectors, it will only be evaluated once. This is fine -// for now because we don't allow users to change the locale after page load. -// Once we DO allow this, we should create an actual action which dispatches the locale into redux, -// then we can modify this to get the locale from state rather than from getLocale() directly. -// Once we do that, this will work as expected and be re-evaluated when the locale changes. export const localeSelector = () => getLocale(); export const countryMessagesSelector = createSelector( localeSelector, @@ -169,9 +148,6 @@ export const profileImageSelector = createSelector( : {}), ); -/** - * This is used by a saga to pull out data to process. - */ export const handleSaveProfileSelector = createSelector( profileDraftsSelector, profilePreferencesSelector, @@ -181,7 +157,6 @@ export const handleSaveProfileSelector = createSelector( }), ); -// Reformats the social links in a platform-keyed hash. const socialLinksByPlatformSelector = createSelector( profileAccountSelector, (account) => { @@ -208,24 +183,18 @@ const draftSocialLinksByPlatformSelector = createSelector( }, ); -// Fleshes out our list of existing social links with all the other ones the user can set. export const formSocialLinksSelector = createSelector( socialLinksByPlatformSelector, draftSocialLinksByPlatformSelector, (linksByPlatform, draftLinksByPlatform) => { const knownPlatforms = ['twitter', 'facebook', 'linkedin']; const socialLinks = []; - // For each known platform knownPlatforms.forEach((platform) => { - // If the link is in our drafts. if (draftLinksByPlatform[platform] !== undefined) { - // Use the draft one. socialLinks.push(draftLinksByPlatform[platform]); } else if (linksByPlatform[platform] !== undefined) { - // Otherwise use the real one. socialLinks.push(linksByPlatform[platform]); } else { - // And if it's not in either, use a stub. socialLinks.push({ platform, socialLink: null, @@ -244,7 +213,6 @@ export const visibilitiesSelector = createSelector( case 'custom': return { visibilityBio: preferences.visibilityBio || 'all_users', - visibilityCourseCertificates: preferences.visibilityCourseCertificates || 'all_users', visibilityCountry: preferences.visibilityCountry || 'all_users', visibilityLevelOfEducation: preferences.visibilityLevelOfEducation || 'all_users', visibilityLanguageProficiencies: preferences.visibilityLanguageProficiencies || 'all_users', @@ -254,7 +222,6 @@ export const visibilitiesSelector = createSelector( case 'private': return { visibilityBio: 'private', - visibilityCourseCertificates: 'private', visibilityCountry: 'private', visibilityLevelOfEducation: 'private', visibilityLanguageProficiencies: 'private', @@ -263,13 +230,8 @@ export const visibilitiesSelector = createSelector( }; case 'all_users': default: - // All users is intended to fall through to default. - // If there is no value for accountPrivacy in perferences, that means it has not been - // explicitly set yet. The server assumes - today - that this means "all_users", - // so we emulate that here in the client. return { visibilityBio: 'all_users', - visibilityCourseCertificates: 'all_users', visibilityCountry: 'all_users', visibilityLevelOfEducation: 'all_users', visibilityLanguageProficiencies: 'all_users', @@ -280,9 +242,6 @@ export const visibilitiesSelector = createSelector( }, ); -/** - * If there's no draft present at all (undefined), use the original committed value. - */ function chooseFormValue(draft, committed) { return draft !== undefined ? draft : committed; } @@ -297,10 +256,6 @@ export const formValuesSelector = createSelector( bio: chooseFormValue(drafts.bio, account.bio), visibilityBio: chooseFormValue(drafts.visibilityBio, visibilities.visibilityBio), courseCertificates, - visibilityCourseCertificates: chooseFormValue( - drafts.visibilityCourseCertificates, - visibilities.visibilityCourseCertificates, - ), country: chooseFormValue(drafts.country, account.country), visibilityCountry: chooseFormValue(drafts.visibilityCountry, visibilities.visibilityCountry), levelOfEducation: chooseFormValue(drafts.levelOfEducation, account.levelOfEducation), @@ -318,7 +273,7 @@ export const formValuesSelector = createSelector( ), name: chooseFormValue(drafts.name, account.name), visibilityName: chooseFormValue(drafts.visibilityName, visibilities.visibilityName), - socialLinks, // Social links is calculated in its own selector, since it's complicated. + socialLinks, visibilitySocialLinks: chooseFormValue( drafts.visibilitySocialLinks, visibilities.visibilitySocialLinks, @@ -335,6 +290,7 @@ export const profilePageSelector = createSelector( isLoadingProfileSelector, draftSocialLinksByPlatformSelector, accountErrorsSelector, + isAuthenticatedUserProfileSelector, ( account, formValues, @@ -344,47 +300,39 @@ export const profilePageSelector = createSelector( isLoadingProfile, draftSocialLinksByPlatform, errors, + isAuthenticatedUserProfile, ) => ({ - // Account data we need username: account.username, profileImage, requiresParentalConsent: account.requiresParentalConsent, dateJoined: account.dateJoined, yearOfBirth: account.yearOfBirth, - // Bio form data bio: formValues.bio, visibilityBio: formValues.visibilityBio, - // Certificates form data courseCertificates: formValues.courseCertificates, - visibilityCourseCertificates: formValues.visibilityCourseCertificates, - // Country form data country: formValues.country, visibilityCountry: formValues.visibilityCountry, - // Education form data levelOfEducation: formValues.levelOfEducation, visibilityLevelOfEducation: formValues.visibilityLevelOfEducation, - // Language proficiency form data languageProficiencies: formValues.languageProficiencies, visibilityLanguageProficiencies: formValues.visibilityLanguageProficiencies, - // Name form data name: formValues.name, visibilityName: formValues.visibilityName, - // Social links form data socialLinks: formValues.socialLinks, visibilitySocialLinks: formValues.visibilitySocialLinks, draftSocialLinksByPlatform, - // Other data we need saveState, savePhotoState, isLoadingProfile, photoUploadError: errors.photo || null, + isAuthenticatedUserProfile, }), ); diff --git a/src/profile/data/services.js b/src/profile/data/services.js index 17f15a4..e6104df 100644 --- a/src/profile/data/services.js +++ b/src/profile/data/services.js @@ -7,7 +7,19 @@ import { FIELD_LABELS } from './constants'; ensureConfig(['LMS_BASE_URL'], 'Profile API service'); function processAccountData(data) { - return camelCaseObject(data); + const processedData = camelCaseObject(data); + return { + ...processedData, + socialLinks: Array.isArray(processedData.socialLinks) ? processedData.socialLinks : [], + languageProficiencies: Array.isArray(processedData.languageProficiencies) + ? processedData.languageProficiencies : [], + name: processedData.name || null, + bio: processedData.bio || null, + country: processedData.country || null, + levelOfEducation: processedData.levelOfEducation || null, + profileImage: processedData.profileImage || {}, + yearOfBirth: processedData.yearOfBirth || null, + }; } function processAndThrowError(error, errorDataProcessor) { @@ -20,15 +32,12 @@ function processAndThrowError(error, errorDataProcessor) { } } -// GET ACCOUNT export async function getAccount(username) { const { data } = await getHttpClient().get(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`); - // Process response data return processAccountData(data); } -// PATCH PROFILE export async function patchProfile(username, params) { const processedParams = snakeCaseObject(params); @@ -42,12 +51,9 @@ export async function patchProfile(username, params) { processAndThrowError(error, processAccountData); }); - // Process response data return processAccountData(data); } -// POST PROFILE PHOTO - export async function postProfilePhoto(username, formData) { // eslint-disable-next-line no-unused-vars const { data } = await getHttpClient().post( @@ -71,8 +77,6 @@ export async function postProfilePhoto(username, formData) { return updatedData.profileImage; } -// DELETE PROFILE PHOTO - export async function deleteProfilePhoto(username) { // eslint-disable-next-line no-unused-vars const { data } = await getHttpClient().delete(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}/image`); @@ -86,14 +90,12 @@ export async function deleteProfilePhoto(username) { return updatedData.profileImage; } -// GET PREFERENCES export async function getPreferences(username) { const { data } = await getHttpClient().get(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`); return camelCaseObject(data); } -// PATCH PREFERENCES export async function patchPreferences(username, params) { let processedParams = snakeCaseObject(params); processedParams = convertKeyNames(processedParams, { @@ -115,8 +117,6 @@ export async function patchPreferences(username, params) { return params; // TODO: Once the server returns the updated preferences object, return that. } -// GET COURSE CERTIFICATES - function transformCertificateData(data) { const transformedData = []; data.forEach((cert) => { diff --git a/src/profile/forms/Bio.jsx b/src/profile/forms/Bio.jsx index e847c97..797bbb6 100644 --- a/src/profile/forms/Bio.jsx +++ b/src/profile/forms/Bio.jsx @@ -1,149 +1,140 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { Form } from '@openedx/paragon'; +import classNames from 'classnames'; import messages from './Bio.messages'; -// Components import FormControls from './elements/FormControls'; import EditableItemHeader from './elements/EditableItemHeader'; import EmptyContent from './elements/EmptyContent'; import SwitchContent from './elements/SwitchContent'; -// Selectors import { editableFormSelector } from '../data/selectors'; +import { + useCloseOpenHandler, + useHandleChange, + useHandleSubmit, + useIsOnMobileScreen, + useIsVisibilityEnabled, +} from '../data/hooks'; -class Bio extends React.Component { - constructor(props) { - super(props); +const Bio = ({ + formId, + bio, + visibilityBio, + editMode, + saveState, + error, + changeHandler, + submitHandler, + closeHandler, + openHandler, +}) => { + const isMobileView = useIsOnMobileScreen(); + const isVisibilityEnabled = useIsVisibilityEnabled(); + const intl = useIntl(); - this.handleChange = this.handleChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.handleClose = this.handleClose.bind(this); - this.handleOpen = this.handleOpen.bind(this); - } + const handleChange = useHandleChange(changeHandler); + const handleSubmit = useHandleSubmit(submitHandler, formId); + const handleOpen = useCloseOpenHandler(openHandler, formId); + const handleClose = useCloseOpenHandler(closeHandler, formId); - handleChange(e) { - const { name, value } = e.target; - this.props.changeHandler(name, value); - } - - handleSubmit(e) { - e.preventDefault(); - this.props.submitHandler(this.props.formId); - } - - handleClose() { - this.props.closeHandler(this.props.formId); - } - - handleOpen() { - this.props.openHandler(this.props.formId); - } - - render() { - const { - formId, bio, visibilityBio, editMode, saveState, error, intl, - } = this.props; - - return ( -++++ Profile information +
+++--- About Me - -
- - - - Everyone on localhost - + Username
+- This is my bio -
+ staff + ++++++++ Full name +
+ ++++++ Lemon Seltzer +
++ +++++ + + + Just me + +
++++++ Country +
++++++ Montenegro +
++ +++++ + + + Everyone on localhost + +
++++++ Primary language spoken +
++++++ Yoruba +
++ +++++ + + + Everyone on localhost + +
++++ Education +
++++++ Elementary/primary school +
++ +++++ + + + Just me + +
++--- My Certificates - -
- - - - Everyone on localhost - + Bio
-++- -+-++++++ + + +++ + + + Everyone on localhost + +
+++@@ -10207,6 +2279,105 @@ exports[`+++ +++++ + +
++ + + Everyone on localhost - - + +++ Facebook +
++++++++ https://www.facebook.com/aloha +
++ +++++ + + + Everyone on localhost + +
+++ LinkedIn +
++Renders correctly in various states without credentials ++++++++ Your certificates +
++++ Your learner records information is only visible to you. Only your username and profile image are visible to others on localhost. +
++++++++ ++++++ ++ Verified Certificate +
++ edX Demonstration Course +
++ From +
++ edX +
++ Completed on + 3/4/2019 +
++ Credential ID +
+- - - - {error !== null && ( - -- {error} - - )} -+ + + {intl.formatMessage(messages['profile.bio.about.me'])} +
+ - -+ {error} + + )} + + - {bio}
- > - ), - empty: ( - <> -- - - > - ), - static: ( - <> -- - {bio}
- > - ), - }} - /> - ); - } -} + ++ {intl.formatMessage(messages['profile.bio.about.me'])} +
++ > + ), + empty: ( + <> + + {intl.formatMessage(messages['profile.bio.about.me'])} +
++ + > + ), + static: ( + <> ++ + {intl.formatMessage(messages['profile.bio.about.me'])} +
++ > + ), + }} + /> + ); +}; Bio.propTypes = { - // It'd be nice to just set this as a defaultProps... - // except the class that comes out on the other side of react-redux's - // connect() method won't have it anymore. Static properties won't survive - // through the higher order function. formId: PropTypes.string.isRequired, - - // From Selector bio: PropTypes.string, visibilityBio: PropTypes.oneOf(['private', 'all_users']), editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']), saveState: PropTypes.string, error: PropTypes.string, - - // Actions changeHandler: PropTypes.func.isRequired, submitHandler: PropTypes.func.isRequired, closeHandler: PropTypes.func.isRequired, openHandler: PropTypes.func.isRequired, - - // i18n - intl: intlShape.isRequired, }; Bio.defaultProps = { @@ -157,4 +148,4 @@ Bio.defaultProps = { export default connect( editableFormSelector, {}, -)(injectIntl(Bio)); +)(Bio); diff --git a/src/profile/forms/Bio.messages.jsx b/src/profile/forms/Bio.messages.jsx index 7808710..0860acd 100644 --- a/src/profile/forms/Bio.messages.jsx +++ b/src/profile/forms/Bio.messages.jsx @@ -3,7 +3,7 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ 'profile.bio.about.me': { id: 'profile.bio.about.me', - defaultMessage: 'About Me', + defaultMessage: 'Bio', description: 'A section of a user profile', }, }); diff --git a/src/profile/forms/Certificates.jsx b/src/profile/forms/Certificates.jsx deleted file mode 100644 index d2ce86e..0000000 --- a/src/profile/forms/Certificates.jsx +++ /dev/null @@ -1,231 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { - FormattedDate, FormattedMessage, injectIntl, intlShape, -} from '@edx/frontend-platform/i18n'; -import { Hyperlink } from '@openedx/paragon'; -import { connect } from 'react-redux'; -import get from 'lodash.get'; - -import messages from './Certificates.messages'; - -// Components -import FormControls from './elements/FormControls'; -import EditableItemHeader from './elements/EditableItemHeader'; -import SwitchContent from './elements/SwitchContent'; - -// Assets -import professionalCertificateSVG from '../assets/professional-certificate.svg'; -import verifiedCertificateSVG from '../assets/verified-certificate.svg'; - -// Selectors -import { certificatesSelector } from '../data/selectors'; - -class Certificates extends React.Component { - constructor(props) { - super(props); - - this.handleChange = this.handleChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.handleClose = this.handleClose.bind(this); - this.handleOpen = this.handleOpen.bind(this); - } - - handleChange(e) { - const { name, value } = e.target; - this.props.changeHandler(name, value); - } - - handleSubmit(e) { - e.preventDefault(); - this.props.submitHandler(this.props.formId); - } - - handleClose() { - this.props.closeHandler(this.props.formId); - } - - handleOpen() { - this.props.openHandler(this.props.formId); - } - - renderCertificate({ - certificateType, courseDisplayName, courseOrganization, modifiedDate, downloadUrl, courseId, - }) { - const { intl } = this.props; - const certificateIllustration = (() => { - switch (certificateType) { - case 'professional': - case 'no-id-professional': - return professionalCertificateSVG; - case 'verified': - return verifiedCertificateSVG; - case 'honor': - case 'audit': - default: - return null; - } - })(); - - return ( - -- ); - } - - renderCertificates() { - if (this.props.certificates === null || this.props.certificates.length === 0) { - return ( -- ------- {intl.formatMessage(get( - messages, - `profile.certificates.types.${certificateType}`, - messages['profile.certificates.types.unknown'], - ))} -
-{courseDisplayName}
--
-- {courseOrganization}
- --
-, - }} - /> - --- {intl.formatMessage(messages['profile.certificates.view.certificate'])} - -- ); - } - - return ( - {this.props.certificates.map(certificate => this.renderCertificate(certificate))}- ); - } - - render() { - const { - visibilityCourseCertificates, editMode, saveState, intl, - } = this.props; - - return ( -- - - {this.renderCertificates()} - > - ), - empty: ( - <> - - {this.renderCertificates()} - > - ), - static: ( - <> - - {this.renderCertificates()} - > - ), - }} - /> - ); - } -} - -Certificates.propTypes = { - // It'd be nice to just set this as a defaultProps... - // except the class that comes out on the other side of react-redux's - // connect() method won't have it anymore. Static properties won't survive - // through the higher order function. - formId: PropTypes.string.isRequired, - - // From Selector - certificates: PropTypes.arrayOf(PropTypes.shape({ - title: PropTypes.string, - })), - visibilityCourseCertificates: PropTypes.oneOf(['private', 'all_users']), - editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']), - saveState: PropTypes.string, - - // Actions - changeHandler: PropTypes.func.isRequired, - submitHandler: PropTypes.func.isRequired, - closeHandler: PropTypes.func.isRequired, - openHandler: PropTypes.func.isRequired, - - // i18n - intl: intlShape.isRequired, -}; - -Certificates.defaultProps = { - editMode: 'static', - saveState: null, - visibilityCourseCertificates: 'private', - certificates: null, -}; - -export default connect( - certificatesSelector, - {}, -)(injectIntl(Certificates)); diff --git a/src/profile/forms/Certificates.messages.jsx b/src/profile/forms/Certificates.messages.jsx deleted file mode 100644 index 17d12b8..0000000 --- a/src/profile/forms/Certificates.messages.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - 'profile.certificates.my.certificates': { - id: 'profile.certificates.my.certificates', - defaultMessage: 'My Certificates', - description: 'A section of a user profile', - }, - 'profile.certificates.view.certificate': { - id: 'profile.certificates.view.certificate', - defaultMessage: 'View Certificate', - description: 'A call to action to view a certificate', - }, - 'profile.certificates.types.verified': { - id: 'profile.certificates.types.verified', - defaultMessage: 'Verified Certificate', - description: 'A type of certificate a user may have earned', - }, - 'profile.certificates.types.professional': { - id: 'profile.certificates.types.professional', - defaultMessage: 'Professional Certificate', - description: 'A type of certificate a user may have earned', - }, - 'profile.certificates.types.unknown': { - id: 'profile.certificates.types.unknown', - defaultMessage: 'Certificate', - description: 'The string to display when a certificate is of an unknown type', - }, -}); - -export default messages; diff --git a/src/profile/forms/Country.jsx b/src/profile/forms/Country.jsx index bfe5ad3..86cd039 100644 --- a/src/profile/forms/Country.jsx +++ b/src/profile/forms/Country.jsx @@ -1,160 +1,137 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { useIntl } from '@edx/frontend-platform/i18n'; import { Form } from '@openedx/paragon'; import messages from './Country.messages'; -// Components import FormControls from './elements/FormControls'; import EditableItemHeader from './elements/EditableItemHeader'; import EmptyContent from './elements/EmptyContent'; import SwitchContent from './elements/SwitchContent'; -// Selectors import { countrySelector } from '../data/selectors'; +import { + useCloseOpenHandler, + useHandleChange, + useHandleSubmit, + useIsVisibilityEnabled, +} from '../data/hooks'; -class Country extends React.Component { - constructor(props) { - super(props); +const Country = ({ + formId, + country, + visibilityCountry, + editMode, + saveState, + error, + translatedCountries, + countriesCodesList, + countryMessages, + changeHandler, + submitHandler, + closeHandler, + openHandler, +}) => { + const isVisibilityEnabled = useIsVisibilityEnabled(); + const intl = useIntl(); - this.handleChange = this.handleChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.handleClose = this.handleClose.bind(this); - this.handleOpen = this.handleOpen.bind(this); - this.isDisabledCountry = this.isDisabledCountry.bind(this); - } + const handleChange = useHandleChange(changeHandler); + const handleSubmit = useHandleSubmit(submitHandler, formId); + const handleOpen = useCloseOpenHandler(openHandler, formId); + const handleClose = useCloseOpenHandler(closeHandler, formId); - handleChange(e) { - const { - name, - value, - } = e.target; - this.props.changeHandler(name, value); - } + const isDisabledCountry = (countryCode) => countriesCodesList.length > 0 + && !countriesCodesList.find(code => code === countryCode); - handleSubmit(e) { - e.preventDefault(); - this.props.submitHandler(this.props.formId); - } - - handleClose() { - this.props.closeHandler(this.props.formId); - } - - handleOpen() { - this.props.openHandler(this.props.formId); - } - - isDisabledCountry = (country) => { - const { countriesCodesList } = this.props; - - return countriesCodesList.length > 0 && !countriesCodesList.find(code => code === country); - }; - - render() { - const { - formId, - country, - visibilityCountry, - editMode, - saveState, - error, - intl, - translatedCountries, - countryMessages, - } = this.props; - - return ( - - + + -+ {intl.formatMessage(messages['profile.country.label'])} +
+- - + {translatedCountries.map(({ code, name }) => ( + + ))} + + {error !== null && ( + + {error} + + )} + +- {countryMessages[country]}
- > - ), - empty: ( - <> -- - {intl.formatMessage(messages['profile.country.empty'])} - - > - ), - static: ( - <> -- {countryMessages[country]}
- > - ), - }} - /> - ); - } -} + ++ {intl.formatMessage(messages['profile.country.label'])} +
++ > + ), + empty: ( + <> + + {intl.formatMessage(messages['profile.country.label'])} +
++ {intl.formatMessage(messages['profile.country.empty'])} + + > + ), + static: ( + <> ++ {intl.formatMessage(messages['profile.country.label'])} +
++ > + ), + }} + /> + ); +}; Country.propTypes = { - // It'd be nice to just set this as a defaultProps... - // except the class that comes out on the other side of react-redux's - // connect() method won't have it anymore. Static properties won't survive - // through the higher order function. formId: PropTypes.string.isRequired, - - // From Selector country: PropTypes.string, visibilityCountry: PropTypes.oneOf(['private', 'all_users']), editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']), @@ -166,15 +143,10 @@ Country.propTypes = { })).isRequired, countriesCodesList: PropTypes.arrayOf(PropTypes.string).isRequired, countryMessages: PropTypes.objectOf(PropTypes.string).isRequired, - - // Actions changeHandler: PropTypes.func.isRequired, submitHandler: PropTypes.func.isRequired, closeHandler: PropTypes.func.isRequired, openHandler: PropTypes.func.isRequired, - - // i18n - intl: intlShape.isRequired, }; Country.defaultProps = { @@ -188,4 +160,4 @@ Country.defaultProps = { export default connect( countrySelector, {}, -)(injectIntl(Country)); +)(Country); diff --git a/src/profile/forms/Country.messages.jsx b/src/profile/forms/Country.messages.jsx index 99875cb..137bff6 100644 --- a/src/profile/forms/Country.messages.jsx +++ b/src/profile/forms/Country.messages.jsx @@ -3,12 +3,12 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ 'profile.country.label': { id: 'profile.country.label', - defaultMessage: 'Location', + defaultMessage: 'Country', description: 'The label for a country in a user profile.', }, 'profile.country.empty': { id: 'profile.country.empty', - defaultMessage: 'Add location', + defaultMessage: 'Add country', description: 'The affordance to add country location to a user’s profile.', }, }); diff --git a/src/profile/forms/Education.jsx b/src/profile/forms/Education.jsx index 8166c2c..5b877fe 100644 --- a/src/profile/forms/Education.jsx +++ b/src/profile/forms/Education.jsx @@ -1,180 +1,160 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import get from 'lodash.get'; import { Form } from '@openedx/paragon'; import messages from './Education.messages'; -// Components import FormControls from './elements/FormControls'; import EditableItemHeader from './elements/EditableItemHeader'; import EmptyContent from './elements/EmptyContent'; import SwitchContent from './elements/SwitchContent'; -// Constants import { EDUCATION_LEVELS } from '../data/constants'; -// Selectors import { editableFormSelector } from '../data/selectors'; +import { + useCloseOpenHandler, + useHandleChange, + useHandleSubmit, + useIsVisibilityEnabled, +} from '../data/hooks'; -class Education extends React.Component { - constructor(props) { - super(props); +const Education = ({ + formId, + levelOfEducation, + visibilityLevelOfEducation, + editMode, + saveState, + error, + changeHandler, + submitHandler, + closeHandler, + openHandler, +}) => { + const isVisibilityEnabled = useIsVisibilityEnabled(); + const intl = useIntl(); - this.handleChange = this.handleChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.handleClose = this.handleClose.bind(this); - this.handleOpen = this.handleOpen.bind(this); - } + const handleChange = useHandleChange(changeHandler); + const handleSubmit = useHandleSubmit(submitHandler, formId); + const handleOpen = useCloseOpenHandler(openHandler, formId); + const handleClose = useCloseOpenHandler(closeHandler, formId); - handleChange(e) { - const { - name, - value, - } = e.target; - this.props.changeHandler(name, value); - } - - handleSubmit(e) { - e.preventDefault(); - this.props.submitHandler(this.props.formId); - } - - handleClose() { - this.props.closeHandler(this.props.formId); - } - - handleOpen() { - this.props.openHandler(this.props.formId); - } - - render() { - const { - formId, levelOfEducation, visibilityLevelOfEducation, editMode, saveState, error, intl, - } = this.props; - - return ( - - + + -+ {intl.formatMessage(messages['profile.education.education'])} +
+- - - - {EDUCATION_LEVELS.map(level => ( - - ))} - - {error !== null && ( -- {error} - - )} -- - + {EDUCATION_LEVELS.map(level => ( + + ))} + + {error !== null && ( + + {error} + + )} + +- - {intl.formatMessage(get( - messages, - `profile.education.levels.${levelOfEducation}`, - messages['profile.education.levels.o'], - ))} -
- > - ), - empty: ( - <> -- - - > - ), - static: ( - <> -- - - {intl.formatMessage(get( - messages, - `profile.education.levels.${levelOfEducation}`, - messages['profile.education.levels.o'], - ))} -
- > - ), - }} - /> - ); - } -} + ++ {intl.formatMessage(messages['profile.education.education'])} +
++ > + ), + empty: ( + <> + + {intl.formatMessage(messages['profile.education.education'])} +
++ + > + ), + static: ( + <> ++ + {intl.formatMessage(messages['profile.education.education'])} +
++ > + ), + }} + /> + ); +}; Education.propTypes = { - // It'd be nice to just set this as a defaultProps... - // except the class that comes out on the other side of react-redux's - // connect() method won't have it anymore. Static properties won't survive - // through the higher order function. formId: PropTypes.string.isRequired, - - // From Selector levelOfEducation: PropTypes.string, visibilityLevelOfEducation: PropTypes.oneOf(['private', 'all_users']), editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']), saveState: PropTypes.string, error: PropTypes.string, - - // Actions changeHandler: PropTypes.func.isRequired, submitHandler: PropTypes.func.isRequired, closeHandler: PropTypes.func.isRequired, openHandler: PropTypes.func.isRequired, - - // i18n - intl: intlShape.isRequired, }; Education.defaultProps = { @@ -188,4 +168,4 @@ Education.defaultProps = { export default connect( editableFormSelector, {}, -)(injectIntl(Education)); +)(Education); diff --git a/src/profile/forms/LearningGoal.jsx b/src/profile/forms/LearningGoal.jsx deleted file mode 100644 index 236377f..0000000 --- a/src/profile/forms/LearningGoal.jsx +++ /dev/null @@ -1,92 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import get from 'lodash.get'; - -// Mock Data -import mockData from '../data/mock_data'; - -import messages from './LearningGoal.messages'; - -// Components -import EditableItemHeader from './elements/EditableItemHeader'; -import SwitchContent from './elements/SwitchContent'; - -// Selectors -import { editableFormSelector } from '../data/selectors'; - -const LearningGoal = (props) => { - let { learningGoal, editMode, visibilityLearningGoal } = props; - const { intl } = props; - - if (!learningGoal) { - learningGoal = mockData.learningGoal; - } - - if (!editMode || editMode === 'empty') { // editMode defaults to 'empty', not sure why yet - editMode = mockData.editMode; - } - - if (!visibilityLearningGoal) { - visibilityLearningGoal = mockData.visibilityLearningGoal; - } - - return ( - - - - {intl.formatMessage(get( - messages, - `profile.learningGoal.options.${learningGoal}`, - messages['profile.learningGoal.options.something_else'], - ))} -
- > - ), - static: ( - <> -- - {intl.formatMessage(get( - messages, - `profile.learningGoal.options.${learningGoal}`, - messages['profile.learningGoal.options.something_else'], - ))} -
- > - ), - }} - /> - ); -}; - -LearningGoal.propTypes = { - // From Selector - learningGoal: PropTypes.oneOf(['advance_career', 'start_career', 'learn_something_new', 'something_else']), - visibilityLearningGoal: PropTypes.oneOf(['private', 'all_users']), - editMode: PropTypes.oneOf(['editable', 'static']), - - // i18n - intl: intlShape.isRequired, -}; - -LearningGoal.defaultProps = { - editMode: 'static', - learningGoal: null, - visibilityLearningGoal: 'private', -}; - -export default connect( - editableFormSelector, - {}, -)(injectIntl(LearningGoal)); diff --git a/src/profile/forms/LearningGoal.messages.jsx b/src/profile/forms/LearningGoal.messages.jsx deleted file mode 100644 index 181fbb3..0000000 --- a/src/profile/forms/LearningGoal.messages.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - 'profile.learningGoal.learningGoal': { - id: 'profile.learningGoal.learningGoal', - defaultMessage: 'Learning Goal', - description: 'A section of a user profile that displays their current learning goal.', - }, - 'profile.learningGoal.options.start_career': { - id: 'profile.learningGoal.options.start_career', - defaultMessage: 'I want to start my career', - description: 'Selected by user if their goal is to start their career.', - }, - 'profile.learningGoal.options.advance_career': { - id: 'profile.learningGoal.options.advance_career', - defaultMessage: 'I want to advance my career', - description: 'Selected by user if their goal is to advance their career.', - }, - 'profile.learningGoal.options.learn_something_new': { - id: 'profile.learningGoal.options.learn_something_new', - defaultMessage: 'I want to learn something new', - description: 'Selected by user if their goal is to learn something new.', - }, - 'profile.learningGoal.options.something_else': { - id: 'profile.learningGoal.options.something_else', - defaultMessage: 'Something else', - description: 'Selected by user if their goal is not described by the other choices.', - }, -}); - -export default messages; diff --git a/src/profile/forms/LearningGoal.test.jsx b/src/profile/forms/LearningGoal.test.jsx deleted file mode 100644 index dcb63b1..0000000 --- a/src/profile/forms/LearningGoal.test.jsx +++ /dev/null @@ -1,116 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useMemo } from 'react'; -import { Provider } from 'react-redux'; -import { render, screen } from '@testing-library/react'; -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import { configure as configureI18n, IntlProvider } from '@edx/frontend-platform/i18n'; -import { getConfig } from '@edx/frontend-platform'; -import { AppContext } from '@edx/frontend-platform/react'; -import messages from '../../i18n'; - -import viewOwnProfileMockStore from '../__mocks__/viewOwnProfile.mockStore'; -import savingEditedBioMockStore from '../__mocks__/savingEditedBio.mockStore'; - -import LearningGoal from './LearningGoal'; - -const mockStore = configureMockStore([thunk]); - -// props to be passed down to LearningGoal component -const requiredLearningGoalProps = { - formId: 'learningGoal', - learningGoal: 'advance_career', - drafts: {}, - visibilityLearningGoal: 'private', - editMode: 'static', - saveState: null, - error: null, - openHandler: jest.fn(), -}; - -configureI18n({ - loggingService: { logError: jest.fn() }, - config: { - ENVIRONMENT: 'production', - LANGUAGE_PREFERENCE_COOKIE_NAME: 'yum', - }, - messages, -}); - -const LearningGoalWrapper = (props) => { - const contextValue = useMemo(() => ({ - authenticatedUser: { userId: null, username: null, administrator: false }, - config: getConfig(), - }), []); - return ( -- - ); -}; - -LearningGoalWrapper.defaultProps = { - store: mockStore(viewOwnProfileMockStore), -}; - -LearningGoalWrapper.propTypes = { - store: PropTypes.shape({}), -}; - -const LearningGoalWrapperWithStore = ({ store }) => { - const contextValue = useMemo(() => ({ - authenticatedUser: { userId: null, username: null, administrator: false }, - config: getConfig(), - }), []); - return ( -- -- -- - - ); -}; - -LearningGoalWrapperWithStore.defaultProps = { - store: mockStore(savingEditedBioMockStore), -}; - -LearningGoalWrapperWithStore.propTypes = { - store: PropTypes.shape({}), -}; - -describe('- -- -- ', () => { - describe('renders the current learning goal', () => { - it('renders "I want to advance my career"', () => { - render( - , - ); - expect(screen.getByText('I want to advance my career')).toBeTruthy(); - }); - - it('renders "Something else"', () => { - requiredLearningGoalProps.learningGoal = 'something_else'; - - render( - , - ); - expect(screen.getByText('Something else')).toBeTruthy(); - }); - }); -}); diff --git a/src/profile/forms/Name.jsx b/src/profile/forms/Name.jsx index 889a9ea..f3e2993 100644 --- a/src/profile/forms/Name.jsx +++ b/src/profile/forms/Name.jsx @@ -1,147 +1,182 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { InfoOutline } from '@openedx/paragon/icons'; +import { Hyperlink, OverlayTrigger, Tooltip } from '@openedx/paragon'; import messages from './Name.messages'; -// Components import FormControls from './elements/FormControls'; import EditableItemHeader from './elements/EditableItemHeader'; import EmptyContent from './elements/EmptyContent'; import SwitchContent from './elements/SwitchContent'; -// Selectors import { editableFormSelector } from '../data/selectors'; +import { + useCloseOpenHandler, + useHandleChange, + useHandleSubmit, + useIsVisibilityEnabled, +} from '../data/hooks'; -class Name extends React.Component { - constructor(props) { - super(props); +const Name = ({ + formId, + name, + visibilityName, + editMode, + saveState, + changeHandler, + submitHandler, + closeHandler, + openHandler, + accountSettingsUrl, +}) => { + const isVisibilityEnabled = useIsVisibilityEnabled(); + const intl = useIntl(); - this.handleChange = this.handleChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.handleClose = this.handleClose.bind(this); - this.handleOpen = this.handleOpen.bind(this); - } + const handleChange = useHandleChange(changeHandler); + const handleSubmit = useHandleSubmit(submitHandler, formId); + const handleOpen = useCloseOpenHandler(openHandler, formId); + const handleClose = useCloseOpenHandler(closeHandler, formId); - handleChange(e) { - const { - name, - value, - } = e.target; - this.props.changeHandler(name, value); - } - - handleSubmit(e) { - e.preventDefault(); - this.props.submitHandler(this.props.formId); - } - - handleClose() { - this.props.closeHandler(this.props.formId); - } - - handleOpen() { - this.props.openHandler(this.props.formId); - } - - render() { - const { - formId, name, visibilityName, editMode, saveState, intl, - } = this.props; - - return ( - - + +++ {intl.formatMessage(messages['profile.name.full.name'])} +
++ ++ {intl.formatMessage(messages['profile.name.tooltip'])} +
+ + )} + > ++ + > + ), + empty: ( + <> + +++ {intl.formatMessage(messages['profile.name.full.name'])} +
++ ++ {intl.formatMessage(messages['profile.name.tooltip'])} +
+ + )} + > ++ + {intl.formatMessage(messages['profile.name.empty'])} + + > + ), + static: ( + <> ++++ {intl.formatMessage(messages['profile.name.full.name'])} +
++ ++ {intl.formatMessage(messages['profile.name.tooltip'])} +
+ + )} + > ++ + > + ), + }} + /> + ); +}; Name.propTypes = { - // It'd be nice to just set this as a defaultProps... - // except the class that comes out on the other side of react-redux's - // connect() method won't have it anymore. Static properties won't survive - // through the higher order function. formId: PropTypes.string.isRequired, - - // From Selector name: PropTypes.string, visibilityName: PropTypes.oneOf(['private', 'all_users']), editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']), saveState: PropTypes.string, - - // Actions changeHandler: PropTypes.func.isRequired, submitHandler: PropTypes.func.isRequired, closeHandler: PropTypes.func.isRequired, openHandler: PropTypes.func.isRequired, - - // i18n - intl: intlShape.isRequired, + accountSettingsUrl: PropTypes.string.isRequired, }; Name.defaultProps = { @@ -154,4 +189,4 @@ Name.defaultProps = { export default connect( editableFormSelector, {}, -)(injectIntl(Name)); +)(Name); diff --git a/src/profile/forms/Name.messages.jsx b/src/profile/forms/Name.messages.jsx index d6a42fc..e9e9317 100644 --- a/src/profile/forms/Name.messages.jsx +++ b/src/profile/forms/Name.messages.jsx @@ -3,19 +3,24 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ 'profile.name.full.name': { id: 'profile.name.full.name', - defaultMessage: 'Full Name', + defaultMessage: 'Full name', description: 'A section of a user profile', }, - 'profile.name.details': { - id: 'profile.name.details', - defaultMessage: 'This is the name that appears in your account and on your certificates.', - description: 'Describes the area for a user to update their name.', - }, 'profile.name.empty': { id: 'profile.name.empty', - defaultMessage: 'Add name', + defaultMessage: 'Add full name', description: 'The affordance to add a name to a user’s profile.', }, + 'profile.name.tooltip': { + id: 'profile.name.tooltip', + defaultMessage: 'The name that is used for ID verification and that appears on your certificates', + description: 'Tooltip for the full name field.', + }, + 'profile.name.redirect': { + id: 'profile.name.redirect', + defaultMessage: 'Edit full name from the Accounts page', + description: 'Redirect message for editing the name from the Accounts page.', + }, }); export default messages; diff --git a/src/profile/forms/PreferredLanguage.jsx b/src/profile/forms/PreferredLanguage.jsx index 1ce3081..54e0e17 100644 --- a/src/profile/forms/PreferredLanguage.jsx +++ b/src/profile/forms/PreferredLanguage.jsx @@ -1,166 +1,140 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { useIntl } from '@edx/frontend-platform/i18n'; import { Form } from '@openedx/paragon'; import messages from './PreferredLanguage.messages'; -// Components import FormControls from './elements/FormControls'; import EditableItemHeader from './elements/EditableItemHeader'; import EmptyContent from './elements/EmptyContent'; import SwitchContent from './elements/SwitchContent'; -// Selectors import { preferredLanguageSelector } from '../data/selectors'; +import { + useCloseOpenHandler, + useHandleSubmit, + useIsVisibilityEnabled, +} from '../data/hooks'; -class PreferredLanguage extends React.Component { - constructor(props) { - super(props); +const PreferredLanguage = ({ + formId, + languageProficiencies, + visibilityLanguageProficiencies, + editMode, + saveState, + error, + sortedLanguages, + languageMessages, + changeHandler, + submitHandler, + closeHandler, + openHandler, +}) => { + const isVisibilityEnabled = useIsVisibilityEnabled(); + const intl = useIntl(); - this.handleChange = this.handleChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.handleClose = this.handleClose.bind(this); - this.handleOpen = this.handleOpen.bind(this); - } - - handleChange(e) { - const { name, value } = e.target; - // Restructure the data. - // We deconstruct our value prop in render() so this - // changes our data's shape back to match what came in - if (name === this.props.formId) { - if (value !== '') { - this.props.changeHandler(name, [{ code: value }]); - } else { - this.props.changeHandler(name, []); - } - } else { - this.props.changeHandler(name, value); + const handleChange = ({ target: { name, value } }) => { + let newValue = value; + if (name === formId) { + newValue = value ? [{ code: value }] : []; } - } + changeHandler(name, newValue); + }; - handleSubmit(e) { - e.preventDefault(); - this.props.submitHandler(this.props.formId); - } + const handleSubmit = useHandleSubmit(submitHandler, formId); + const handleOpen = useCloseOpenHandler(openHandler, formId); + const handleClose = useCloseOpenHandler(closeHandler, formId); - handleClose() { - this.props.closeHandler(this.props.formId); - } + const value = languageProficiencies.length ? languageProficiencies[0].code : ''; - handleOpen() { - this.props.openHandler(this.props.formId); - } - - render() { - const { - formId, - languageProficiencies, - visibilityLanguageProficiencies, - editMode, - saveState, - error, - intl, - sortedLanguages, - languageMessages, - } = this.props; - - const value = languageProficiencies.length ? languageProficiencies[0].code : ''; - - return ( - - - + + + -+ {intl.formatMessage(messages['profile.preferredlanguage.label'])} +
+- - - - {sortedLanguages.map(({ code, name }) => ( - - ))} - - {error !== null && ( -- {error} - - )} -- - + {sortedLanguages.map(({ code, name }) => ( + + ))} + + {error !== null && ( + + {error} + + )} + +- {languageMessages[value]}
- > - ), - empty: ( - <> -- - {intl.formatMessage(messages['profile.preferredlanguage.empty'])} - - > - ), - static: ( - <> -- {languageMessages[value]}
- > - ), - }} - /> - ); - } -} + ++ {intl.formatMessage(messages['profile.preferredlanguage.label'])} +
++ > + ), + empty: ( + <> + + {intl.formatMessage(messages['profile.preferredlanguage.label'])} +
++ {intl.formatMessage(messages['profile.preferredlanguage.empty'])} + + > + ), + static: ( + <> ++ {intl.formatMessage(messages['profile.preferredlanguage.label'])} +
++ > + ), + }} + /> + ); +}; PreferredLanguage.propTypes = { - // It'd be nice to just set this as a defaultProps... - // except the class that comes out on the other side of react-redux's - // connect() method won't have it anymore. Static properties won't survive - // through the higher order function. formId: PropTypes.string.isRequired, - - // From Selector languageProficiencies: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.shape({ code: PropTypes.string })), - // TODO: ProfilePageSelector should supply null values - // instead of empty strings when no value exists PropTypes.oneOf(['']), ]), visibilityLanguageProficiencies: PropTypes.oneOf(['private', 'all_users']), @@ -172,15 +146,10 @@ PreferredLanguage.propTypes = { name: PropTypes.string.isRequired, })).isRequired, languageMessages: PropTypes.objectOf(PropTypes.string).isRequired, - - // Actions changeHandler: PropTypes.func.isRequired, submitHandler: PropTypes.func.isRequired, closeHandler: PropTypes.func.isRequired, openHandler: PropTypes.func.isRequired, - - // i18n - intl: intlShape.isRequired, }; PreferredLanguage.defaultProps = { @@ -194,4 +163,4 @@ PreferredLanguage.defaultProps = { export default connect( preferredLanguageSelector, {}, -)(injectIntl(PreferredLanguage)); +)(PreferredLanguage); diff --git a/src/profile/forms/PreferredLanguage.messages.jsx b/src/profile/forms/PreferredLanguage.messages.jsx index c91a009..f24ed1b 100644 --- a/src/profile/forms/PreferredLanguage.messages.jsx +++ b/src/profile/forms/PreferredLanguage.messages.jsx @@ -8,7 +8,7 @@ const messages = defineMessages({ }, 'profile.preferredlanguage.label': { id: 'profile.preferredlanguage.label', - defaultMessage: 'Primary Language Spoken', + defaultMessage: 'Primary language spoken', description: 'The label for a user’s primary spoken language.', }, }); diff --git a/src/profile/forms/ProfileAvatar.jsx b/src/profile/forms/ProfileAvatar.jsx index 19ce9c7..0b0d968 100644 --- a/src/profile/forms/ProfileAvatar.jsx +++ b/src/profile/forms/ProfileAvatar.jsx @@ -1,158 +1,155 @@ -import React from 'react'; +import React, { useRef } from 'react'; import PropTypes from 'prop-types'; -import { Button, Dropdown } from '@openedx/paragon'; -import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { + Dropdown, + IconButton, + Icon, + Tooltip, + OverlayTrigger, +} from '@openedx/paragon'; +import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; +import { PhotoCamera } from '@openedx/paragon/icons'; import { ReactComponent as DefaultAvatar } from '../assets/avatar.svg'; - import messages from './ProfileAvatar.messages'; -class ProfileAvatar extends React.Component { - constructor(props) { - super(props); +const ProfileAvatar = ({ + src, + isDefault, + onSave, + onDelete, + savePhotoState, + isEditable, +}) => { + const intl = useIntl(); + const fileInput = useRef(null); + const form = useRef(null); - this.fileInput = React.createRef(); - this.form = React.createRef(); + const onClickUpload = () => { + fileInput.current.click(); + }; - this.onClickUpload = this.onClickUpload.bind(this); - this.onClickDelete = this.onClickDelete.bind(this); - this.onChangeInput = this.onChangeInput.bind(this); - this.onSubmit = this.onSubmit.bind(this); - } + const onClickDelete = () => { + onDelete(); + }; - onClickUpload() { - this.fileInput.current.click(); - } - - onClickDelete() { - this.props.onDelete(); - } - - onChangeInput() { - this.onSubmit(); - } - - onSubmit(e) { + const onSubmit = (e) => { if (e) { e.preventDefault(); } - this.props.onSave(new FormData(this.form.current)); - this.form.current.reset(); - } + onSave(new FormData(form.current)); + form.current.reset(); + }; - renderPending() { - return ( - - -- ); - } + const onChangeInput = () => { + onSubmit(); + }; - renderMenuContent() { - const { intl } = this.props; + const renderPending = () => ( ++ ++ ); - if (this.props.isDefault) { - return ( - - ); - } - - return ( -- - ); - } - - renderMenu() { - if (!this.props.isEditable) { + const renderEditButton = () => { + if (!isEditable) { return null; } return ( -- {intl.formatMessage(messages['profile.profileavatar.change-button'])} - -- -- -- - -- - {this.renderMenuContent()} ++); - } + }; - renderAvatar() { - const { intl } = this.props; - - return this.props.isDefault ? ( + const renderAvatar = () => ( + isDefault ? (+ + {!isDefault ? ( + ++ {intl.formatMessage(messages['profile.profileavatar.tooltip.edit'])} +
+ ) : ( ++ {intl.formatMessage(messages['profile.profileavatar.tooltip.upload'])} +
+ )} + + )} + > ++ + ++ + {!isDefault && ( ++ + + )} ++ ) : ( - ); - } + ) + ); - render() { - return ( -
-- {this.props.savePhotoState === 'pending' ? this.renderPending() : this.renderMenu() } - {this.renderAvatar()} --- {/* The name of this input must be 'file' */} - - + return ( +++ ); +}; ProfileAvatar.propTypes = { src: PropTypes.string, @@ -161,7 +158,6 @@ ProfileAvatar.propTypes = { onDelete: PropTypes.func.isRequired, savePhotoState: PropTypes.oneOf([null, 'pending', 'complete', 'error']), isEditable: PropTypes.bool, - intl: intlShape.isRequired, }; ProfileAvatar.defaultProps = { @@ -170,3 +166,5 @@ ProfileAvatar.defaultProps = { savePhotoState: null, isEditable: false, }; + +export default ProfileAvatar; diff --git a/src/profile/forms/ProfileAvatar.messages.jsx b/src/profile/forms/ProfileAvatar.messages.jsx index 121da2b..5076ad1 100644 --- a/src/profile/forms/ProfileAvatar.messages.jsx +++ b/src/profile/forms/ProfileAvatar.messages.jsx @@ -11,6 +11,16 @@ const messages = defineMessages({ defaultMessage: 'Change', description: 'Change photo button', }, + 'profile.profileavatar.tooltip.edit': { + id: 'profile.profileavatar.tooltip.edit', + defaultMessage: 'Edit photo', + description: 'Tooltip for edit photo button', + }, + 'profile.profileavatar.tooltip.upload': { + id: 'profile.profileavatar.tooltip.upload', + defaultMessage: 'Upload photo', + description: 'Tooltip for upload photo button', + }, }); export default messages; diff --git a/src/profile/forms/SocialLinks.jsx b/src/profile/forms/SocialLinks.jsx index f7d178e..60677fc 100644 --- a/src/profile/forms/SocialLinks.jsx +++ b/src/profile/forms/SocialLinks.jsx @@ -1,22 +1,18 @@ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Alert } from '@openedx/paragon'; import { connect } from 'react-redux'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faTwitter, faFacebook, faLinkedin } from '@fortawesome/free-brands-svg-icons'; -import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; import classNames from 'classnames'; -import messages from './SocialLinks.messages'; - -// Components import FormControls from './elements/FormControls'; import EditableItemHeader from './elements/EditableItemHeader'; import EmptyContent from './elements/EmptyContent'; import SwitchContent from './elements/SwitchContent'; -// Selectors import { editableFormSelector } from '../data/selectors'; +import { useIsVisibilityEnabled } from '../data/hooks'; const platformDisplayInfo = { facebook: { @@ -25,7 +21,7 @@ const platformDisplayInfo = { }, twitter: { icon: faTwitter, - name: 'Twitter', + name: 'X', }, linkedin: { icon: faLinkedin, @@ -33,283 +29,203 @@ const platformDisplayInfo = { }, }; -const SocialLink = ({ url, name, platform }) => ( - -+ {savePhotoState === 'pending' && renderPending()} + {renderAvatar()}- ); - } -} - -export default injectIntl(ProfileAvatar); + {renderEditButton()} ++ + +- {name} - -); - -SocialLink.propTypes = { - url: PropTypes.string.isRequired, - platform: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, -}; - -const EditableListItem = ({ - url, platform, onClickEmptyContent, name, +const SocialLinks = ({ + formId, + socialLinks, + draftSocialLinksByPlatform, + visibilitySocialLinks, + editMode, + saveState, + error, + changeHandler, + submitHandler, + closeHandler, + openHandler, }) => { - const linkDisplay = url ? ( - - ) : ( - Add {name} - ); + const isVisibilityEnabled = useIsVisibilityEnabled(); + const [activePlatform, setActivePlatform] = useState(null); - return- {linkDisplay}
; -}; - -EditableListItem.propTypes = { - url: PropTypes.string, - platform: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - onClickEmptyContent: PropTypes.func, -}; -EditableListItem.defaultProps = { - url: null, - onClickEmptyContent: null, -}; - -const EditingListItem = ({ - platform, name, value, onChange, error, -}) => ( -- - - -
-); - -EditingListItem.propTypes = { - platform: PropTypes.string.isRequired, - value: PropTypes.string, - name: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - error: PropTypes.string, -}; - -EditingListItem.defaultProps = { - value: null, - error: null, -}; - -const EmptyListItem = ({ onClick, name }) => ( -- -
-); - -EmptyListItem.propTypes = { - name: PropTypes.string.isRequired, - onClick: PropTypes.func.isRequired, -}; - -const StaticListItem = ({ name, url, platform }) => ( -- -- - -
-); - -StaticListItem.propTypes = { - name: PropTypes.string.isRequired, - url: PropTypes.string, - platform: PropTypes.string.isRequired, -}; - -StaticListItem.defaultProps = { - url: null, -}; - -class SocialLinks extends React.Component { - constructor(props) { - super(props); - - this.handleChange = this.handleChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.handleClose = this.handleClose.bind(this); - this.handleOpen = this.handleOpen.bind(this); - } - - handleChange(e) { - const { name, value } = e.target; - - // The social links are a bit special. If we're updating them, we need to merge them - // with any existing social link drafts, essentially sending a fresh copy of the whole - // data structure back to the reducer. This helps the reducer stay simple and keeps - // special cases out of it, concentrating them here, where they began. - if (name !== 'visibilitySocialLinks') { - this.props.changeHandler( - 'socialLinks', - this.mergeWithDrafts({ - platform: name, - // If it's an empty string, send it as null. - // The empty string is just for the input. We want nulls. - socialLink: value, - }), - ); - } else { - this.props.changeHandler(name, value); - } - } - - handleSubmit(e) { - e.preventDefault(); - this.props.submitHandler(this.props.formId); - } - - handleClose() { - this.props.closeHandler(this.props.formId); - } - - handleOpen() { - this.props.openHandler(this.props.formId); - } - - mergeWithDrafts(newSocialLink) { + const mergeWithDrafts = (newSocialLink) => { const knownPlatforms = ['twitter', 'facebook', 'linkedin']; const updated = []; knownPlatforms.forEach((platform) => { if (newSocialLink.platform === platform) { updated.push(newSocialLink); - } else if (this.props.draftSocialLinksByPlatform[platform] !== undefined) { - updated.push(this.props.draftSocialLinksByPlatform[platform]); + } else if (draftSocialLinksByPlatform[platform] !== undefined) { + updated.push(draftSocialLinksByPlatform[platform]); } }); return updated; - } + }; - render() { - const { - socialLinks, visibilitySocialLinks, editMode, saveState, error, intl, - } = this.props; + const handleChange = (e) => { + const { name, value } = e.target; + if (name !== 'visibilitySocialLinks') { + changeHandler( + 'socialLinks', + mergeWithDrafts({ + platform: name, + socialLink: value, + }), + ); + } else { + changeHandler(name, value); + } + }; - return ( -- - - - {socialLinks.map(({ platform }) => ( -
- > - ), - static: ( - <> -- ))} - { + e.preventDefault(); + submitHandler(formId); + setActivePlatform(null); + }; + + const handleClose = () => { + closeHandler(formId); + setActivePlatform(null); + }; + + const handleOpen = (platform) => { + openHandler(formId); + setActivePlatform(platform); + }; + + const renderPlatformContent = (platform, socialLink, isEditing) => { + if (isEditing) { + return ( + + + {error !== null && ( + + )} ++ ), + static: ( ++ -+ + ); + } + if (socialLink) { + return ( +- {socialLinks - .filter(({ socialLink }) => Boolean(socialLink)) - .map(({ platform, socialLink }) => ( -
- > - ), - editable: ( - <> -- ))} - - - {socialLinks.map(({ platform, socialLink }) => ( -
- > - ), - editing: ( -- ))} - -- ), - }} - /> +- - {/* TODO: Replace this alert with per-field errors. Needs API update. */} - - - {socialLinks.map(({ platform, socialLink }) => ( -
-- ))} - - + ++ ); + } + return ( +handleOpen(platform)} + showVisibility={visibilitySocialLinks !== null && isVisibilityEnabled} + visibility={visibilitySocialLinks} + /> + handleOpen(platform)}> + Add {platformDisplayInfo[platform].name} + ); - } -} + }; + + return ( ++ + {socialLinks.map(({ platform }) => ( ++++ ))} ++ {platformDisplayInfo[platform].name} +
+handleOpen(platform)}> + ++ ++ ), + editable: ( ++ {socialLinks + .filter(({ socialLink }) => Boolean(socialLink)) + .map(({ platform, socialLink }) => ( ++++ ))} ++ {platformDisplayInfo[platform].name} +
++ ++ ), + editing: ( ++ {socialLinks.map(({ platform, socialLink }) => ( ++++ ))} ++ {platformDisplayInfo[platform].name} +
+ {renderPlatformContent(platform, socialLink, activePlatform === platform)} +++ ), + }} + /> + ); +}; SocialLinks.propTypes = { - // It'd be nice to just set this as a defaultProps... - // except the class that comes out on the other side of react-redux's - // connect() method won't have it anymore. Static properties won't survive - // through the higher order function. formId: PropTypes.string.isRequired, - - // From Selector socialLinks: PropTypes.arrayOf(PropTypes.shape({ platform: PropTypes.string, socialLink: PropTypes.string, @@ -322,15 +238,10 @@ SocialLinks.propTypes = { editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']), saveState: PropTypes.string, error: PropTypes.string, - - // Actions changeHandler: PropTypes.func.isRequired, submitHandler: PropTypes.func.isRequired, closeHandler: PropTypes.func.isRequired, openHandler: PropTypes.func.isRequired, - - // i18n - intl: intlShape.isRequired, }; SocialLinks.defaultProps = { @@ -344,4 +255,4 @@ SocialLinks.defaultProps = { export default connect( editableFormSelector, {}, -)(injectIntl(SocialLinks)); +)(SocialLinks); diff --git a/src/profile/forms/SocialLinks.test.jsx b/src/profile/forms/SocialLinks.test.jsx deleted file mode 100644 index 0392d06..0000000 --- a/src/profile/forms/SocialLinks.test.jsx +++ /dev/null @@ -1,165 +0,0 @@ -import { render, fireEvent, screen } from '@testing-library/react'; -import PropTypes from 'prop-types'; -import React, { useMemo } from 'react'; -import { Provider } from 'react-redux'; -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; -import { configure as configureI18n, IntlProvider } from '@edx/frontend-platform/i18n'; -import { getConfig } from '@edx/frontend-platform'; -import { AppContext } from '@edx/frontend-platform/react'; - -import SocialLinks from './SocialLinks'; -import * as savingEditedBio from '../__mocks__/savingEditedBio.mockStore'; -import messages from '../../i18n'; - -const mockStore = configureMockStore([thunk]); - -const defaultProps = { - formId: 'socialLinks', - socialLinks: [ - { - platform: 'facebook', - socialLink: 'https://www.facebook.com/aloha', - }, - { - platform: 'twitter', - socialLink: 'https://www.twitter.com/ALOHA', - }, - ], - drafts: {}, - visibilitySocialLinks: 'private', - editMode: 'static', - saveState: null, - error: null, - changeHandler: jest.fn(), - submitHandler: jest.fn(), - closeHandler: jest.fn(), - openHandler: jest.fn(), -}; - -configureI18n({ - loggingService: { logError: jest.fn() }, - config: { - ENVIRONMENT: 'production', - LANGUAGE_PREFERENCE_COOKIE_NAME: 'yum', - }, - messages, -}); - -const SocialLinksWrapper = (props) => { - const contextValue = useMemo(() => ({ - authenticatedUser: { userId: null, username: null, administrator: false }, - config: getConfig(), - }), []); - return ( -+ {socialLinks.map(({ platform, socialLink }) => ( ++++ ))} ++ {platformDisplayInfo[platform].name} +
+ {renderPlatformContent(platform, socialLink, activePlatform === platform)} +- - ); -}; - -SocialLinksWrapper.defaultProps = { - store: mockStore(savingEditedBio), -}; - -SocialLinksWrapper.propTypes = { - store: PropTypes.shape({}), -}; - -const SocialLinksWrapperWithStore = ({ store }) => { - const contextValue = useMemo(() => ({ - authenticatedUser: { userId: null, username: null, administrator: false }, - config: getConfig(), - }), []); - return ( -- -- -- - - ); -}; - -SocialLinksWrapperWithStore.defaultProps = { - store: mockStore(savingEditedBio), -}; - -SocialLinksWrapperWithStore.propTypes = { - store: PropTypes.shape({}), -}; - -describe('- -- -- ', () => { - ['certificates', 'bio', 'goals', 'socialLinks'].forEach(editMode => ( - it(`calls social links with edit mode ${editMode}`, () => { - const component = ; - const { container: tree } = render(component); - expect(tree).toMatchSnapshot(); - }) - )); - - it('calls social links with editing', () => { - const changeHandler = jest.fn(); - const submitHandler = jest.fn(); - const closeHandler = jest.fn(); - const { container } = render( - , - ); - - const { platform } = defaultProps.socialLinks[0]; - const inputField = container.querySelector(`#social-${platform}`); - fireEvent.change(inputField, { target: { value: 'test', name: platform } }); - expect(changeHandler).toHaveBeenCalledTimes(1); - - const selectElement = container.querySelector('#visibilitySocialLinks'); - expect(selectElement.value).toBe('private'); - fireEvent.change(selectElement, { target: { value: 'all_users', name: 'visibilitySocialLinks' } }); - expect(changeHandler).toHaveBeenCalledTimes(2); - - fireEvent.submit(container.querySelector('[aria-labelledby="editing-form"]')); - expect(submitHandler).toHaveBeenCalledTimes(1); - - fireEvent.click(screen.getByRole('button', { name: 'Cancel' })); - expect(closeHandler).toHaveBeenCalledTimes(1); - }); - - it('calls social links with static', () => { - const openHandler = jest.fn(); - render( - , - ); - const addFacebookButton = screen.getByRole('button', { name: 'Add Facebook' }); - fireEvent.click(addFacebookButton); - - expect(openHandler).toHaveBeenCalledTimes(1); - }); - - it('calls social links with error', () => { - const newStore = JSON.parse(JSON.stringify(savingEditedBio)); - newStore.profilePage.errors.bio = { userMessage: 'error' }; - - const { container } = render( ); - - const alertDanger = container.querySelector('.alert-danger'); - expect(alertDanger).toBeInTheDocument(); - }); -}); diff --git a/src/profile/forms/__snapshots__/SocialLinks.test.jsx.snap b/src/profile/forms/__snapshots__/SocialLinks.test.jsx.snap deleted file mode 100644 index 22ba79c..0000000 --- a/src/profile/forms/__snapshots__/SocialLinks.test.jsx.snap +++ /dev/null @@ -1,504 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` calls social links with edit mode bio 1`] = ` - --`; - -exports[`------- --- -- Social Links -
--
-- - - -
-- - - -
---- - - - - --- - - - -- - --calls social links with edit mode certificates 1`] = ` - --`; - -exports[`------- Social Links - -
-- - - - Just me - -
--
-- - - - Facebook - -
-- - - - Twitter - -
-calls social links with edit mode goals 1`] = ` - --`; - -exports[`------- Social Links -
--
-- -
-- --- -
-- --calls social links with edit mode socialLinks 1`] = ` - --`; diff --git a/src/profile/forms/elements/EditButton.jsx b/src/profile/forms/elements/EditButton.jsx index 278b34d..80cb479 100644 --- a/src/profile/forms/elements/EditButton.jsx +++ b/src/profile/forms/elements/EditButton.jsx @@ -1,25 +1,34 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faPencilAlt } from '@fortawesome/free-solid-svg-icons'; +import { EditOutline } from '@openedx/paragon/icons'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { Button } from '@openedx/paragon'; - +import { Button, OverlayTrigger, Tooltip } from '@openedx/paragon'; import messages from './EditButton.messages'; const EditButton = ({ onClick, className, style, intl, }) => ( - + + ); export default injectIntl(EditButton); @@ -28,8 +37,6 @@ EditButton.propTypes = { onClick: PropTypes.func.isRequired, className: PropTypes.string, style: PropTypes.object, // eslint-disable-line - - // i18n intl: intlShape.isRequired, }; diff --git a/src/profile/forms/elements/EditableItemHeader.jsx b/src/profile/forms/elements/EditableItemHeader.jsx index 7b4b7b5..5381e3a 100644 --- a/src/profile/forms/elements/EditableItemHeader.jsx +++ b/src/profile/forms/elements/EditableItemHeader.jsx @@ -1,8 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; import EditButton from './EditButton'; import { Visibility } from './Visibility'; +import { useIsOnMobileScreen } from '../../data/hooks'; const EditableItemHeader = ({ content, @@ -11,15 +13,39 @@ const EditableItemHeader = ({ showEditButton, onClickEdit, headingId, -}) => ( -------- Social Links - -
-- - - - Just me - -
--
-- - - - Facebook - -
-- - - - Twitter - -
---); +}) => { + const isMobileView = useIsOnMobileScreen(); + return ( + <> +- {content} - {showEditButton ?
- {showVisibility ?: null} - : null} -
+++++ {content} +
++ {showEditButton ?+: null} + + {showVisibility ?+ > + ); +}; export default EditableItemHeader; @@ -33,7 +59,8 @@ EditableItemHeader.propTypes = { }; EditableItemHeader.defaultProps = { - onClickEdit: () => {}, + onClickEdit: () => { + }, showVisibility: false, showEditButton: false, content: '', diff --git a/src/profile/forms/elements/EmptyContent.jsx b/src/profile/forms/elements/EmptyContent.jsx index 1af947c..3978d80 100644 --- a/src/profile/forms/elements/EmptyContent.jsx +++ b/src/profile/forms/elements/EmptyContent.jsx @@ -4,16 +4,16 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faPlus } from '@fortawesome/free-solid-svg-icons'; const EmptyContent = ({ children, onClick, showPlusIcon }) => ( -: null} +
+{onClick ? ( ) : children} diff --git a/src/profile/forms/elements/FormControls.jsx b/src/profile/forms/elements/FormControls.jsx index e1fcaac..ddb2866 100644 --- a/src/profile/forms/elements/FormControls.jsx +++ b/src/profile/forms/elements/FormControls.jsx @@ -6,38 +6,47 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import messages from './FormControls.messages'; import { VisibilitySelect } from './Visibility'; +import { useIsVisibilityEnabled } from '../../data/hooks'; const FormControls = ({ cancelHandler, changeHandler, visibility, visibilityId, saveState, intl, }) => { - // Eliminate error/failed state for save button const buttonState = saveState === 'error' ? null : saveState; + const isVisibilityEnabled = useIsVisibilityEnabled(); return (-- --- -); @@ -68,7 +75,6 @@ FormControls.propTypes = { cancelHandler: PropTypes.func.isRequired, changeHandler: PropTypes.func.isRequired, - // i18n intl: intlShape.isRequired, }; diff --git a/src/profile/forms/elements/SwitchContent.jsx b/src/profile/forms/elements/SwitchContent.jsx index 1b45296..3bb1de5 100644 --- a/src/profile/forms/elements/SwitchContent.jsx +++ b/src/profile/forms/elements/SwitchContent.jsx @@ -3,18 +3,13 @@ import PropTypes from 'prop-types'; import { TransitionReplace } from '@openedx/paragon'; const onChildExit = (htmlNode) => { - // If the leaving child has focus, take control and redirect it if (htmlNode.contains(document.activeElement)) { - // Get the newly entering sibling. - // It's the previousSibling, but not for any explicit reason. So checking for both. const enteringChild = htmlNode.previousSibling || htmlNode.nextSibling; - // There's no replacement, do nothing. if (!enteringChild) { return; } - // Get all the focusable elements in the entering child and focus the first one const focusableElements = enteringChild.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'); if (focusableElements.length) { focusableElements[0].focus(); diff --git a/src/profile/forms/elements/Visibility.jsx b/src/profile/forms/elements/Visibility.jsx index cd59680..cfe67a2 100644 --- a/src/profile/forms/elements/Visibility.jsx +++ b/src/profile/forms/elements/Visibility.jsx @@ -23,7 +23,6 @@ const Visibility = ({ to, intl }) => { Visibility.propTypes = { to: PropTypes.oneOf(['private', 'all_users']), - // i18n intl: intlShape.isRequired, }; Visibility.defaultProps = { @@ -36,7 +35,7 @@ const VisibilitySelect = ({ intl, className, ...props }) => { return ( - +{ + {isVisibilityEnabled && ( + + ++ )} ++ ++ +++{ // Swallow clicks if the state is pending. // We do this instead of disabling the button to prevent // it from losing focus (disabled elements cannot have focus). @@ -45,15 +54,13 @@ const FormControls = ({ // Swallowing the onSubmit event on the form would be better, but // we would have to add that logic for every field given our // current structure of the application. - if (buttonState === 'pending') { - e.preventDefault(); - } - }} - disabledStates={[]} - /> - + if (buttonState === 'pending') { + e.preventDefault(); + } + }} + disabledStates={[]} + /> + @@ -58,7 +57,6 @@ VisibilitySelect.propTypes = { value: PropTypes.oneOf(['private', 'all_users']), onChange: PropTypes.func, - // i18n intl: intlShape.isRequired, }; VisibilitySelect.defaultProps = { diff --git a/src/profile/index.scss b/src/profile/index.scss index bee9a81..87907c2 100644 --- a/src/profile/index.scss +++ b/src/profile/index.scss @@ -17,29 +17,13 @@ } .profile-page-bg-banner { - height: 12rem; + height: 298px; + width: 100%; background-image: url('./assets/dot-pattern-light.png'); background-repeat: repeat-x; background-size: auto 85%; } -.username-description { - width: auto; - position: absolute; - left: 1.5rem; - top: 5.25rem; - color: var(--pgn-color-gray-500); - line-height: 0.9rem; - font-size: 0.8rem; - font-style: normal; - font-weight: 400; - margin-left: 0.9rem; -} - -.mb-2rem { - margin-bottom: 2rem; -} - .icon-visibility-off { height: 1rem; color: var(--pgn-color-gray-500); @@ -47,11 +31,12 @@ .profile-page { .edit-section-header { - font-size: var(--pgn-typography-font-size-h6-base); + font-size: var(--pgn-typography-font-size-h4-base); display: block; - font-weight: normal; + font-weight: 400; letter-spacing: 0; margin: 0; + line-height: 2.25rem; } label.edit-section-header { @@ -62,11 +47,16 @@ @media (--pgn-size-breakpoint-min-width-md) { max-width: 12rem; margin-right: 0; - margin-top: -8rem; - margin-bottom: 2rem; + height: auto; } } + .profile-avatar-button { + position: absolute; + left: 76px; + top: 76px; + } + .profile-avatar-menu-container { background: rgba(0,0,0,.65); position: absolute; @@ -77,7 +67,7 @@ align-items: center; border-radius: 50%; - @media (--pgn-size-breakpoint-min-width-md) { + @media (--pgn-size-breakpoint-min-width-md) { background: linear-gradient(to top, rgba(0,0,0,.65) 4rem, rgba(0,0,0,0) 4rem); align-items: flex-end; } @@ -95,7 +85,7 @@ } .btn { - color: var(--pgn-color-white); + color:var(--pgn-color-white);; background: transparent; border-color: transparent; margin: 0; @@ -104,13 +94,13 @@ } .profile-avatar { - width: 5rem; - height: 5rem; + width: 7.5rem; + height: 7.5rem; position: relative; @media (--pgn-size-breakpoint-min-width-md) { - width: 12rem; - height: 12rem; + width: 7.5rem; + height: 7.5rem; } .profile-avatar-edit-button { @@ -139,27 +129,137 @@ } .certificate { - position: relative; - - .certificate-title { - font-family: var(--pgn-typography-font-family-serif); - font-weight: 400; - } + background-color: #F3F1ED; + border-radius: 0.75rem; + overflow: hidden; + border: 1px #E7E4DB solid; .certificate-type-illustration { position: absolute; top: 1rem; right: 1rem; bottom: 0; - width: 12rem; + width: 15.15rem; opacity: .06; background-size: 90%; background-repeat: no-repeat; background-position: right top; } - - .card-body { - position: relative; - } } } + +.info-icon { + width: 1.5rem; + height: 1.5rem; + padding-left: 0.125rem; +} + +.max-width-32em { + max-width: 32em; +} + +.height-50vh { + height: 50vh; +} + +// Todo: Move the following to edx-paragon + +.btn-rounded { + border-radius: 100px; +} + +.min-width-179px { + min-width: 179px; +} + +.max-width-304px{ + max-width: 304px; +} + +.width-314px { + width: 314px; +} + +.w-90{ + max-width: 90%; +} + +.width-24px{ + width: 24px; +} + +.height-42px { + height: 42px; +} + +.rounded-75 { + border-radius: 0.75rem; +} + +.pt-40px{ + padding-top: 40px; +} + +.pl-40px { + padding-left: 40px; +} + +.py-10px{ + padding-top: 10px; + padding-bottom: 10px; +} + +.py-36px { + padding-top: 36px; + padding-bottom: 36px; +} + +.px-120px { + padding-left: 120px; + padding-right: 120px; +} + +.px-40px { + padding-left: 40px; + padding-right: 40px; +} + +.g-15rem { + gap: 1.5rem; +} + +.g-5rem { + gap: 0.5rem; +} + +.g-1rem { + gap: 1rem; +} + +.g-3rem { + gap: 3rem; +} + +.color-black { + color: #000; +} + +.bg-color-grey-FBFAF9 { + background-color: #FBFAF9; +} + +.background-black-65 { + background-color: rgba(0,0,0,.65) +} + +.object-fit-cover { + object-fit: cover; +} + +.lh-36px { + line-height: 36px; +} + +.overflowWrap-breakWord { + overflow-wrap: break-word; +} diff --git a/src/profile/utils.js b/src/profile/utils.js index 29981a6..4a68d4d 100644 --- a/src/profile/utils.js +++ b/src/profile/utils.js @@ -2,7 +2,6 @@ import camelCase from 'lodash.camelcase'; import snakeCase from 'lodash.snakecase'; export function modifyObjectKeys(object, modify) { - // If the passed in object is not an object, return it. if ( object === undefined || object === null @@ -15,7 +14,6 @@ export function modifyObjectKeys(object, modify) { return object.map(value => modifyObjectKeys(value, modify)); } - // Otherwise, process all its keys. const result = {}; Object.entries(object).forEach(([key, value]) => { result[modify(key)] = modifyObjectKeys(value, modify); diff --git a/src/routes/AppRoutes.jsx b/src/routes/AppRoutes.jsx index 5743a09..8fead5a 100644 --- a/src/routes/AppRoutes.jsx +++ b/src/routes/AppRoutes.jsx @@ -1,30 +1,18 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { AuthenticatedPageRoute, PageWrap } from '@edx/frontend-platform/react'; import { Routes, Route, useNavigate } from 'react-router-dom'; import { ProfilePage, NotFoundPage } from '../profile'; -import { ProfilePage as NewProfilePage, NotFoundPage as NewNotFoundPage } from '../profile-v2'; -const AppRoutes = ({ isNewProfileEnabled }) => { - const SelectedProfilePage = isNewProfileEnabled ? NewProfilePage : ProfilePage; - const SelectedNotFoundPage = isNewProfileEnabled ? NewNotFoundPage : NotFoundPage; +const AppRoutes = () => { const navigate = useNavigate(); return ( - ); }; -AppRoutes.propTypes = { - isNewProfileEnabled: PropTypes.bool, -}; - -AppRoutes.defaultProps = { - isNewProfileEnabled: null, -}; - export default AppRoutes; diff --git a/src/routes/routes.test.jsx b/src/routes/routes.test.jsx index 3b2b622..9bd40df 100644 --- a/src/routes/routes.test.jsx +++ b/src/routes/routes.test.jsx @@ -17,11 +17,6 @@ jest.mock('../profile', () => ({ NotFoundPage: () => (} /> - } /> - } /> + } /> + } /> + } /> Not found page), })); -jest.mock('../profile-v2', () => ({ - ProfilePage: () => (Profile page), - NotFoundPage: () => (Not found page), -})); - const RoutesWithProvider = (context, path) => (