diff --git a/package-lock.json b/package-lock.json index d11a194bf..80efb804f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@fortawesome/free-regular-svg-icons": "6.7.2", "@fortawesome/free-solid-svg-icons": "6.7.2", "@fortawesome/react-fontawesome": "0.2.3", + "@openedx/frontend-plugin-framework": "^1.7.0", "@openedx/paragon": "^23.4.5", "@pact-foundation/pact": "^11.0.2", "@redux-devtools/extension": "3.3.0", @@ -57,6 +58,31 @@ "redux-mock-store": "1.5.5" } }, + "frontend-component-extended-fields": { + "name": "@edunext/frontend-component-extended-fields", + "version": "1.0.0", + "extraneous": true, + "license": "AGPL-3.0", + "dependencies": { + "@openedx/frontend-plugin-framework": "^1.5.0" + }, + "devDependencies": { + "@edx/browserslist-config": "^1.1.1", + "@edx/frontend-component-footer": "^14.2.0", + "@openedx/frontend-build": "^14.3.1", + "core-js": "3.42.0", + "glob": "7.2.3", + "husky": "7.0.4", + "jest": "29.7.0", + "prop-types": "^15.8.1", + "react-dom": "^18.3.1" + }, + "peerDependencies": { + "@edx/frontend-component-footer": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@adobe/css-tools": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.3.tgz", diff --git a/package.json b/package.json index 4f7999f95..535459358 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@fortawesome/free-regular-svg-icons": "6.7.2", "@fortawesome/free-solid-svg-icons": "6.7.2", "@fortawesome/react-fontawesome": "0.2.3", + "@openedx/frontend-plugin-framework": "^1.7.0", "@openedx/paragon": "^23.4.5", "@pact-foundation/pact": "^11.0.2", "@redux-devtools/extension": "3.3.0", diff --git a/src/plugin-slots/AdditionalProfileFieldsSlot/README.md b/src/plugin-slots/AdditionalProfileFieldsSlot/README.md new file mode 100644 index 000000000..77a194fb7 --- /dev/null +++ b/src/plugin-slots/AdditionalProfileFieldsSlot/README.md @@ -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); +``` \ No newline at end of file diff --git a/src/plugin-slots/AdditionalProfileFieldsSlot/example/index.jsx b/src/plugin-slots/AdditionalProfileFieldsSlot/example/index.jsx new file mode 100644 index 000000000..177e125d2 --- /dev/null +++ b/src/plugin-slots/AdditionalProfileFieldsSlot/example/index.jsx @@ -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 ( +
+ Favorite Color +
++ Favorite Color +
+Click to add your favorite color
+