-
Notifications
You must be signed in to change notification settings - Fork 125
feat: add slot to extend the profile fields #1211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
brian-smith-tcril
merged 15 commits into
openedx:master
from
eduNEXT:bc/add-extra-fields-slot
Aug 13, 2025
Merged
Changes from 13 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
60b63e2
feat: add extended profile fields functionality with context and form…
bra-i-am 76e8855
refactor: replace string literals with FORM_MODE constants in profile…
bra-i-am a3754a9
feat: implement BaseField component and refactor field elements to us…
bra-i-am 696157d
chore: remove unused webpack development configuration file
bra-i-am 39181ad
feat: refactor extended profile fields implementation and remove unus…
bra-i-am 974f3f8
feat: update dependencies for frontend-plugin-framework and remove un…
bra-i-am 482f19b
refactor: simplify pluginProps structure in ExtendedProfileFieldsSlot…
bra-i-am fae0cd0
feat: add README and example images for Extended Profile Fields slot
bra-i-am 3ef7ff2
refactor: improve performance & keep consistency
bra-i-am 40f3dc4
feat: add Additional Profile Fields slot with example implementation …
bra-i-am f1813e8
feat: update custom fields image for Additional Profile Fields slot
bra-i-am d7e849b
fix: reorder import of AdditionalProfileFieldsSlot for consistency
bra-i-am 0369b8f
test: fix snapshot
bra-i-am e60629d
fix: adjust margin in example to avoid oddities on mobile
bra-i-am b2a686b
fix: remove unnecessary empty divs from ProfilePage snapshots
bra-i-am File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
# Additional Profile Fields | ||
|
||
### Slot ID: `org.openedx.frontend.profile.additional_profile_fields.v1` | ||
|
||
## Description | ||
|
||
This slot is used to replace/modify/hide the additional profile fields in the profile page. | ||
|
||
## Example | ||
The following `env.config.jsx` will extend the default fields with a additional custom fields through a simple example component. | ||
|
||
 | ||
|
||
### Using the Additional Fields Component | ||
Create a file named `env.config.jsx` at the MFE root with this: | ||
|
||
```jsx | ||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; | ||
import Example from './src/plugin-slots/AdditionalProfileFieldsSlot/example'; | ||
|
||
const config = { | ||
pluginSlots: { | ||
'org.openedx.frontend.profile.additional_profile_fields.v1': { | ||
plugins: [ | ||
{ | ||
op: PLUGIN_OPERATIONS.Insert, | ||
widget: { | ||
id: 'additional_profile_fields', | ||
type: DIRECT_PLUGIN, | ||
RenderWidget: Example, | ||
}, | ||
}, | ||
], | ||
}, | ||
}, | ||
}; | ||
|
||
export default config; | ||
``` | ||
|
||
## Plugin Props | ||
|
||
When implementing a plugin for this slot, the following props are available: | ||
|
||
### `updateUserProfile` | ||
- **Type**: Function | ||
- **Description**: A function for updating the user's profile with new field values. This handles the API call to persist changes to the backend. | ||
- **Usage**: Pass an object containing the field updates to be saved to the user's profile. The function automatically handles the persistence and UI updates. | ||
|
||
#### Example | ||
```javascript | ||
updateUserProfile({ extendedProfile: [{ fieldName: 'favorite_color', fieldValue: value }] }); | ||
``` | ||
|
||
### `profileFieldValues` | ||
- **Type**: Array of Objects | ||
- **Description**: Contains the current values of all additional profile fields as an array of objects. Each object has a `fieldName` property (string) and a `fieldValue` property (which can be string, boolean, number, or other data types depending on the field type). | ||
- **Usage**: Access specific field values by finding the object with the matching `fieldName` and reading its `fieldValue` property. Use array methods like `find()` to locate specific fields. | ||
|
||
#### Example | ||
```javascript | ||
// Finding a specific field value | ||
const nifField = profileFieldValues.find(field => field.fieldName === 'nif'); | ||
const nifValue = nifField ? nifField.fieldValue : null; | ||
|
||
// Example data structure: | ||
[ | ||
{ | ||
"fieldName": "favorite_color", | ||
"fieldValue": "red" | ||
}, | ||
{ | ||
"fieldName": "employment_situation", | ||
"fieldValue": "Unemployed" | ||
}, | ||
] | ||
``` | ||
|
||
### `profileFieldErrors` | ||
- **Type**: Object | ||
- **Description**: Contains validation errors for profile fields. Each key corresponds to a field name, and the value is the error message. | ||
- **Usage**: Check for field-specific errors to display validation feedback to users. | ||
|
||
### `formComponents` | ||
- **Type**: Object | ||
- **Description**: Provides access to reusable form components that are consistent with the rest of the profile page styling and behavior. These components follow the platform's design system and include proper validation and accessibility features. | ||
- **Usage**: Use these components in your custom fields implementation to maintain UI consistency. Available components include `SwitchContent` for managing different UI states, `EmptyContent` for empty states, and `EditableItemHeader` for consistent headers. | ||
|
||
### `refreshUserProfile` | ||
- **Type**: Function | ||
- **Description**: A function that triggers a refresh of the user's profile data. This can be used after updating profile fields to ensure the UI reflects the latest data from the server. | ||
- **Usage**: Call this function with the username parameter when you need to manually reload the user profile information. Note that `updateUserProfile` typically handles data refresh automatically. | ||
|
||
#### Example | ||
```javascript | ||
refreshUserProfile(username); | ||
``` |
129 changes: 129 additions & 0 deletions
129
src/plugin-slots/AdditionalProfileFieldsSlot/example/index.jsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import { useEffect, useState } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { Button } from '@openedx/paragon'; | ||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; | ||
|
||
/** | ||
* Straightforward example of how you could use the pluginProps provided by | ||
* the AdditionalProfileFieldsSlot to create a custom profile field. | ||
* | ||
* Here you can set a 'favorite_color' field with radio buttons and | ||
* save it to the user's profile, especifically to their `meta` in | ||
* the user's model. For more information, see the documentation: | ||
* | ||
* https://github.com/openedx/edx-platform/blob/master/openedx/core/djangoapps/user_api/README.rst#persisting-optional-user-metadata | ||
*/ | ||
const Example = ({ | ||
updateUserProfile, | ||
profileFieldValues, | ||
profileFieldErrors, | ||
formComponents: { SwitchContent, EditableItemHeader, EmptyContent } = {}, | ||
}) => { | ||
const authenticatedUser = getAuthenticatedUser(); | ||
const [formMode, setFormMode] = useState('editable'); | ||
|
||
// Get current favorite color from profileFieldValues | ||
const currentColorField = profileFieldValues?.find(field => field.fieldName === 'favorite_color'); | ||
const currentColor = currentColorField ? currentColorField.fieldValue : ''; | ||
|
||
const [value, setValue] = useState(currentColor); | ||
const handleChange = e => setValue(e.target.value); | ||
|
||
// Get any validation errors for the favorite_color field | ||
const colorFieldError = profileFieldErrors?.favorite_color; | ||
|
||
useEffect(() => { | ||
if (!value) { setFormMode('empty'); } | ||
if (colorFieldError) { | ||
setFormMode('editing'); | ||
} | ||
}, [colorFieldError, value]); | ||
|
||
const handleSubmit = () => { | ||
try { | ||
updateUserProfile(authenticatedUser.username, { extendedProfile: [{ fieldName: 'favorite_color', fieldValue: value }] }); | ||
setFormMode('editable'); | ||
} catch (error) { | ||
setFormMode('editing'); | ||
} | ||
}; | ||
|
||
return ( | ||
<div className="border .border-accent-500 p-3"> | ||
<h3 className="h3">Example Additional Profile Fields Slot</h3> | ||
|
||
<SwitchContent | ||
className="pt-40px" | ||
expression={formMode} | ||
cases={{ | ||
editing: ( | ||
<> | ||
<label className="edit-section-header" htmlFor="favorite_color"> | ||
Favorite Color | ||
</label> | ||
<input | ||
className="form-control" | ||
id="favorite_color" | ||
name="favorite_color" | ||
value={value} | ||
onChange={handleChange} | ||
/> | ||
<Button type="button" className="mt-2" onClick={handleSubmit}> | ||
Save | ||
</Button> | ||
</> | ||
), | ||
editable: ( | ||
<> | ||
<div className="row m-0 pb-1.5 align-items-center"> | ||
<p data-hj-suppress className="h5 font-weight-bold m-0"> | ||
Favorite Color | ||
</p> | ||
</div> | ||
<EditableItemHeader | ||
content={value} | ||
showEditButton | ||
onClickEdit={() => setFormMode('editing')} | ||
showVisibility={false} | ||
visibility="private" | ||
/> | ||
</> | ||
), | ||
empty: ( | ||
<> | ||
<div className="row m-0 pb-1.5 align-items-center"> | ||
<p data-hj-suppress className="h5 font-weight-bold m-0"> | ||
Favorite Color | ||
</p> | ||
</div> | ||
<EmptyContent onClick={() => setFormMode('editing')}> | ||
<p className="mb-0">Click to add your favorite color</p> | ||
</EmptyContent> | ||
</> | ||
), | ||
}} | ||
/> | ||
|
||
</div> | ||
); | ||
}; | ||
|
||
Example.propTypes = { | ||
updateUserProfile: PropTypes.func.isRequired, | ||
profileFieldValues: PropTypes.arrayOf( | ||
PropTypes.shape({ | ||
fieldName: PropTypes.string.isRequired, | ||
fieldValue: PropTypes.oneOfType([ | ||
PropTypes.string, | ||
PropTypes.bool, | ||
PropTypes.number, | ||
]).isRequired, | ||
}), | ||
), | ||
profileFieldErrors: PropTypes.objectOf(PropTypes.string), | ||
formComponents: PropTypes.shape({ | ||
SwitchContent: PropTypes.elementType.isRequired, | ||
}), | ||
}; | ||
|
||
export default Example; |
Binary file added
BIN
+62.6 KB
src/plugin-slots/AdditionalProfileFieldsSlot/images/custom_fields.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { PluginSlot } from '@openedx/frontend-plugin-framework'; | ||
import { useDispatch, useSelector } from 'react-redux'; | ||
|
||
import { useCallback } from 'react'; | ||
import { patchProfile } from '../../profile/data/services'; | ||
import { fetchProfile } from '../../profile/data/actions'; | ||
|
||
import SwitchContent from '../../profile/forms/elements/SwitchContent'; | ||
import EmptyContent from '../../profile/forms/elements/EmptyContent'; | ||
import EditableItemHeader from '../../profile/forms/elements/EditableItemHeader'; | ||
|
||
const AdditionalProfileFieldsSlot = () => { | ||
const dispatch = useDispatch(); | ||
const extendedProfileValues = useSelector((state) => state.profilePage.account.extendedProfile); | ||
const errors = useSelector((state) => state.profilePage.errors); | ||
|
||
const pluginProps = { | ||
refreshUserProfile: useCallback((username) => dispatch(fetchProfile(username)), [dispatch]), | ||
updateUserProfile: patchProfile, | ||
profileFieldValues: extendedProfileValues, | ||
profileFieldErrors: errors, | ||
formComponents: { | ||
SwitchContent, | ||
EmptyContent, | ||
EditableItemHeader, | ||
}, | ||
}; | ||
|
||
return ( | ||
<PluginSlot | ||
id="org.openedx.frontend.profile.additional_profile_fields.v1" | ||
pluginProps={pluginProps} | ||
/> | ||
); | ||
}; | ||
|
||
export default AdditionalProfileFieldsSlot; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
pt-40px
here will lead to layout oddities on mobile.Without the
pt-40px
div
With the
pt-40px
div