Skip to content

Commit

Permalink
feat: support merging props with PluginOperations.Modify
Browse files Browse the repository at this point in the history
  • Loading branch information
adamstankiewicz committed Aug 28, 2024
1 parent bf8705b commit 1931be1
Show file tree
Hide file tree
Showing 26 changed files with 763 additions and 135 deletions.
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
39 changes: 32 additions & 7 deletions example/env.config.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ 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';

Expand All @@ -18,12 +17,28 @@ const modifyWidget = (widget) => {
return modifiedWidget;
};

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 modifyWidgetDefaultContents = (widget) => {
const newContent = {
'data-custom-attr': 'customValue',
'data-another-custom-attr': '',
className: 'font-weight-bold',
style: { color: 'blue' },
onClick: (e) => { console.log('Username clicked!', 'custom', e); },
};
widget.content = newContent;
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 +194,17 @@ const config = {
},
},
],
}
},
slot_with_username_pii: {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Modify,
widgetId: 'default_contents',
fn: modifyWidgetDefaultContents,
},
],
},
},
};

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
107 changes: 107 additions & 0 deletions example/src/pluginSlots/PluginSlotWithModifyDefaultContents.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React from 'react';

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


// Example component used as the default childen within a PluginSlot
const Username = ({ className, onClick, ...rest }) => {
const authenticatedUser = { username: 'testuser' };
const { username } = authenticatedUser;
return (
<span
className={classNames('default-classname', className)}
onClick={(e) => {
console.log('Username clicked!', 'default', e);
onClick?.(e);
}}
{...rest}
>
{username}
</span>
);
};

const UsernameWithPluginContent = ({ className, onClick, content = {} }) => {
const {
className: classNameFromPluginContent,
onClick: onClickFromPluginContent,
...contentRest
} = content;
const updatedProps = {
className: classNames(className, classNameFromPluginContent),
onClick: (e) => {
onClick?.(e);
onClickFromPluginContent?.(e);
},
};
return <Username {...updatedProps} {...contentRest} />;
};

function PluginSlotWithModifyDefaultContents({ id, label }) {
return (
<div className="border border-primary px-3">
<h3 id={id}>{label}</h3>
<p>
The following <code>PluginSlot</code> examples demonstrate the <code>PLUGIN_OPERATIONS.Modify</code> operation, when
the <code>widgetId</code> is <code>default_contents</code>. Any configured, custom plugin <code>content</code> is
merged with any existing props passed to the component(s) represented by <code>default_contents</code>.
</p>
<ul>
<li>Custom <code>className</code> overrides are concatenated with the <code>className</code> prop passed to the <code>default_contents</code> component(s), if any.</li>
<li>Custom <code>style</code> overrides are shallow merged with the <code>style</code> prop passed to the <code>default_contents</code> component(s), if any.</li>
<li>Custom event handlers (e.g., <code>onClick</code>) are executed in sequence, after any event handlers passed to the <code>default_contents</code> component(s), if any.</li>
</ul>
<PluginSlot
id="slot_with_username_pii"
as="div"
// Default slotOptions
slotOptions={{
mergeProps: false,
}}
>
<UsernameWithPluginContent
className="tetsing"
onClick={(e) => {
console.log('Username clicked!', 'prop', e);
}}
/>
</PluginSlot>
<PluginSlot
id="slot_with_username_pii"
as="div"
slotOptions={{
mergeProps: true,
}}
>
<Username
className="d-block abc123"
onClick={(e) => {
console.log('Username clicked!', 'prop', e);
}}
/>
{/* <Username className="ghi789" /> // Note: uncomment to see example of incorrectly using multiple `children` elements within `PluginSlot` */}
</PluginSlot>
<PluginSlot
id="slot_with_username_pii"
as="div"
pluginProps={{
className: 'bg-accent-b',
onClick: (e) => { console.log('Username clicked!', 'pluginProps', e); },
}}
slotOptions={{
mergeProps: true,
}}
>
<Username
className="ghi789"
onClick={(e) => {
console.log('Username clicked!', 'prop', e);
}}
/>
</PluginSlot>
</div>
);
}

export default PluginSlotWithModifyDefaultContents;
8 changes: 4 additions & 4 deletions example/src/pluginSlots/PluginSlotWithModifyWrapHide.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@ import { PluginSlot } from '@openedx/frontend-plugin-framework';
import ModularComponent from '../components/ModularComponent';


function PluginSlotWithModifyWrapHide() {
function PluginSlotWithModifyWrapHide({ id, label }) {
const content = {
title: 'Default Content',
uniqueText: "Because this modular component is default content, this text is passed in as a prop within PluginSlot."
}

return (
<div className="border border-primary mb-2">
<h2 className="pl-3">Plugin Operation: Modify, Wrap, and Hide</h2>
<div className="border border-primary">
<h3 id={id} className="pl-3">{label}</h3>
<PluginSlot
id="slot_with_modify_wrap_hidden_operations"
>
<ModularComponent content={content}/>
<ModularComponent content={content} />
</PluginSlot>
</div>
);
Expand Down
Loading

0 comments on commit 1931be1

Please sign in to comment.