From 12fd62ffa882e1d33ead8d985875a17ab8bafb3e Mon Sep 17 00:00:00 2001 From: Adam Butterworth Date: Tue, 30 Apr 2019 16:56:16 -0400 Subject: [PATCH] feat: add social links (#11) --- src/account-settings/AccountSettingsPage.jsx | 21 ++++- .../AccountSettingsPage.messages.jsx | 26 ++++++ .../components/EditableField.jsx | 21 +---- src/account-settings/reducers.js | 3 + src/account-settings/sagas.js | 4 +- src/account-settings/service.js | 90 ++++++++++++++----- 6 files changed, 124 insertions(+), 41 deletions(-) diff --git a/src/account-settings/AccountSettingsPage.jsx b/src/account-settings/AccountSettingsPage.jsx index fe2d42d..4946654 100644 --- a/src/account-settings/AccountSettingsPage.jsx +++ b/src/account-settings/AccountSettingsPage.jsx @@ -18,6 +18,7 @@ import { PageLoading } from '../common'; import EditableField from './components/EditableField'; import PasswordReset from './components/PasswordReset'; import ThirdPartyAuth from './components/ThirdPartyAuth'; + import { YEAR_OF_BIRTH_OPTIONS, EDUCATION_LEVELS, @@ -101,11 +102,27 @@ class AccountSettingsPage extends React.Component { name="language_proficiencies" type="select" options={this.languageProficiencyOptions} - transformValue={v => (v.length ? v[0].code : null)} - reverseTransform={v => ([{ code: v }])} label={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies'])} /> + +

{this.props.intl.formatMessage(messages['account.settings.section.social.media'])}

+

{this.props.intl.formatMessage(messages['account.settings.section.social.media.description'])}

+ + + diff --git a/src/account-settings/AccountSettingsPage.messages.jsx b/src/account-settings/AccountSettingsPage.messages.jsx index 88e2d21..374e9a8 100644 --- a/src/account-settings/AccountSettingsPage.messages.jsx +++ b/src/account-settings/AccountSettingsPage.messages.jsx @@ -144,6 +144,32 @@ const messages = defineMessages({ defaultMessage: 'Spoken Languages', description: 'Label for account settings spoken languages field.', }, + + 'account.settings.section.social.media': { + id: 'account.settings.section.social.media', + defaultMessage: 'Social Media Links', + description: 'Section header for social media links settings', + }, + 'account.settings.section.social.media.description': { + id: 'account.settings.section.social.media.description', + defaultMessage: 'Optionally, link your personal accounts to the social media icons on your edX profile.', + description: 'Section subheader for social media links settings', + }, + 'account.settings.field.social.platform.name.linkedin': { + id: 'account.settings.field.social.platform.name.linkedin', + defaultMessage: 'LinkedIn', + description: 'Label for LinkedIn', + }, + 'account.settings.field.social.platform.name.twitter': { + id: 'account.settings.field.social.platform.name.twitter', + defaultMessage: 'Twitter', + description: 'Label for Twitter', + }, + 'account.settings.field.social.platform.name.facebook': { + id: 'account.settings.field.social.platform.name.facebook', + defaultMessage: 'Facebook', + description: 'Label for Facebook', + }, }); export default messages; diff --git a/src/account-settings/components/EditableField.jsx b/src/account-settings/components/EditableField.jsx index 6a67550..a390643 100644 --- a/src/account-settings/components/EditableField.jsx +++ b/src/account-settings/components/EditableField.jsx @@ -24,7 +24,7 @@ function EditableField(props) { name, label, type, - value: propValue, + value, options, saveState, error, @@ -38,18 +38,12 @@ function EditableField(props) { isEditing, isEditable, intl, - transformValue, - reverseTransform, ...others } = props; const id = `field-${name}`; - const value = transformValue(propValue); const getValue = (rawValue) => { if (options) { - if (Array.isArray(rawValue)) { - return rawValue.map(getValue).join(', '); - } // Use == instead of === to prevent issues when HTML casts numbers as strings // eslint-disable-next-line eqeqeq const selectedOption = options.find(option => option.value == rawValue); @@ -60,14 +54,11 @@ function EditableField(props) { const handleSubmit = (e) => { e.preventDefault(); - const data = { - [name]: reverseTransform(new FormData(e.target).get(name)), - }; - onSubmit(name, data); + onSubmit(name, new FormData(e.target).get(name)); }; const handleChange = (e) => { - onChange(name, reverseTransform(e.target.value)); + onChange(name, e.target.value); }; const handleEdit = () => { @@ -81,7 +72,7 @@ function EditableField(props) { const renderConfirmationMessage = () => { if (!confirmationMessageDefinition || !confirmationValue) return null; return intl.formatMessage(confirmationMessageDefinition, { - value: transformValue(confirmationValue), + value: confirmationValue, }); }; @@ -190,8 +181,6 @@ EditableField.propTypes = { isEditing: PropTypes.bool, isEditable: PropTypes.bool, intl: intlShape.isRequired, - transformValue: PropTypes.func, - reverseTransform: PropTypes.func, }; EditableField.defaultProps = { @@ -205,8 +194,6 @@ EditableField.defaultProps = { helpText: undefined, isEditing: false, isEditable: true, - transformValue: v => v, - reverseTransform: v => v, }; diff --git a/src/account-settings/reducers.js b/src/account-settings/reducers.js index 93c19e6..ad585b5 100644 --- a/src/account-settings/reducers.js +++ b/src/account-settings/reducers.js @@ -60,6 +60,9 @@ const accountSettingsReducer = (state = defaultState, action) => { return { ...state, openFormId: action.payload.formId, + saveState: null, + errors: {}, + drafts: {}, }; case CLOSE_FORM: dispatcherIsOpenForm = action.payload.formId === state.openFormId; diff --git a/src/account-settings/sagas.js b/src/account-settings/sagas.js index d03c964..1da5d6c 100644 --- a/src/account-settings/sagas.js +++ b/src/account-settings/sagas.js @@ -46,8 +46,8 @@ export function* handleSaveAccount(action) { yield put(saveAccountBegin()); const username = yield select(usernameSelector); - const { commitValues } = action.payload; - const savedValues = yield call(ApiService.patchAccount, username, commitValues); + const { commitValues, formId } = action.payload; + const savedValues = yield call(ApiService.patchAccount, username, { [formId]: commitValues }); yield put(saveAccountSuccess(savedValues, commitValues)); yield put(closeForm(action.payload.formId)); } catch (e) { diff --git a/src/account-settings/service.js b/src/account-settings/service.js index 2b5a8f7..ae8cf3b 100644 --- a/src/account-settings/service.js +++ b/src/account-settings/service.js @@ -7,7 +7,13 @@ let config = { PASSWORD_RESET_URL: null, }; -let apiClient = null; // eslint-disable-line no-unused-vars +const SOCIAL_PLATFORMS = [ + { id: 'twitter', key: 'social_link_twitter' }, + { id: 'facebook', key: 'social_link_facebook' }, + { id: 'linkedin', key: 'social_link_linkedin' }, +]; + +let apiClient = null; function validateConfiguration(newConfig) { Object.keys(config).forEach((key) => { @@ -17,30 +23,75 @@ function validateConfiguration(newConfig) { }); } -function handleRequestError(error) { - if (error.response && error.response.data.field_errors) { - const apiError = Object.create(error); - apiError.fieldErrors = Object.entries(error.response.data.field_errors) - .reduce((acc, [k, v]) => { - acc[k] = v.user_message; - return acc; - }, {}); - - throw apiError; - } - throw error; -} - export function configureApiService(newConfig, newApiClient) { validateConfiguration(newConfig); config = pick(newConfig, Object.keys(config)); apiClient = newApiClient; } + +function unpackFieldErrors(fieldErrors) { + const unpackedFieldErrors = fieldErrors; + if (fieldErrors.social_links) { + SOCIAL_PLATFORMS.forEach(({ key }) => { + unpackedFieldErrors[key] = fieldErrors.social_links; + }); + } + return Object.entries(unpackedFieldErrors) + .reduce((acc, [k, v]) => { + acc[k] = v.user_message; + return acc; + }, {}); +} + +function unpackAccountResponseData(data) { + const unpackedData = data; + + SOCIAL_PLATFORMS.forEach(({ id, key }) => { + const platformData = data.social_links.find(({ platform }) => platform === id); + unpackedData[key] = typeof platformData === 'object' ? platformData.social_link : ''; + }); + + if (Array.isArray(data.language_proficiencies)) { + if (data.language_proficiencies.length) { + unpackedData.language_proficiencies = data.language_proficiencies[0].code; + } else { + unpackedData.language_proficiencies = ''; + } + } + + return unpackedData; +} +function packAccountCommitData(commitData) { + const packedData = commitData; + + SOCIAL_PLATFORMS.forEach(({ id, key }) => { + if (commitData[key]) { + packedData.social_links = [{ platform: id, social_link: commitData[key] }]; + } + delete packedData[key]; + }); + + if (commitData.language_proficiencies) { + packedData.language_proficiencies = [{ code: commitData.language_proficiencies }]; + } + return packedData; +} + + +function handleRequestError(error) { + if (error.response && error.response.data.field_errors) { + const apiError = Object.create(error); + apiError.fieldErrors = unpackFieldErrors(error.response.data.field_errors); + throw apiError; + } + throw error; +} + + export async function getAccount(username) { const { data } = await apiClient.get(`${config.ACCOUNTS_API_BASE_URL}/${username}`); - - return data; + return unpackAccountResponseData(data); } export async function patchAccount(username, commitValues) { @@ -50,11 +101,11 @@ export async function patchAccount(username, commitValues) { const { data } = await apiClient.patch( `${config.ACCOUNTS_API_BASE_URL}/${username}`, - commitValues, + packAccountCommitData(commitValues), requestConfig, ).catch(handleRequestError); - return data; + return unpackAccountResponseData(data); } export async function postResetPassword() { @@ -65,7 +116,6 @@ export async function postResetPassword() { return data; } - export async function getThirdPartyAuthProviders() { const { data } = await apiClient.get(`${config.LMS_BASE_URL}/api/third_party_auth/v0/providers/user_status`) .catch(handleRequestError);