Compare commits

..

17 Commits

Author SHA1 Message Date
renovate[bot]
fa7267f17c fix(deps): update dependency @openedx/paragon to v23.14.0 (#1230)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 06:54:42 +00:00
renovate[bot]
7b754edef8 fix(deps): update dependency @edx/frontend-component-header to v6.4.1 (#1229)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-07 06:54:14 +00:00
renovate[bot]
0bb7ee2fd4 fix(deps): update dependency core-js to v3.43.0 (#1223)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 11:39:56 +00:00
renovate[bot]
335dd7819d chore(deps): update dependency glob to v11.0.3 (#1222)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 11:39:43 +00:00
renovate[bot]
b18a01c302 fix(deps): update dependency @openedx/paragon to v23.13.0 (#1226)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 05:06:16 +00:00
Brian Smith
981dccf2d5 feat!: add design tokens support (#1220)
BREAKING CHANGE: Pre-design-tokens theming is no longer supported.

Co-authored-by: Diana Olarte <diana.olarte@edunext.co>
2025-06-18 12:13:59 -04:00
renovate[bot]
e8be148ca9 fix(deps): update dependency @edx/frontend-component-footer to v14.9.0 (#1217)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 08:03:40 +00:00
renovate[bot]
167a8bd9a8 fix(deps): update dependency @edx/frontend-platform to v8.3.9 (#1216)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-09 07:58:23 +00:00
renovate[bot]
db2336ac09 fix(deps): update react-router monorepo to v6.30.1 (#1214)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-26 06:07:45 +00:00
renovate[bot]
a13c25d4ea fix(deps): update dependency @edx/frontend-component-footer to v14.7.2 (#1213)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-26 06:07:26 +00:00
renovate[bot]
9452b72525 fix(deps): update dependency @edx/frontend-platform to v8.3.7 (#1209)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 13:58:23 +00:00
renovate[bot]
3601cb6c05 chore(deps): update commitlint monorepo to v19.8.1 (#1206)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 07:18:40 +00:00
renovate[bot]
b73c0f0f26 fix(deps): update dependency @edx/frontend-platform to v8.3.6 (#1207)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 07:07:33 +00:00
renovate[bot]
37feffc0db fix(deps): update dependency core-js to v3.42.0 (#1205)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 06:10:05 +00:00
renovate[bot]
c9d2813009 fix(deps): update dependency @edx/frontend-component-footer to v14.7.1 (#1204)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 06:09:50 +00:00
renovate[bot]
8ec67d9ed2 fix(deps): update dependency @edx/frontend-component-footer to v14.7.0 (#1203)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 06:04:06 +00:00
renovate[bot]
1d149f12ea chore(deps): update dependency glob to v11.0.2 (#1202)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 06:03:48 +00:00
13 changed files with 11243 additions and 2316 deletions

2
.env
View File

@@ -29,3 +29,5 @@ APP_ID=''
MFE_CONFIG_API_URL=''
SEARCH_CATALOG_URL=''
ENABLE_SKILLS_BUILDER_PROFILE=''
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -30,3 +30,5 @@ APP_ID=''
MFE_CONFIG_API_URL=''
SEARCH_CATALOG_URL='http://localhost:18000/courses'
ENABLE_SKILLS_BUILDER_PROFILE=''
# Fallback in local style files
PARAGON_THEME_URLS={}

View File

@@ -25,3 +25,4 @@ LEARNER_RECORD_MFE_BASE_URL='http://localhost:1990'
COLLECT_YEAR_OF_BIRTH=true
APP_ID=''
MFE_CONFIG_API_URL=''
PARAGON_THEME_URLS={}

13214
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -39,12 +39,11 @@
"@fortawesome/free-regular-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "0.2.2",
"@openedx/frontend-plugin-framework": "^1.7.0",
"@openedx/paragon": "^22.17.0",
"@openedx/paragon": "^23.4.5",
"@pact-foundation/pact": "^11.0.2",
"@redux-devtools/extension": "3.3.0",
"classnames": "2.5.1",
"core-js": "3.41.0",
"core-js": "3.43.0",
"history": "5.3.0",
"lodash.camelcase": "4.3.0",
"lodash.get": "4.4.2",
@@ -55,8 +54,8 @@
"react-dom": "18.3.1",
"react-helmet": "6.1.0",
"react-redux": "7.2.9",
"react-router": "6.30.0",
"react-router-dom": "6.30.0",
"react-router": "6.30.1",
"react-router-dom": "6.30.1",
"redux": "4.2.1",
"redux-logger": "3.0.6",
"redux-saga": "1.3.0",
@@ -66,14 +65,14 @@
"universal-cookie": "4.0.4"
},
"devDependencies": {
"@commitlint/cli": "19.8.0",
"@commitlint/config-angular": "19.8.0",
"@commitlint/cli": "19.8.1",
"@commitlint/config-angular": "19.8.1",
"@edx/browserslist-config": "^1.1.1",
"@edx/reactifex": "2.2.0",
"@openedx/frontend-build": "^14.3.3",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "14.3.1",
"glob": "11.0.1",
"glob": "11.0.3",
"reactifex": "1.1.1",
"redux-mock-store": "1.5.5"
}

View File

@@ -1,7 +1,5 @@
@import "~@edx/brand/paragon/fonts";
@import "~@edx/brand/paragon/variables";
@import "~@openedx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
@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";

View File

@@ -1,97 +0,0 @@
# 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.
![Screenshot of Custom Fields](./images/custom_fields.png)
### 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);
```

View File

@@ -1,129 +0,0 @@
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 mt-5">
<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 not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -1,37 +0,0 @@
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;

View File

@@ -43,8 +43,6 @@ import messages from './ProfilePage.messages';
import withParams from '../utils/hoc';
import AdditionalProfileFieldsSlot from '../plugin-slots/AdditionalProfileFieldsSlot';
ensureConfig(['CREDENTIALS_BASE_URL', 'LMS_BASE_URL'], 'ProfilePage');
class ProfilePage extends React.Component {
@@ -291,9 +289,6 @@ class ProfilePage extends React.Component {
{...commonFormProps}
/>
)}
<div className="mb-4">
<AdditionalProfileFieldsSlot />
</div>
</div>
<div className="pt-md-3 col-md-8 col-lg-7 offset-lg-1">
{!this.isYOBDisabled() && this.renderAgeMessage()}

View File

@@ -547,9 +547,6 @@ exports[`<ProfilePage /> Renders correctly in various states successfully redire
</ul>
</div>
</div>
<div
class="mb-4"
/>
</div>
<div
class="pt-md-3 col-md-8 col-lg-7 offset-lg-1"
@@ -2633,9 +2630,6 @@ exports[`<ProfilePage /> Renders correctly in various states test country edit w
</ul>
</div>
</div>
<div
class="mb-4"
/>
</div>
<div
class="pt-md-3 col-md-8 col-lg-7 offset-lg-1"
@@ -3635,9 +3629,6 @@ exports[`<ProfilePage /> Renders correctly in various states test education edit
</ul>
</div>
</div>
<div
class="mb-4"
/>
</div>
<div
class="pt-md-3 col-md-8 col-lg-7 offset-lg-1"
@@ -5512,9 +5503,6 @@ exports[`<ProfilePage /> Renders correctly in various states test preferreded la
</ul>
</div>
</div>
<div
class="mb-4"
/>
</div>
<div
class="pt-md-3 col-md-8 col-lg-7 offset-lg-1"
@@ -6108,9 +6096,6 @@ exports[`<ProfilePage /> Renders correctly in various states viewing other profi
/>
</div>
</div>
<div
class="mb-4"
/>
</div>
<div
class="pt-md-3 col-md-8 col-lg-7 offset-lg-1"
@@ -6801,9 +6786,6 @@ exports[`<ProfilePage /> Renders correctly in various states viewing own profile
</ul>
</div>
</div>
<div
class="mb-4"
/>
</div>
<div
class="pt-md-3 col-md-8 col-lg-7 offset-lg-1"
@@ -7681,9 +7663,6 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
</ul>
</div>
</div>
<div
class="mb-4"
/>
</div>
<div
class="pt-md-3 col-md-8 col-lg-7 offset-lg-1"
@@ -8625,9 +8604,6 @@ exports[`<ProfilePage /> Renders correctly in various states while saving an edi
</ul>
</div>
</div>
<div
class="mb-4"
/>
</div>
<div
class="pt-md-3 col-md-8 col-lg-7 offset-lg-1"
@@ -9497,9 +9473,6 @@ exports[`<ProfilePage /> Renders correctly in various states without credentials
</ul>
</div>
</div>
<div
class="mb-4"
/>
</div>
<div
class="pt-md-3 col-md-8 col-lg-7 offset-lg-1"

View File

@@ -28,7 +28,7 @@
position: absolute;
left: 1.5rem;
top: 5.25rem;
color: $gray-500;
color: var(--pgn-color-gray-500);
line-height: 0.9rem;
font-size: 0.8rem;
font-style: normal;
@@ -42,12 +42,12 @@
.icon-visibility-off {
height: 1rem;
color: $gray-500;
color: var(--pgn-color-gray-500);
}
.profile-page {
.edit-section-header {
@extend .h6;
font-size: var(--pgn-typography-font-size-h6-base);
display: block;
font-weight: normal;
letter-spacing: 0;
@@ -55,11 +55,11 @@
}
label.edit-section-header {
margin-bottom: $spacer * .5;
margin-bottom: calc(var(--pgn-spacing-spacer-base) * .5);
}
.profile-avatar-wrap {
@include media-breakpoint-up(md) {
@media (--pgn-size-breakpoint-min-width-md) {
max-width: 12rem;
margin-right: 0;
margin-top: -8rem;
@@ -77,25 +77,25 @@
align-items: center;
border-radius: 50%;
@include media-breakpoint-up(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;
}
.btn {
text-decoration: none;
@include media-breakpoint-up(md) {
@media (--pgn-size-breakpoint-min-width-md) {
margin-bottom: 1.2rem;
}
}
.dropdown {
@include media-breakpoint-up(md) {
@media (--pgn-size-breakpoint-min-width-md) {
margin-bottom: 1.2rem;
}
.btn {
color: $white;
color: var(--pgn-color-white);
background: transparent;
border-color: transparent;
margin: 0;
@@ -108,7 +108,7 @@
height: 5rem;
position: relative;
@include media-breakpoint-up(md) {
@media (--pgn-size-breakpoint-min-width-md) {
width: 12rem;
height: 12rem;
}
@@ -128,7 +128,7 @@
border-radius:0;
transition: opacity 200ms ease;
@include media-breakpoint-up(md) {
@media (--pgn-size-breakpoint-min-width-md) {
height: 4rem;
}
@@ -142,7 +142,7 @@
position: relative;
.certificate-title {
font-family: $font-family-serif;
font-family: var(--pgn-typography-font-family-serif);
font-weight: 400;
}