Compare commits

...

74 Commits

Author SHA1 Message Date
Brayan Ceron
e7cddb60f7 chore: update snapshots 2026-01-28 19:48:32 +05:30
Brayan Ceron
9b6e928260 feat: wrap AdditionalProfileFieldsSlot in a div for improved layout 2026-01-28 19:48:32 +05:30
Brayan Cerón
0e676ffff3 feat: add slot to extend the profile fields (#1211)
* feat: add extended profile fields functionality with context and form components

* refactor: replace string literals with FORM_MODE constants in profile fields components

* feat: implement BaseField component and refactor field elements to use it

* chore: remove unused webpack development configuration file

* feat: refactor extended profile fields implementation and remove unused components

* feat: update dependencies for frontend-plugin-framework and remove unused dompurify

* refactor: simplify pluginProps structure in ExtendedProfileFieldsSlot component

* feat: add README and example images for Extended Profile Fields slot

* refactor: improve performance & keep consistency

* feat: add Additional Profile Fields slot with example implementation and documentation

* feat: update custom fields image for Additional Profile Fields slot

* fix: reorder import of AdditionalProfileFieldsSlot for consistency

* test: fix snapshot

* fix: adjust margin in example to avoid oddities on mobile

* fix: remove unnecessary empty divs from ProfilePage snapshots
2026-01-28 19:48:32 +05:30
Brian Smith
9516ee0e92 feat: import FooterSlot from component package instead of slot package (#1198) 2025-04-24 12:11:20 -04:00
renovate[bot]
29fd7176c8 fix(deps): update dependency @edx/frontend-component-header to v6.4.0 (#1201)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-23 20:18:38 +00:00
renovate[bot]
16ddd7abba fix(deps): update dependency @edx/openedx-atlas to ^0.7.0 (#1200)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-21 05:46:19 +00:00
renovate[bot]
577ef6ab0b fix(deps): update dependency @edx/frontend-component-footer to v14.6.0 (#1199)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-21 05:46:13 +00:00
renovate[bot]
8268fa4eab fix(deps): update dependency @edx/frontend-component-header to v6.3.0 (#1197)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-14 06:11:54 +00:00
renovate[bot]
5665f8a0d6 fix(deps): update dependency @edx/frontend-component-footer to v14.4.0 (#1196)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-14 06:11:46 +00:00
renovate[bot]
4b7a3207e0 fix(deps): update dependency @edx/frontend-platform to v8.3.4 (#1195)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-07 07:51:52 +00:00
Hunia Fatima
3db0289aab feat: upgrade react to v18 (#1190) 2025-04-04 11:16:23 -04:00
renovate[bot]
85d3eca9e4 fix(deps): update react-router monorepo to v6.30.0 (#1192)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-31 06:47:19 +00:00
renovate[bot]
f7fd2959ac chore(deps): update dependency @openedx/frontend-build to v14.4.1 (#1191)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-31 06:47:09 +00:00
Brian Smith
8652206aa4 chore(deps): update @openedx dependencies to versions that support React 18 (#1189) 2025-03-27 16:17:12 -04:00
renovate[bot]
7a5e03967d fix(deps): update dependency core-js to v3.41.0 (#1188)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 06:30:58 +00:00
renovate[bot]
da19dfaadc fix(deps): update dependency @edx/frontend-platform to v8.3.3 (#1187)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 06:30:34 +00:00
renovate[bot]
60d960276d fix(deps): update dependency @edx/frontend-platform to v8.3.1 (#1178)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-18 11:02:27 +00:00
Awais Ansari
c8a6f9fbd8 test: updated profile test cases (#1182) 2025-03-18 15:58:48 +05:00
renovate[bot]
2ef5a7baff chore(deps): update commitlint monorepo to v19.8.0 (#1177)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 01:35:10 -04:00
renovate[bot]
d59c641b3d chore(deps): update dependency @openedx/frontend-build to v14.3.2 (#1176)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-10 01:34:58 -04:00
renovate[bot]
4ea80a2a09 chore(deps): update dependency @openedx/frontend-build to v14.3.1 (#1175)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 01:52:17 -05:00
renovate[bot]
45fe50f7f7 fix(deps): update dependency @openedx/paragon to v22.15.3 (#1174)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 01:51:54 -05:00
renovate[bot]
933a177c78 fix(deps): update dependency @openedx/paragon to v22.15.2 (#1170)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 06:10:14 +00:00
renovate[bot]
14fff570a4 fix(deps): update dependency @edx/frontend-component-header to v5.8.3 (#1169)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-24 01:09:59 -05:00
sundasnoreen12
3d0f3806e1 Merge pull request #1168 from openedx/revert-1166-sundas/INF-1779
Revert "feat: addd countries changes for embargo"
2025-02-22 15:47:48 +05:00
sundasnoreen12
b597e2cc14 Revert "feat: addd countries changes for embargo" 2025-02-22 15:44:11 +05:00
sundasnoreen12
22dc85470a Merge pull request #1166 from openedx/sundas/INF-1779
feat: addd countries changes for embargo
2025-02-22 15:10:21 +05:00
sundasnoreen12
9a26dc7088 refactor: refactored test file 2025-02-20 16:55:53 +05:00
sundasnoreen12
770a248d8c refactor: fixed review suggestions 2025-02-19 16:14:48 +05:00
sundasnoreen12
70cb1803b4 refactor: added constant 2025-02-17 21:09:11 +05:00
sundasnoreen12
e9aa787ade test: fixed test case 2025-02-17 18:29:21 +05:00
sundasnoreen12
01e2f1af79 test: fixed test cases 2025-02-17 18:10:57 +05:00
sundasnoreen12
1e67d51394 feat: addd countries changes for embargo 2025-02-17 13:18:02 +05:00
renovate[bot]
95936419c2 fix(deps): update react-router monorepo to v6.29.0 (#1162)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-10 08:50:22 +00:00
renovate[bot]
00ada93994 fix(deps): update font awesome to v6.7.2 (#1161)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-10 03:50:09 -05:00
renovate[bot]
c57a924cc3 fix(deps): update dependency core-js to v3.40.0 (#1160)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-10 05:36:00 +00:00
renovate[bot]
dc2f03dfad fix(deps): update dependency @openedx/paragon to v22.15.1 (#1159)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-10 00:35:51 -05:00
renovate[bot]
5c7a521705 chore(deps): update dependency @edx/browserslist-config to v1.5.0 (#1158)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-03 05:11:55 +00:00
renovate[bot]
074374b2af chore(deps): update commitlint monorepo to v19.7.1 (#1157)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-03 00:11:44 -05:00
renovate[bot]
0a3aad38dc fix(deps): update dependency @edx/frontend-platform to v8.1.5 (#1156)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-27 05:10:58 +00:00
renovate[bot]
1730b5a2c4 chore(deps): update dependency glob to v11.0.1 (#1155)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-27 00:10:45 -05:00
Deborah Kaplan
12269c2c8e Merge pull request #1154 from salman2013/salman/update-catalog-info-file
Add catalog-info.yaml file for release data
2025-01-14 11:06:08 -05:00
salman2013
630dbefb7e chore: Update catalog-info file and remove openedx.yaml 2025-01-14 15:04:47 +05:00
renovate[bot]
00c8697c59 chore(deps): update dependency @commitlint/config-angular to v19.7.0 (#1153)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-06 02:23:40 -05:00
renovate[bot]
8df3d06598 fix(deps): update dependency @edx/frontend-platform to v8.1.4 (#1152)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-06 07:23:31 +00:00
renovate[bot]
f6ed6ee1f5 fix(deps): update dependency @openedx/paragon to v22.13.0 (#1151)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-30 06:41:46 +00:00
renovate[bot]
7e8d22ec6a chore(deps): update dependency @edx/browserslist-config to v1.4.0 (#1150)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-30 01:41:34 -05:00
renovate[bot]
d0595e679d fix(deps): update react-router monorepo to v6.28.1 (#1149)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 03:37:53 -05:00
renovate[bot]
4c7e713b6e fix(deps): update dependency @edx/frontend-platform to v8.1.3 (#1148)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-23 08:37:41 +00:00
renovate[bot]
bc0683281f fix(deps): update dependency @edx/frontend-component-header to v5.8.2 (#1147)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 06:47:58 +00:00
renovate[bot]
b954345b38 chore(deps): update dependency @commitlint/cli to v19.6.1 (#1146)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 06:47:46 +00:00
renovate[bot]
ca4f78bd1c chore(deps): update dependency @edx/browserslist-config to v1.3.0 (#1143)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 06:42:17 +00:00
renovate[bot]
3dc8b156fe chore(deps): update dependency @openedx/frontend-build to v14.2.2 (#1142)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 01:41:56 -05:00
renovate[bot]
a818cd4dc4 fix(deps): update dependency @edx/frontend-component-header to v5.8.1 (#1141)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-02 08:30:04 +00:00
renovate[bot]
2e9dcd165e fix(deps): update dependency @openedx/frontend-slot-footer to v1.0.7 (#1140)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-02 03:29:49 -05:00
sundasnoreen12
53a52b8f06 Merge pull request #1139 from openedx/sundasMasterBranch-INF-1594
feat: fixed loading issue for wrong username
2024-11-26 19:59:05 +05:00
sundasnoreen12
c66facee92 feat: fixed loading issue for wrong username 2024-11-26 16:32:50 +05:00
renovate[bot]
3270e27c94 chore(deps): update dependency @openedx/frontend-build to v14.2.0 (#1136)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-25 03:33:45 -05:00
renovate[bot]
01cd125d4f chore(deps): update commitlint monorepo to v19.6.0 (#1135)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-25 08:33:31 +00:00
renovate[bot]
5fcef4edf4 fix(deps): update dependency @openedx/paragon to v22.10.0 (#1128)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 06:25:48 +00:00
renovate[bot]
d33c79a525 fix(deps): update dependency @edx/frontend-component-header to v5.7.2 (#1127)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-18 01:25:22 -05:00
renovate[bot]
d36c61d44e fix(deps): update react-router monorepo to v6.28.0 (#1126)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 07:10:19 +00:00
renovate[bot]
aed6081d37 fix(deps): update dependency core-js to v3.39.0 (#1125)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 07:09:23 +00:00
renovate[bot]
66a4eef910 fix(deps): update dependency @openedx/frontend-slot-footer to v1.0.6 (#1124)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 06:43:51 +00:00
renovate[bot]
6da6fedc57 fix(deps): update dependency @edx/frontend-component-header to v5.7.1 (#1123)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-11-11 06:43:30 +00:00
edX requirements bot
a7e095f3bf chore: update browserslist DB (#1091)
Co-authored-by: jsnwesson <62807795+jsnwesson@users.noreply.github.com>
2024-11-11 07:16:51 +05:00
Muhammad Adeel Tajamul
9a393d8e43 Merge pull request #1105 from openedx/renovate/glob-11.x
chore(deps): update dependency glob to v11
2024-11-11 07:16:06 +05:00
renovate[bot]
1a5c4f2404 chore(deps): update dependency glob to v11 2024-11-07 05:13:12 +00:00
Muhammad Adeel Tajamul
13aaca03fd Merge pull request #1118 from openedx/renovate/reselect-5.x
fix(deps): update dependency reselect to v5
2024-11-07 10:12:13 +05:00
renovate[bot]
422aff1915 fix(deps): update dependency reselect to v5 2024-11-04 09:21:34 +00:00
renovate[bot]
cac4e42364 fix(deps): update dependency @edx/frontend-component-header to v5.7.0 2024-11-04 06:01:48 +00:00
renovate[bot]
c6d39884c8 chore(deps): update dependency @testing-library/jest-dom to v6.6.3 2024-11-04 06:01:26 +00:00
Bilal Qamar
4bb3678d4c test: Remove support for Node 18 (#1075) 2024-10-31 14:46:22 -04:00
renovate[bot]
5c2951de40 chore(deps): update dependency redux-mock-store to v1.5.5 2024-10-28 04:53:32 +00:00
19 changed files with 4905 additions and 2767 deletions

View File

@@ -13,12 +13,11 @@ jobs:
- i18n_extract
- lint
- test
node: [18, 20]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
node-version-file: '.nvmrc'
- run: make requirements
- run: make test NPM_TESTS=build
- run: make test NPM_TESTS=${{ matrix.npm-test }}

View File

@@ -17,6 +17,7 @@ metadata:
openedx.org/arch-interest-groups: ""
# This can be multiple comma-separated projects.
openedx.org/add-to-projects: "openedx:23"
openedx.org/release: "master"
spec:
owner: group:2u-infinity
type: 'service'

View File

@@ -1,6 +0,0 @@
# This file describes this Open edX repo, as described in OEP-2:
# https://open-edx-proposals.readthedocs.io/en/latest/oep-0002-bp-repo-metadata.html#specification
nick: prof
oeps: {}
openedx-release: {ref: master}

6525
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"dev": "PUBLIC_PATH=/profile/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
"test": "TZ=UTC fedx-scripts jest --coverage --passWithNoTests",
"stubs": "pact-stub-service ./src/pacts/frontend-app-profile-edx-platform.json --port 18000"
},
@@ -29,50 +30,51 @@
],
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-header": "^5.6.0",
"@edx/frontend-platform": "8.1.2",
"@edx/openedx-atlas": "^0.6.0",
"@fortawesome/fontawesome-svg-core": "6.6.0",
"@fortawesome/free-brands-svg-icons": "6.6.0",
"@fortawesome/free-regular-svg-icons": "6.6.0",
"@fortawesome/free-solid-svg-icons": "6.6.0",
"@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-header": "^6.2.0",
"@edx/frontend-platform": "^8.3.1",
"@edx/openedx-atlas": "^0.7.0",
"@fortawesome/fontawesome-svg-core": "6.7.2",
"@fortawesome/free-brands-svg-icons": "6.7.2",
"@fortawesome/free-regular-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "0.2.2",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "^22.2.2",
"@openedx/frontend-plugin-framework": "^1.7.0",
"@openedx/paragon": "^22.17.0",
"@pact-foundation/pact": "^11.0.2",
"@redux-devtools/extension": "3.3.0",
"classnames": "2.5.1",
"core-js": "3.38.1",
"core-js": "3.41.0",
"history": "5.3.0",
"lodash.camelcase": "4.3.0",
"lodash.get": "4.4.2",
"lodash.pick": "4.4.0",
"lodash.snakecase": "4.1.1",
"prop-types": "15.8.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-helmet": "6.1.0",
"react-redux": "7.2.9",
"react-router": "6.27.0",
"react-router-dom": "6.27.0",
"react-router": "6.30.0",
"react-router-dom": "6.30.0",
"redux": "4.2.1",
"redux-logger": "3.0.6",
"redux-saga": "1.3.0",
"redux-thunk": "2.4.2",
"regenerator-runtime": "0.14.1",
"reselect": "4.1.8",
"reselect": "5.1.1",
"universal-cookie": "4.0.4"
},
"devDependencies": {
"@commitlint/cli": "19.5.0",
"@commitlint/config-angular": "19.5.0",
"@commitlint/cli": "19.8.0",
"@commitlint/config-angular": "19.8.0",
"@edx/browserslist-config": "^1.1.1",
"@edx/reactifex": "2.2.0",
"@openedx/frontend-build": "14.1.5",
"@testing-library/jest-dom": "6.6.2",
"@testing-library/react": "12.1.5",
"glob": "10.4.5",
"@openedx/frontend-build": "^14.3.3",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "14.3.1",
"glob": "11.0.1",
"reactifex": "1.1.1",
"redux-mock-store": "1.5.4"
"redux-mock-store": "1.5.5"
}
}

View File

@@ -13,11 +13,12 @@ import {
ErrorPage,
} from '@edx/frontend-platform/react';
import React from 'react';
import ReactDOM from 'react-dom';
import React, { StrictMode } from 'react';
// eslint-disable-next-line import/no-unresolved
import { createRoot } from 'react-dom/client';
import Header from '@edx/frontend-component-header';
import FooterSlot from '@openedx/frontend-slot-footer';
import { FooterSlot } from '@edx/frontend-component-footer';
import messages from './i18n';
import configureStore from './data/configureStore';
@@ -27,22 +28,24 @@ import Head from './head/Head';
import AppRoutes from './routes/AppRoutes';
const rootNode = createRoot(document.getElementById('root'));
subscribe(APP_READY, () => {
ReactDOM.render(
<AppProvider store={configureStore()}>
<Head />
<Header />
<main id="main">
<AppRoutes />
</main>
<FooterSlot />
</AppProvider>,
document.getElementById('root'),
rootNode.render(
<StrictMode>
<AppProvider store={configureStore()}>
<Head />
<Header />
<main id="main">
<AppRoutes />
</main>
<FooterSlot />
</AppProvider>
</StrictMode>,
);
});
subscribe(APP_INIT_ERROR, (error) => {
ReactDOM.render(<ErrorPage message={error.message} />, document.getElementById('root'));
rootNode.render(<ErrorPage message={error.message} />);
});
initialize({

View File

@@ -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.
![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

@@ -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 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.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -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;

View File

@@ -1,12 +1,15 @@
# Footer Slot
### Slot ID: `footer_slot`
### Slot ID: `org.openedx.frontend.layout.footer.v1`
### Slot ID Aliases
* `footer_slot`
## Description
This slot is used to replace/modify/hide the footer.
The implementation of the `FooterSlot` component lives in [the `frontend-slot-footer` repository](https://github.com/openedx/frontend-slot-footer/).
The implementation of the `FooterSlot` component lives in [the `frontend-component-footer` repository](https://github.com/openedx/frontend-component-footer/).
## Example
@@ -23,7 +26,7 @@ import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-frame
const config = {
pluginSlots: {
footer_slot: {
'org.openedx.frontend.layout.footer.v1': {
plugins: [
{
// Hide the default footer

View File

@@ -1,3 +1,3 @@
# `frontend-app-profile` Plugin Slots
* [`footer_slot`](./FooterSlot/)
* [`org.openedx.frontend.layout.footer.v1`](./FooterSlot/)

View File

@@ -43,6 +43,8 @@ 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 {
@@ -183,12 +185,19 @@ class ProfilePage extends React.Component {
visibilityBio,
requiresParentalConsent,
isLoadingProfile,
username,
saveState,
navigate,
} = this.props;
if (isLoadingProfile) {
return <PageLoading srMessage={this.props.intl.formatMessage(messages['profile.loading'])} />;
}
if (!username && saveState === 'error' && navigate) {
navigate('/notfound');
}
const commonFormProps = {
openHandler: this.handleOpen,
closeHandler: this.handleClose,
@@ -282,6 +291,9 @@ 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()}
@@ -330,6 +342,7 @@ ProfilePage.propTypes = {
// Account data
requiresParentalConsent: PropTypes.bool,
dateJoined: PropTypes.string,
username: PropTypes.string,
// Bio form data
bio: PropTypes.string,
@@ -395,6 +408,7 @@ ProfilePage.propTypes = {
openForm: PropTypes.func.isRequired,
closeForm: PropTypes.func.isRequired,
updateDraft: PropTypes.func.isRequired,
navigate: PropTypes.func.isRequired,
// Router
params: PropTypes.shape({
@@ -407,6 +421,7 @@ ProfilePage.propTypes = {
ProfilePage.defaultProps = {
saveState: null,
username: '',
savePhotoState: null,
photoUploadError: {},
profileImage: {},

View File

@@ -9,6 +9,7 @@ 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 messages from '../i18n';
import ProfilePage from './ProfilePage';
@@ -16,6 +17,7 @@ import ProfilePage from './ProfilePage';
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'),
@@ -65,6 +67,23 @@ beforeEach(() => {
analytics.sendTrackingLogEvent.mockReset();
});
const ProfileWrapper = ({ params, requiresParentalConsent }) => {
const navigate = useNavigate();
return (
<ProfilePage
{...requiredProfilePageProps}
params={params}
requiresParentalConsent={requiresParentalConsent}
navigate={navigate}
/>
);
};
ProfileWrapper.propTypes = {
params: PropTypes.shape({}).isRequired,
requiresParentalConsent: PropTypes.bool.isRequired,
};
const ProfilePageWrapper = ({
contextValue, store, params, requiresParentalConsent,
}) => (
@@ -73,7 +92,12 @@ const ProfilePageWrapper = ({
>
<IntlProvider locale="en">
<Provider store={store}>
<ProfilePage {...requiredProfilePageProps} params={params} requiresParentalConsent={requiresParentalConsent} />
<BrowserRouter>
<ProfileWrapper
params={params}
requiresParentalConsent={requiresParentalConsent}
/>
</BrowserRouter>
</Provider>
</IntlProvider>
</AppContext.Provider>
@@ -103,6 +127,16 @@ describe('<ProfilePage />', () => {
expect(tree).toMatchSnapshot();
});
it('successfully redirected to not found page.', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = <ProfilePageWrapper contextValue={contextValue} store={mockStore(storeMocks.invalidUser)} />;
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
it('viewing own profile', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },

View File

@@ -0,0 +1,41 @@
module.exports = {
userAccount: {
loading: false,
error: null,
username: 'staff',
email: null,
bio: null,
name: null,
country: null,
socialLinks: null,
profileImage: {
imageUrlMedium: null,
imageUrlLarge: null
},
levelOfEducation: null,
learningGoal: null
},
profilePage: {
errors: {},
saveState: 'error',
savePhotoState: null,
currentlyEditingField: null,
account: {
username: '',
socialLinks: []
},
preferences: {},
courseCertificates: [],
drafts: {},
isLoadingProfile: false,
isAuthenticatedUserProfile: true,
},
router: {
location: {
pathname: '/u/staffTest',
search: '',
hash: ''
},
action: 'POP'
}
};

View File

@@ -29,6 +29,644 @@ exports[`<ProfilePage /> Renders correctly in various states app loading 1`] = `
</div>
`;
exports[`<ProfilePage /> Renders correctly in various states successfully redirected to not found page. 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block p-relative"
/>
<div
class="container-fluid"
>
<div
class="row align-items-center pt-4 mb-4 pt-md-0 mb-md-0"
>
<div
class="col-auto col-md-4 col-lg-3"
>
<div
class="d-flex align-items-center d-md-block"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
class="profile-avatar-menu-container"
>
<button
class="text-white btn-block btn btn-link btn-sm"
type="button"
>
Upload Photo
</button>
</div>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
</div>
</div>
<div
class="col"
>
<div
class="d-md-none"
>
<span
data-hj-suppress="true"
>
<h1
class="h2 mb-0 font-weight-bold text-truncate"
>
staff
</h1>
<div
class="d-flex align-items-center mt-3 mb-2rem"
>
<span
class="pgn__icon icon-visibility-off"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 6.5c2.76 0 5 2.24 5 5 0 .51-.1 1-.24 1.46l3.06 3.06c1.39-1.23 2.49-2.77 3.18-4.53C21.27 7.11 17 4 12 4c-1.27 0-2.49.2-3.64.57l2.17 2.17c.47-.14.96-.24 1.47-.24ZM3.42 2.45 2.01 3.87l2.68 2.68A11.738 11.738 0 0 0 1 11.5C2.73 15.89 7 19 12 19c1.52 0 2.97-.3 4.31-.82l3.43 3.43 1.41-1.41L3.42 2.45ZM12 16.5c-2.76 0-5-2.24-5-5 0-.77.18-1.5.49-2.14l1.57 1.57c-.03.18-.06.37-.06.57 0 1.66 1.34 3 3 3 .2 0 .38-.03.57-.07L14.14 16c-.65.32-1.37.5-2.14.5Zm2.97-5.33a2.97 2.97 0 0 0-2.64-2.64l2.64 2.64Z"
fill="currentColor"
/>
</svg>
</span>
<div
class="username-description"
>
Your profile information is only visible to you. Only your username is visible to others on localhost.
</div>
</div>
<hr
class="d-none d-md-block"
/>
</span>
</div>
<div
class="d-none d-md-block float-right"
>
<a
class="pgn__hyperlink default-link standalone-link btn btn-primary"
href="http://localhost:18150/records"
rel="noopener noreferrer"
target="_blank"
>
View My Records
<span
class="pgn__hyperlink__external-icon"
title="Opens in a new tab"
>
<span
class="pgn__icon"
data-testid="hyperlink-icon"
style="height: 1em; width: 1em;"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
<span
class="sr-only"
>
in a new tab
</span>
</span>
</span>
</a>
</div>
</div>
</div>
<div
class="row"
>
<div
class="col-md-4 col-lg-4"
>
<div
class="d-none d-md-block mb-4"
>
<span
data-hj-suppress="true"
>
<h1
class="h2 mb-0 font-weight-bold text-truncate"
>
staff
</h1>
<div
class="d-flex align-items-center mt-3 mb-2rem"
>
<span
class="pgn__icon icon-visibility-off"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 6.5c2.76 0 5 2.24 5 5 0 .51-.1 1-.24 1.46l3.06 3.06c1.39-1.23 2.49-2.77 3.18-4.53C21.27 7.11 17 4 12 4c-1.27 0-2.49.2-3.64.57l2.17 2.17c.47-.14.96-.24 1.47-.24ZM3.42 2.45 2.01 3.87l2.68 2.68A11.738 11.738 0 0 0 1 11.5C2.73 15.89 7 19 12 19c1.52 0 2.97-.3 4.31-.82l3.43 3.43 1.41-1.41L3.42 2.45ZM12 16.5c-2.76 0-5-2.24-5-5 0-.77.18-1.5.49-2.14l1.57 1.57c-.03.18-.06.37-.06.57 0 1.66 1.34 3 3 3 .2 0 .38-.03.57-.07L14.14 16c-.65.32-1.37.5-2.14.5Zm2.97-5.33a2.97 2.97 0 0 0-2.64-2.64l2.64 2.64Z"
fill="currentColor"
/>
</svg>
</span>
<div
class="username-description"
>
Your profile information is only visible to you. Only your username is visible to others on localhost.
</div>
</div>
<hr
class="d-none d-md-block"
/>
</span>
</div>
<div
class="d-md-none mb-4"
>
<a
class="pgn__hyperlink default-link standalone-link btn btn-primary"
href="http://localhost:18150/records"
rel="noopener noreferrer"
target="_blank"
>
View My Records
<span
class="pgn__hyperlink__external-icon"
title="Opens in a new tab"
>
<span
class="pgn__icon"
data-testid="hyperlink-icon"
style="height: 1em; width: 1em;"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
<span
class="sr-only"
>
in a new tab
</span>
</span>
</span>
</a>
</div>
<div
class="pgn-transition-replace-group position-relative mb-5"
>
<div
style="padding: .1px 0px;"
>
<div
class="editable-item-header mb-2"
>
<h2
class="edit-section-header"
>
Full Name
</h2>
</div>
<div>
<button
class="pl-0 text-left btn btn-link"
tabindex="0"
type="button"
>
<svg
aria-hidden="true"
class="svg-inline--fa fa-plus fa-xs mr-2"
data-icon="plus"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z"
fill="currentColor"
/>
</svg>
Add name
</button>
</div>
<small
class="form-text text-muted"
>
This is the name that appears in your account and on your certificates.
</small>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative mb-5"
>
<div
style="padding: .1px 0px;"
>
<div
class="editable-item-header mb-2"
>
<h2
class="edit-section-header"
>
Location
</h2>
</div>
<div>
<button
class="pl-0 text-left btn btn-link"
tabindex="0"
type="button"
>
<svg
aria-hidden="true"
class="svg-inline--fa fa-plus fa-xs mr-2"
data-icon="plus"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z"
fill="currentColor"
/>
</svg>
Add location
</button>
</div>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative mb-5"
>
<div
style="padding: .1px 0px;"
>
<div
class="editable-item-header mb-2"
>
<h2
class="edit-section-header"
>
Primary Language Spoken
</h2>
</div>
<div>
<button
class="pl-0 text-left btn btn-link"
tabindex="0"
type="button"
>
<svg
aria-hidden="true"
class="svg-inline--fa fa-plus fa-xs mr-2"
data-icon="plus"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z"
fill="currentColor"
/>
</svg>
Add language
</button>
</div>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative mb-5"
>
<div
style="padding: .1px 0px;"
>
<div
class="editable-item-header mb-2"
>
<h2
class="edit-section-header"
>
Education
</h2>
</div>
<div>
<button
class="pl-0 text-left btn btn-link"
tabindex="0"
type="button"
>
<svg
aria-hidden="true"
class="svg-inline--fa fa-plus fa-xs mr-2"
data-icon="plus"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z"
fill="currentColor"
/>
</svg>
Add education
</button>
</div>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative mb-5"
>
<div
style="padding: .1px 0px;"
>
<div
class="editable-item-header mb-2"
>
<h2
class="edit-section-header"
>
Social Links
</h2>
</div>
<ul
class="list-unstyled"
>
<li
class="mb-4"
>
<div>
<button
class="pl-0 text-left btn btn-link"
tabindex="0"
type="button"
>
<svg
aria-hidden="true"
class="svg-inline--fa fa-plus fa-xs mr-2"
data-icon="plus"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z"
fill="currentColor"
/>
</svg>
Add Twitter
</button>
</div>
</li>
<li
class="mb-4"
>
<div>
<button
class="pl-0 text-left btn btn-link"
tabindex="0"
type="button"
>
<svg
aria-hidden="true"
class="svg-inline--fa fa-plus fa-xs mr-2"
data-icon="plus"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z"
fill="currentColor"
/>
</svg>
Add Facebook
</button>
</div>
</li>
<li
class="mb-4"
>
<div>
<button
class="pl-0 text-left btn btn-link"
tabindex="0"
type="button"
>
<svg
aria-hidden="true"
class="svg-inline--fa fa-plus fa-xs mr-2"
data-icon="plus"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z"
fill="currentColor"
/>
</svg>
Add LinkedIn
</button>
</div>
</li>
</ul>
</div>
</div>
<div
class="mb-4"
/>
</div>
<div
class="pt-md-3 col-md-8 col-lg-7 offset-lg-1"
>
<div
class="pgn-transition-replace-group position-relative mb-5"
>
<div
style="padding: .1px 0px;"
>
<div
class="editable-item-header mb-2"
>
<h2
class="edit-section-header"
>
About Me
</h2>
</div>
<div>
<button
class="pl-0 text-left btn btn-link"
tabindex="0"
type="button"
>
<svg
aria-hidden="true"
class="svg-inline--fa fa-plus fa-xs mr-2"
data-icon="plus"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z"
fill="currentColor"
/>
</svg>
Add a short bio
</button>
</div>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative mb-4"
>
<div
style="padding: .1px 0px;"
>
<div
class="editable-item-header mb-2"
>
<h2
class="edit-section-header"
>
My Certificates
<button
class="float-right px-0 btn btn-link btn-sm"
style="margin-top: -.35rem;"
type="button"
>
<svg
aria-hidden="true"
class="svg-inline--fa fa-pencil mr-1"
data-icon="pencil"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 512 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M410.3 231l11.3-11.3-33.9-33.9-62.1-62.1L291.7 89.8l-11.3 11.3-22.6 22.6L58.6 322.9c-10.4 10.4-18 23.3-22.2 37.4L1 480.7c-2.5 8.4-.2 17.5 6.1 23.7s15.3 8.5 23.7 6.1l120.3-35.4c14.1-4.2 27-11.8 37.4-22.2L387.7 253.7 410.3 231zM160 399.4l-9.1 22.7c-4 3.1-8.5 5.4-13.3 6.9L59.4 452l23-78.1c1.4-4.9 3.8-9.4 6.9-13.3l22.7-9.1 0 32c0 8.8 7.2 16 16 16l32 0zM362.7 18.7L348.3 33.2 325.7 55.8 314.3 67.1l33.9 33.9 62.1 62.1 33.9 33.9 11.3-11.3 22.6-22.6 14.5-14.5c25-25 25-65.5 0-90.5L453.3 18.7c-25-25-65.5-25-90.5 0zm-47.4 168l-144 144c-6.2 6.2-16.4 6.2-22.6 0s-6.2-16.4 0-22.6l144-144c6.2-6.2 16.4-6.2 22.6 0s6.2 16.4 0 22.6z"
fill="currentColor"
/>
</svg>
Edit
</button>
</h2>
<p
class="mb-0"
>
<span
class="ml-auto small text-muted"
>
<svg
aria-hidden="true"
class="svg-inline--fa fa-eye "
data-icon="eye"
data-prefix="far"
focusable="false"
role="img"
viewBox="0 0 576 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M288 80c-65.2 0-118.8 29.6-159.9 67.7C89.6 183.5 63 226 49.4 256c13.6 30 40.2 72.5 78.6 108.3C169.2 402.4 222.8 432 288 432s118.8-29.6 159.9-67.7C486.4 328.5 513 286 526.6 256c-13.6-30-40.2-72.5-78.6-108.3C406.8 109.6 353.2 80 288 80zM95.4 112.6C142.5 68.8 207.2 32 288 32s145.5 36.8 192.6 80.6c46.8 43.5 78.1 95.4 93 131.1c3.3 7.9 3.3 16.7 0 24.6c-14.9 35.7-46.2 87.7-93 131.1C433.5 443.2 368.8 480 288 480s-145.5-36.8-192.6-80.6C48.6 356 17.3 304 2.5 268.3c-3.3-7.9-3.3-16.7 0-24.6C17.3 208 48.6 156 95.4 112.6zM288 336c44.2 0 80-35.8 80-80s-35.8-80-80-80c-.7 0-1.3 0-2 0c1.3 5.1 2 10.5 2 16c0 35.3-28.7 64-64 64c-5.5 0-10.9-.7-16-2c0 .7 0 1.3 0 2c0 44.2 35.8 80 80 80zm0-208a128 128 0 1 1 0 256 128 128 0 1 1 0-256z"
fill="currentColor"
/>
</svg>
Everyone on localhost
</span>
</p>
</div>
You don't have any certificates yet.
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`<ProfilePage /> Renders correctly in various states test country edit with error 1`] = `
<div>
<div
@@ -1995,6 +2633,9 @@ 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"
@@ -2994,6 +3635,9 @@ 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"
@@ -4868,6 +5512,9 @@ 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"
@@ -5133,9 +5780,10 @@ exports[`<ProfilePage /> Renders correctly in various states viewing other profi
<div
class="profile-avatar rounded-circle bg-light"
>
<iconmock
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
@@ -5460,6 +6108,9 @@ 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"
@@ -6150,6 +6801,9 @@ 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"
@@ -7027,6 +7681,9 @@ 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"
@@ -7968,6 +8625,9 @@ 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"
@@ -8837,6 +9497,9 @@ 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

@@ -63,12 +63,14 @@ const profilePage = (state = initialState, action = {}) => {
return {
...state,
saveState: 'error',
isLoadingProfile: false,
errors: { ...state.errors, ...action.payload.errors },
};
case SAVE_PROFILE.RESET:
return {
...state,
saveState: null,
isLoadingProfile: false,
errors: {},
};

View File

@@ -1,4 +1,3 @@
import { history } from '@edx/frontend-platform';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import pick from 'lodash.pick';
import {
@@ -95,7 +94,11 @@ export function* handleFetchProfile(action) {
yield put(fetchProfileReset());
} catch (e) {
if (e.response.status === 404) {
history.push('/notfound');
if (e.processedData && e.processedData.fieldErrors) {
yield put(saveProfileFailure(e.processedData.fieldErrors));
} else {
yield put(saveProfileFailure(e.customAttributes));
}
} else {
throw e;
}

View File

@@ -3,15 +3,19 @@ import {
AuthenticatedPageRoute,
PageWrap,
} from '@edx/frontend-platform/react';
import { Routes, Route } from 'react-router-dom';
import { Routes, Route, useNavigate } from 'react-router-dom';
import { ProfilePage, NotFoundPage } from '../profile';
const AppRoutes = () => (
<Routes>
<Route path="/u/:username" element={<AuthenticatedPageRoute><ProfilePage /></AuthenticatedPageRoute>} />
<Route path="/notfound" element={<PageWrap><NotFoundPage /></PageWrap>} />
<Route path="*" element={<PageWrap><NotFoundPage /></PageWrap>} />
</Routes>
);
const AppRoutes = () => {
const navigate = useNavigate();
return (
<Routes>
<Route path="/u/:username" element={<AuthenticatedPageRoute><ProfilePage navigate={navigate} /></AuthenticatedPageRoute>} />
<Route path="/notfound" element={<PageWrap><NotFoundPage /></PageWrap>} />
<Route path="*" element={<PageWrap><NotFoundPage /></PageWrap>} />
</Routes>
);
};
export default AppRoutes;