Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
const path = require('path');
/* eslint-disable import/no-extraneous-dependencies */

const { createConfig } = require('@edx/frontend-build');

module.exports = createConfig('eslint', {
settings: {
'import/resolver': {
webpack: {
config: [
path.resolve(__dirname, 'webpack.dev.config.js'),
path.resolve(__dirname, 'webpack.prod.config.js'),
],
},
},
},
rules: {
'react/function-component-definition': 'off',
'import/no-extraneous-dependencies': [
'error', {
devDependencies: false,
optionalDependencies: false,
peerDependencies: false,
packageDir: __dirname,
},
],

},
});
3 changes: 3 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ module.exports = createConfig('jest', {
'src/setupTest.js',
'src/i18n',
],
moduleNameMapper: {
'@node_modules/(.*)': '<rootDir>/node_modules/$1'
},
});
10,521 changes: 5,073 additions & 5,448 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/react-fontawesome": "0.2.0",
"@loadable/component": "^5.15.3",
"@openedx-plugins/communications-app-check-box-form": "file:plugins/communications-app/CheckBoxForm",
"@openedx-plugins/communications-app-input-form": "file:plugins/communications-app/InputForm",
"@tinymce/tinymce-react": "3.14.0",
"axios": "0.27.2",
"classnames": "2.3.2",
Expand All @@ -69,6 +72,7 @@
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.5",
"axios-mock-adapter": "1.21.2",
"eslint-import-resolver-webpack": "^0.13.8",
"glob": "7.2.3",
"husky": "7.0.4",
"jest": "27.5.1",
Expand Down
18 changes: 18 additions & 0 deletions plugins/communications-app/CheckBoxForm/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import PropTypes from 'prop-types';
import { Form, Container } from '@edx/paragon';

const CheckBoxForm = ({ isChecked, handleChange, label }) => (
<Container className="my-4 border border-success-300 p-4">
<Form.Checkbox checked={isChecked} onChange={handleChange}>
{label}
</Form.Checkbox>
</Container>
);

CheckBoxForm.propTypes = {
isChecked: PropTypes.bool.isRequired,
handleChange: PropTypes.func.isRequired,
label: PropTypes.string.isRequired,
};

export default CheckBoxForm;
20 changes: 20 additions & 0 deletions plugins/communications-app/CheckBoxForm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@openedx-plugins/communications-app-check-box-form",
"version": "1.0.0",
"description": "edx input type checkbox form to use it in this mfe",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"peerDependencies": {
"@edx/frontend-app-communications": "*",
"@edx/frontend-platform": "*",
"@edx/paragon": "*",
"prop-types": "*",
"react": "*"
},
"peerDependenciesMeta": {
"@edx/frontend-app-communications": {
"optional": true
}
}
}
29 changes: 29 additions & 0 deletions plugins/communications-app/InputForm/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import PropTypes from 'prop-types';
import { Form, Container } from '@edx/paragon';

const InputForm = ({
isValid, controlId, label, feedbackText,
}) => {
const feedbackType = isValid ? 'valid' : 'invalid';
return (
<Form.Group isValid={isValid} controlId={controlId} data-testid="plugin-input" className="p-3 border border-success-300">
<Form.Label className="h3 text-primary-500">{label}</Form.Label>
<Container className="row">
<Form.Control className="col-3" />
<p className="col-8">@openedx-plugins/communications-app-input-form</p>
</Container>
<Form.Control.Feedback type={feedbackType}>
{feedbackText}
</Form.Control.Feedback>
</Form.Group>
);
};

InputForm.propTypes = {
isValid: PropTypes.bool.isRequired,
controlId: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
feedbackText: PropTypes.string.isRequired,
};

export default InputForm;
20 changes: 20 additions & 0 deletions plugins/communications-app/InputForm/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@openedx-plugins/communications-app-input-form",
"version": "1.0.0",
"description": "edx input form to use it in this mfe",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"peerDependencies": {
"@edx/frontend-app-communications": "*",
"@edx/frontend-platform": "*",
"@edx/paragon": "*",
"prop-types": "*",
"react": "*"
},
"peerDependenciesMeta": {
"@edx/frontend-app-communications": {
"optional": true
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`PluggableComponent renders correctly 1`] = `
<div>
<div
class="pgn__form-group"
data-testid="plugin-input"
>
<label
class="pgn__form-label"
for="randomID"
>
Hello
</label>
<div
class="pgn__form-control-decorator-group"
>
<input
class="form-control is-valid"
id="randomID"
/>
</div>
<div
class="pgn__form-control-description pgn__form-text pgn__form-text-valid"
>
<span
class="pgn__icon"
>
<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="M9 16.2 4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2Z"
fill="currentColor"
/>
</svg>
</span>
<div>
You are correct
</div>
</div>
</div>
</div>
`;
61 changes: 61 additions & 0 deletions src/components/PluggableComponent/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useState, useEffect } from 'react';
import loadable from '@loadable/component';
import PropTypes from 'prop-types';

import { isPluginAvailable } from './utils';

/**
* PluggableComponent - A component that allows dynamic loading and replacement of child components.
*
* @param {object} props - Component props
* @param {React.ReactNode} props.children - Child elements to be passed to the plugin component
* @param {string} props.as - String indicating the module to import dynamically
* @param {string} props.id - Identifier for the plugin
* @param {object} props.pluggableComponentProps - Additional props to be passed to the component
* @returns {React.ReactNode} - Rendered component
*/
const PluggableComponent = ({
children,
as,
id,
...pluggableComponentProps
}) => {
const [newComponent, setNewComponent] = useState(children);

useEffect(() => {
const loadPluginComponent = async () => {
try {
const hasModuleInstalled = await isPluginAvailable(as);

if (hasModuleInstalled) {
const PluginComponent = loadable(() => import(`@node_modules/@openedx-plugins/${as}`));

const component = children ? (
<PluginComponent key={id} {...pluggableComponentProps}>
{children}
</PluginComponent>
) : (
<PluginComponent key={id} {...pluggableComponentProps} />
);

setNewComponent(component);
}
} catch (error) {
console.error(`Failed to load plugin ${as}:`, error);
}
};

loadPluginComponent();
}, [id, as, children]);

return newComponent;
};

PluggableComponent.propTypes = {
children: PropTypes.node,
as: PropTypes.string,
id: PropTypes.string,
};

export default PluggableComponent;
62 changes: 62 additions & 0 deletions src/components/PluggableComponent/index.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from 'react';
import { render, waitFor, screen } from '@testing-library/react';
import PluggableComponent from '.';

describe('PluggableComponent', () => {
it('renders correctly', async () => {
const props = {
isValid: true,
controlId: 'randomID',
label: 'Hello',
feedbackText: 'You are correct',
};
const { container } = render(
<PluggableComponent
id="pluggableComponent"
as="communications-app-input-form"
{...props}
>
<h1>Hi this is the original component</h1>
</PluggableComponent>,
);

await waitFor(() => {
const inputForm = screen.getByTestId('plugin-input');
expect(inputForm).toBeInTheDocument();
expect(screen.getByText(props.label)).toBeInTheDocument();
expect(screen.getByText(props.feedbackText)).toBeInTheDocument();
expect(container).toMatchSnapshot();
});
});

it('loads children component when import is invalid', async () => {
render(
<PluggableComponent id="est-pluggable" as="invalid import">
<div data-testid="plugin">Plugin Loaded</div>
</PluggableComponent>,
);

await waitFor(() => {
const defaultComponent = screen.getByTestId('plugin');
expect(screen.getByText('Plugin Loaded')).toBeInTheDocument();
expect(defaultComponent).toBeInTheDocument();
});
});

it('loads children component when import object', async () => {
render(
<PluggableComponent
id="test-pluggable"
as=""
>
<div data-testid="plugin">Plugin Loaded</div>
</PluggableComponent>,
);

await waitFor(() => {
const defaultComponent = screen.getByTestId('plugin');
expect(screen.getByText('Plugin Loaded')).toBeInTheDocument();
expect(defaultComponent).toBeInTheDocument();
});
});
});
10 changes: 10 additions & 0 deletions src/components/PluggableComponent/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const isPluginAvailable = async (pluginName) => {
if (!pluginName) { return false; }

try {
await import(`@node_modules/@openedx-plugins/${pluginName}`);
return true;
} catch (error) {
return false;
}
};
23 changes: 23 additions & 0 deletions src/components/PluggableComponent/utils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { isPluginAvailable } from './utils';

describe('isPluginAvailable util', () => {
it('returns true if a plugin is installed', async () => {
const checkBoxPlugin = await isPluginAvailable('communications-app-check-box-form');
expect(checkBoxPlugin).toBe(true);
});

it('returns false if a plugin is not installed', async () => {
const nonexistentPlugin = await isPluginAvailable('nonexistentPlugin');
expect(nonexistentPlugin).toBe(false);
});

it('returns false if an empty string is provided as plugin name', async () => {
const emptyPlugin = await isPluginAvailable('');
expect(emptyPlugin).toBe(false);
});

it('returns false if null is provided as plugin name', async () => {
const nullPLugin = await isPluginAvailable(null);
expect(nullPLugin).toBe(false);
});
});
Loading