Skip to content

Commit

Permalink
feat: support merging base component props, custom props, and `plugin…
Browse files Browse the repository at this point in the history
…Props` via `slotOptions.mergeProps` (#84)

* feat: support merging props with PluginOperations.Modify

* fix: provide default content object for widget modification

* chore: pr feedback

* chore: consolidate examples

* chore: remove content prop from LinkExample

* chore: pr feedback, updating comment
  • Loading branch information
adamstankiewicz committed Sep 6, 2024
1 parent bf8705b commit f34f4df
Show file tree
Hide file tree
Showing 27 changed files with 786 additions and 142 deletions.
6 changes: 2 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -228,13 +228,11 @@ or its priority. The operation requires the id of the widget that will be modifi
.. code-block::
const modifyWidget = (widget) => {
const newContent = {
widget.content = {
propExampleA: 'University XYZ Sidebar',
propExampleB: SomeOtherIcon,
};
const modifiedWidget = widget;
modifiedWidget.content = newContent;
return modifiedWidget;
return widget;
};
/*
Expand Down
4 changes: 2 additions & 2 deletions example-plugin-app/src/DefaultIframe.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Plugin } from '@openedx/frontend-plugin-framework';
function DefaultComponent() {
return (
<section className="bg-light p-3 h-100">
<h3>Default iFrame Widget</h3>
<h4>Default iFrame Widget</h4>
<p>
This is a component that lives in the example-plugins-app and is provided in this host MFE via iFrame.
</p>
Expand All @@ -17,7 +17,7 @@ function DefaultComponent() {
function ErrorFallback(error) {
return (
<div className="text-center">
<p className="h3 text-muted">
<p className="h4 text-muted">
Oops! An error occurred. Please refresh the screen to try again.
</p>
<br />
Expand Down
2 changes: 1 addition & 1 deletion example-plugin-app/src/PluginIframe.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export default function PluginIframe() {
return (
<Plugin>
<section className="bg-light p-3 h-100">
<h3>Inserted iFrame Plugin</h3>
<h4>Inserted iFrame Plugin</h4>
<p>
This is a component that lives in the example-plugins-app and is provided in this host MFE via iFrame plugin.
</p>
Expand Down
59 changes: 48 additions & 11 deletions example/env.config.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,43 @@ import {
IFRAME_PLUGIN,
PLUGIN_OPERATIONS,
} from '@openedx/frontend-plugin-framework';
import DefaultDirectWidget from './src/components/DefaultDirectWidget';
import PluginDirect from './src/components/PluginDirect';
import ModularComponent from './src/components/ModularComponent';

const modifyWidget = (widget) => {
const newContent = {
widget.content = {
title: 'Modified Modular Plugin',
uniqueText: 'Note that the original text defined in the JS config is replaced by this modified one.',
};
const modifiedWidget = widget;
modifiedWidget.content = newContent;
return modifiedWidget;
return widget;
};

const wrapWidget = ({ component, idx }) => (
<div className="bg-warning" data-testid={`wrapper${idx + 1}`} key={idx}>
<p>This is a wrapper component that is placed around the default content.</p>
const modifyWidgetDefaultContentsUsernamePII = (widget) => {
widget.content = {
'data-custom-attr': 'customValue',
'data-another-custom-attr': '',
className: 'font-weight-bold',
style: { color: 'blue' },
onClick: (e) => { console.log('Username clicked!', 'custom', e); },
};
return widget;
};

const modifyWidgetDefaultContentsLink = (widget) => {
widget.content.href = 'https://openedx.org';
return widget;
};

const wrapWidget = ({ component }) => (
<div className="bg-warning" data-testid="wrapper">
<div className="px-3">
<p className="mb-0">This is a wrapper component that is placed around the default content.</p>
</div>
{component}
<p>With this wrapper, you can add anything before or after a component.</p>
<p>Note in the JS config that an iFrame plugin was Inserted, but a Hide operation was also used to hide it!</p>
<div className="px-3">
<p>With this wrapper, you can add anything before or after a component.</p>
<p className="mb-0">Note in the JS config that an iFrame plugin was Inserted, but a Hide operation was also used to hide it!</p>
</div>
</div>
);

Expand Down Expand Up @@ -179,7 +196,27 @@ const config = {
},
},
],
}
},
slot_with_username_pii: {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Modify,
widgetId: 'default_contents',
fn: modifyWidgetDefaultContentsUsernamePII,
},
],
},
slot_with_hyperlink: {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Modify,
widgetId: 'default_contents',
fn: modifyWidgetDefaultContentsLink,
},
],
},
},
};

Expand Down
6 changes: 6 additions & 0 deletions example/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"author": "edX",
"license": "AAGPL-3.0",
"dependencies": {
"classnames": "^2.5.1",
"core-js": "^3.29.1",
"prop-types": "^15.8.1",
"react": "^17.0.0",
Expand Down
83 changes: 63 additions & 20 deletions example/src/ExamplePage.jsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,74 @@
import React from 'react';
import {
Container, Row, Col, Stack,
} from '@openedx/paragon';

import PluginSlotWithModifyDefaultContents from './pluginSlots/PluginSlotWithModifyDefaultContents';
import PluginSlotWithInsert from './pluginSlots/PluginSlotWithInsert';
import PluginSlotWithModifyWrapHide from './pluginSlots/PluginSlotWithModifyWrapHide';
import PluginSlotWithModularPlugins from './pluginSlots/PluginSlotWithModularPlugins';
import PluginSlotWithoutDefault from './pluginSlots/PluginSlotWithoutDefault';

const pluginExamples = [
{
id: 'plugin-operation-insert',
label: 'Plugin Operation: Insert',
Component: PluginSlotWithInsert,
},
{
id: 'plugin-operation-modify-wrap-hide',
label: 'Plugin Operation: Modify, Wrap, and Hide',
Component: PluginSlotWithModifyWrapHide,
},
{
id: 'plugin-operation-modify-default-content',
label: 'Plugin Operation: Modify Default Content',
Component: PluginSlotWithModifyDefaultContents,
},
{
id: 'direct-plugins-modular-components',
label: 'Direct Plugins Using Modular Components',
Component: PluginSlotWithModularPlugins,
},
{
id: 'no-default-content',
label: 'Default Content Set to False',
Component: PluginSlotWithoutDefault,
},
];

export default function ExamplePage() {
return (
<main className="center m-5">
<h1>Plugins Page</h1>

<p>
This page is here to help test the plugins module. A plugin configuration can be added in
index.jsx and this page will display that plugin.
</p>
<p>
To do this, a plugin MFE must be running on some other port.
To make it a more realistic test, you may also want to edit your
/etc/hosts file (or your system&apos;s equivalent) to provide an alternate domain for
127.0.0.1 at which you can load the plugin.
</p>
<div className="d-flex flex-column">
<PluginSlotWithInsert />
<PluginSlotWithModifyWrapHide />
<PluginSlotWithModularPlugins />
<PluginSlotWithoutDefault />
</div>
<main>
<Container size="lg" className="py-3">
<Row>
<Col>
<h1>Plugins Page</h1>
<p>
This page is here to help test the plugins module. A plugin configuration can be added in
index.jsx and this page will display that plugin.
</p>
<p>
To do this, a plugin MFE must be running on some other port.
To make it a more realistic test, you may also want to edit your
/etc/hosts file (or your system&apos;s equivalent) to provide an alternate domain for
127.0.0.1 at which you can load the plugin.
</p>
<h2>Examples</h2>
<Stack gap={3}>
<ul>
{pluginExamples.map(({ id, label }) => (
<li key={id}>
<a href={`#${id}`}>{label}</a>
</li>
))}
</ul>
<Stack gap={5}>
{pluginExamples.map(({ Component, id, label }) => <Component key={id} id={id} label={label} />)}
</Stack>
</Stack>
</Col>
</Row>
</Container>
</main>
);
}
2 changes: 1 addition & 1 deletion example/src/components/DefaultDirectWidget.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react';
export default function DefaultDirectWidget() {
return (
<section className="bg-success p-3 text-light">
<h3>Default Direct Widget</h3>
<h4>Default Direct Widget</h4>
<p>
This widget is a default component that lives in the example app and is directly inserted via JS configuration.
Note that this default widget appears after the Inserted Direct Plugin. This is because this component&apos;s
Expand Down
4 changes: 2 additions & 2 deletions example/src/components/ModularComponent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import PropTypes from 'prop-types';
export default function ModularComponent({ content }) {
return (
<section className="bg-light p-3">
<h3>{ content.title }</h3>
<h4>{content.title}</h4>
<p>
This is a modular component that lives in the example app.
</p>
<p>
<p className="mb-0">
<em>{content.uniqueText}</em>
</p>
</section>
Expand Down
2 changes: 1 addition & 1 deletion example/src/components/PluginDirect.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React from 'react';
export default function PluginDirect() {
return (
<section className="bg-warning p-3">
<h3>Inserted Direct Plugin</h3>
<h4>Inserted Direct Plugin</h4>
<p>
This plugin is a component that lives in the example app and is directly inserted via JS configuration.
What makes this unique is that it isn&apos;t part of the default content defined for this slot, but is instead
Expand Down
8 changes: 4 additions & 4 deletions example/src/pluginSlots/PluginSlotWithInsert.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import React from 'react';

import { PluginSlot } from '@openedx/frontend-plugin-framework';

function PluginSlotWithInsert() {
function PluginSlotWithInsert({ id, label }) {
return (
<div className="border border-primary mb-2">
<h2 className="pl-3">Plugin Operation: Insert</h2>
<div className="border border-primary">
<h3 id={id} className="pl-3">{label}</h3>
<PluginSlot
id="slot_with_insert_operation"
>
<section className="bg-success p-3 text-light">
<h3>Default Content</h3>
<h4>Default Content</h4>
<p>
This widget represents a component that is wrapped by the Plugin Slot.

Expand Down
Loading

0 comments on commit f34f4df

Please sign in to comment.