From b89017045ba60c4d6449669fdc62cdd6324bd098 Mon Sep 17 00:00:00 2001 From: OEX Workshop Date: Tue, 5 May 2026 12:10:00 +0000 Subject: [PATCH 1/2] feat: add additional_fields plugin slot to register form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new ``frontend-plugin-framework`` plugin slot, ``org.openedx.frontend.authn.register.additional_fields.v1``, in ``RegistrationPage``. The slot is positioned just above the submit button so plugins can append optional fields (e.g. demographics, institutional affiliation, role) without modifying the form's core required-fields layout. Why a slot here, and why "additional_fields"? * Several deployments already maintain forks of ``RegistrationPage`` to inject extra fields. Forking the authn MFE is a well-known pain point — the slot replaces a class of long-lived forks with a single documented extension point. * "additional_fields" deliberately scopes the slot to *learner-supplied optional input*, not to layout chrome (links, banners, marketing copy). Layout-level extension is better served by separate slots with simpler contracts. * The slot exposes ``formFields`` (current form values) and ``setFormField(name, value)`` so plugin components participate in the existing form-state machinery instead of maintaining shadow state and submitting via a side channel. This change is additive: with no plugin registered, the slot renders nothing and the form is byte-identical to today. Companion server-side changes: - openedx-events: ``REGISTRATION_DEMOGRAPHICS_CAPTURED`` (#NNNN) - edx-platform: fire the event from the authn registration view (#NNNN) Reference: OEX 2026 workshop "Leveraging Open edX Extension Points", Section 4. --- .../RegisterAdditionalFieldsSlot/README.md | 60 +++++++++++++++++++ .../RegisterAdditionalFieldsSlot/index.jsx | 16 +++++ src/register/RegistrationPage.jsx | 6 ++ 3 files changed, 82 insertions(+) create mode 100644 src/plugin-slots/RegisterAdditionalFieldsSlot/README.md create mode 100644 src/plugin-slots/RegisterAdditionalFieldsSlot/index.jsx diff --git a/src/plugin-slots/RegisterAdditionalFieldsSlot/README.md b/src/plugin-slots/RegisterAdditionalFieldsSlot/README.md new file mode 100644 index 000000000..6481c1ed9 --- /dev/null +++ b/src/plugin-slots/RegisterAdditionalFieldsSlot/README.md @@ -0,0 +1,60 @@ +# RegisterAdditionalFieldsSlot + +### Slot ID + +`org.openedx.frontend.authn.register.additional_fields.v1` + +### Plugin Props + +| Name | Type | Description | +| -------------- | ------------------------------- | ----------------------------------------------------------------- | +| `formFields` | `Record` | Current values of every field in the registration form. | +| `setFormField` | `(event: ChangeEvent) => void` | Form change handler — call with a synthetic event whose `target` | +| | | has `name` and `value` to update form state. | + +### Description + +Renders just above the **Create Account** submit button. Use this slot +to append optional fields whose values should travel with the standard +registration POST. The plugin component is responsible for its own +layout but should match Paragon form spacing for visual consistency. + +Fields written via `setFormField` are submitted as part of `request.POST` +to the LMS registration view alongside the built-in fields. + +### Example usage + +```jsx +import { Form } from '@openedx/paragon'; + +const DemographicsFields = ({ formFields, setFormField }) => ( + <> + + + + + + + + + + + + +); +``` + +The companion backend plugin +(`openedx-registration-demographics-plugin`) validates these fields via +the `StudentRegistrationRequested` filter and persists them on receipt diff --git a/src/plugin-slots/RegisterAdditionalFieldsSlot/index.jsx b/src/plugin-slots/RegisterAdditionalFieldsSlot/index.jsx new file mode 100644 index 000000000..18bbb8db8 --- /dev/null +++ b/src/plugin-slots/RegisterAdditionalFieldsSlot/index.jsx @@ -0,0 +1,16 @@ +import PropTypes from 'prop-types'; +import { PluginSlot } from '@openedx/frontend-plugin-framework'; + +const RegisterAdditionalFieldsSlot = ({ formFields, setFormField }) => ( + +); + +RegisterAdditionalFieldsSlot.propTypes = { + // eslint-disable-next-line react/forbid-prop-types + formFields: PropTypes.object.isRequired, + setFormField: PropTypes.func.isRequired, +}; + diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx index 91c63de3d..fbd7e916d 100644 --- a/src/register/RegistrationPage.jsx +++ b/src/register/RegistrationPage.jsx @@ -37,6 +37,7 @@ import { import { getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, setCookie, } from '../data/utils'; +import RegisterAdditionalFieldsSlot from '../plugin-slots/RegisterAdditionalFieldsSlot'; import { useRegisterContext } from './components/RegisterContext'; /** * Inner Registration Page component that uses the context @@ -391,6 +392,11 @@ const RegistrationPage = (props) => { autoSubmitRegisterForm={autoSubmitRegForm} fieldDescriptions={fieldDescriptions} /> + + Date: Mon, 11 May 2026 10:55:16 -0400 Subject: [PATCH 2/2] temp: Branch for conference demo, demonstrates adding a new plugin slot and testing it --- env.config.jsx | 44 +++++++++++++++++++ package-lock.json | 13 ++++++ package.json | 1 + .../RegisterAdditionalFieldsSlot/index.jsx | 18 +++++--- webpack.prod.config.js | 6 ++- 5 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 env.config.jsx diff --git a/env.config.jsx b/env.config.jsx new file mode 100644 index 000000000..18298c974 --- /dev/null +++ b/env.config.jsx @@ -0,0 +1,44 @@ +import { DemographicsFields } from "@openedx/openedx-demographics-plugin"; +import { + DIRECT_PLUGIN, + PLUGIN_OPERATIONS, +} from "@openedx/frontend-plugin-framework"; + +function addPlugins(config, slot_name, plugins) { + if (slot_name in config.pluginSlots === false) { + config.pluginSlots[slot_name] = { + keepDefault: true, + plugins: [], + }; + } + + config.pluginSlots[slot_name].plugins.push(...plugins); +} + +async function setConfig() { + let config = { + pluginSlots: {}, + }; + + try { + addPlugins(config, "org.openedx.frontend.authn.register.additional_fields.v1", [ + { + op: PLUGIN_OPERATIONS.Insert, + widget: { + id: "demographics_fields", + type: DIRECT_PLUGIN, + priority: 50, + RenderWidget: DemographicsFields, + }, + }, + ]); + } catch (err) { + console.error("env.config.jsx failed to apply: ", err); + } + + // eslint-disable-next-line no-console + console.log("[env.config] setConfig returning:", JSON.stringify(config, null, 2)); + return config; +} + +export default setConfig; diff --git a/package-lock.json b/package-lock.json index f043aa70f..94dbe587a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@fortawesome/free-solid-svg-icons": "6.7.2", "@fortawesome/react-fontawesome": "0.2.6", "@openedx/frontend-plugin-framework": "^1.7.0", + "@openedx/openedx-demographics-plugin": "file:../oex26-demographics-plugin/frontend/openedx-demographics-plugin-0.1.0.tgz", "@openedx/paragon": "^23.4.2", "@optimizely/react-sdk": "^2.9.1", "@tanstack/react-query": "^5.90.19", @@ -6987,6 +6988,18 @@ } } }, + "node_modules/@openedx/openedx-demographics-plugin": { + "name": "openedx-demographics-plugin", + "version": "0.1.0", + "resolved": "file:../oex26-demographics-plugin/frontend/openedx-demographics-plugin-0.1.0.tgz", + "integrity": "sha512-tawviYmOeAzHVMAF6Xv98lt762uhSZic5632SpoiBlNsTVOqOqX4LoSgJdSZjw8qaE/U3NrIQf7wt7fieVmSkA==", + "license": "Apache-2.0", + "peerDependencies": { + "@openedx/paragon": ">=22", + "react": ">=17", + "react-dom": ">=17" + } + }, "node_modules/@openedx/paragon": { "version": "23.19.2", "resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-23.19.2.tgz", diff --git a/package.json b/package.json index 85c83f10c..5bb7619c5 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@fortawesome/free-solid-svg-icons": "6.7.2", "@fortawesome/react-fontawesome": "0.2.6", "@openedx/frontend-plugin-framework": "^1.7.0", + "@openedx/openedx-demographics-plugin": "file:../oex26-demographics-plugin/frontend/openedx-demographics-plugin-0.1.0.tgz", "@openedx/paragon": "^23.4.2", "@optimizely/react-sdk": "^2.9.1", "@tanstack/react-query": "^5.90.19", diff --git a/src/plugin-slots/RegisterAdditionalFieldsSlot/index.jsx b/src/plugin-slots/RegisterAdditionalFieldsSlot/index.jsx index 18bbb8db8..e0c41e762 100644 --- a/src/plugin-slots/RegisterAdditionalFieldsSlot/index.jsx +++ b/src/plugin-slots/RegisterAdditionalFieldsSlot/index.jsx @@ -1,12 +1,16 @@ import PropTypes from 'prop-types'; import { PluginSlot } from '@openedx/frontend-plugin-framework'; -const RegisterAdditionalFieldsSlot = ({ formFields, setFormField }) => ( - -); +const RegisterAdditionalFieldsSlot = ({ formFields, setFormField }) => { + // eslint-disable-next-line no-console + console.log('[RegisterAdditionalFieldsSlot] rendering, formFields keys:', Object.keys(formFields)); + return ( + + ); +} RegisterAdditionalFieldsSlot.propTypes = { // eslint-disable-next-line react/forbid-prop-types @@ -14,3 +18,5 @@ RegisterAdditionalFieldsSlot.propTypes = { setFormField: PropTypes.func.isRequired, }; +export default RegisterAdditionalFieldsSlot; + diff --git a/webpack.prod.config.js b/webpack.prod.config.js index 73d22693e..9bca2b1c0 100644 --- a/webpack.prod.config.js +++ b/webpack.prod.config.js @@ -1,7 +1,9 @@ -const { createConfig } = require('@openedx/frontend-build'); +const { createConfig } = require("@openedx/frontend-build"); -const config = createConfig('webpack-prod'); +const config = createConfig("webpack-prod"); config.module.rules[0].exclude = /node_modules\/(?!(fastest-levenshtein|@edx))/; +config.module.rules[0].exclude = + /node_modules\/(?!(fastest-levenshtein|@(open)?edx))/; module.exports = config;