Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: UI slot component with the basic config #662

Closed
wants to merge 3 commits into from
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
28 changes: 26 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
const path = require('path');
// eslint-disable-next-line import/no-extraneous-dependencies
const { getBaseConfig } = require('@openedx/frontend-build');

const config = getBaseConfig('eslint');

config.settings = {
'import/resolver': {
webpack: {
config: path.resolve(__dirname, 'webpack.dev.config.js'),
},
alias: {
map: [
['@communications-app', '.'],
],
extensions: ['.ts', '.js', '.jsx', '.json'],
},
},
};
config.rules = {
'import/no-extraneous-dependencies': ['error', {
devDependencies: [
Expand All @@ -12,11 +26,12 @@ config.rules = {
'example/*',
],
}],
'import/prefer-default-export': 'off',
'import/extensions': ['error', {
ignore: ['@edx/frontend-platform*'],
ignore: ['@edx/frontend-platform*', '@openedx/frontend-build*'],
}],
'import/no-unresolved': ['error', {
ignore: ['@edx/frontend-platform*'],
ignore: ['@edx/frontend-platform*', '@openedx/frontend-build*'],
}],
'jsx-a11y/anchor-is-valid': ['error', {
components: ['Link'],
Expand All @@ -25,4 +40,13 @@ config.rules = {
}],
};

config.overrides = [
{
files: ['plugins/**/*.jsx'],
rules: {
'import/no-extraneous-dependencies': 'off',
},
},
];

module.exports = config;
6 changes: 6 additions & 0 deletions example/ExamplePage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { logInfo } from '@edx/frontend-platform/logging';
import { AppContext } from '@edx/frontend-platform/react';
import { ensureConfig, mergeConfig, getConfig } from '@edx/frontend-platform';
import PluggableComponent from '@root_path/src/react/PluggableComponent';
import messages from './messages';

mergeConfig({
Expand Down Expand Up @@ -49,6 +50,11 @@ class ExamplePage extends Component {
<p>JS_FILE_VAR var came through: <strong>{getConfig().JS_FILE_VAR}</strong></p>
<p>Visit <Link to="/authenticated">authenticated page</Link>.</p>
<p>Visit <Link to="/error_example">error page</Link>.</p>
<PluggableComponent
id="pluggable-component-test"
as="any-mfe-plugins-test"
title="This is my button plugin"
/>
</div>
);
}
Expand Down
3,590 changes: 1,620 additions & 1,970 deletions package-lock.json

Large diffs are not rendered by default.

13 changes: 10 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"docs": "jsdoc -c jsdoc.json",
"docs-watch": "nodemon -w src -w docs/template -w README.md -e js,jsx --exec npm run docs",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"i18n_extract": "fedx-scripts formatjs extract",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
Expand All @@ -35,13 +36,16 @@
"devDependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/browserslist-config": "1.2.0",
"@openedx/frontend-build": "13.0.28",
"@openedx/frontend-build": "git+https://github.com/eduNEXT/frontend-build.git#jv/feat-ui-slot-config",
"@openedx/paragon": "22.1.1",
"@testing-library/jest-dom": "6.4.2",
"@testing-library/react": "12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"axios-mock-adapter": "^1.21.3",
"core-js": "3.36.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-import-resolver-webpack": "^0.13.8",
"eslint-plugin-import": "^2.29.1",
"husky": "8.0.3",
"jsdoc": "^4.0.0",
"nodemon": "3.1.0",
Expand All @@ -57,6 +61,8 @@
"@cospired/i18n-iso-languages": "4.2.0",
"@formatjs/intl-pluralrules": "4.3.3",
"@formatjs/intl-relativetimeformat": "10.0.1",
"@loadable/component": "^5.16.3",
"@openedx-plugins/any-mfe-plugins-test": "file:plugins/any-mfe-plugins/TestComponent",
"axios": "0.27.2",
"axios-cache-interceptor": "0.10.7",
"form-urlencoded": "4.1.4",
Expand All @@ -72,10 +78,11 @@
"lodash.snakecase": "4.1.1",
"pubsub-js": "1.9.4",
"react-intl": "6.5.5",
"universal-cookie": "4.0.4"
"universal-cookie": "4.0.4",
"use-deep-compare-effect": "^1.8.1"
},
"peerDependencies": {
"@openedx/frontend-build": ">= 13.0.15",
"@openedx/frontend-build": "git+https://github.com/johnvente/frontend-build.git#jv/feat-ui-slot-config",
"@openedx/paragon": ">= 21.5.7 < 23.0.0",
"prop-types": "^15.7.2",
"react": "^16.9.0 || ^17.0.0",
Expand Down
21 changes: 21 additions & 0 deletions plugins/any-mfe-plugins/TestComponent/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';

function TestComponent({ handleClick, title }) {
return (
<button type="button" onClick={handleClick} data-testid="button-test">
{title}
</button>
);
}

TestComponent.defaultProps = {
handleClick: () => {},
};

TestComponent.propTypes = {
handleClick: PropTypes.func,
title: PropTypes.string.isRequired,
};

export default TestComponent;
12 changes: 12 additions & 0 deletions plugins/any-mfe-plugins/TestComponent/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@openedx-plugins/any-mfe-plugins-test",
"version": "1.0.0",
"description": "edx input form to use it in this mfe",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"peerDependencies": {
"prop-types": "*",
"react": "*"
}
}
119 changes: 119 additions & 0 deletions src/react/PluggableComponent/MultiplePlugins.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React, { useState, useEffect, useRef } from 'react';
import loadable from '@loadable/component';
import useDeepCompareEffect from 'use-deep-compare-effect';
import PropTypes from 'prop-types';

import { isPluginAvailable, getPluginsByPrefix } from './utils';

function MultiplePlugins({
plugins,
pluggableComponentProps,
prefix,
loadingComponent,
containerPluginsProps,
}) {
const [pluginComponents, setPluginComponents] = useState({});
const loadedAllPluginsRef = useRef(null);

useEffect(() => {
const loadPlugins = (pluginsList) => {
pluginsList.forEach((plugin, index) => {
// Initially set the loading component for each plugin
setPluginComponents(previousPluginComponents => ({
...previousPluginComponents,
[plugin.id]: loadingComponent || null,
}));

const loadPlugin = async () => {
try {
const hasModuleInstalled = await isPluginAvailable(plugin.name);
if (hasModuleInstalled) {
const PluginComponent = loadable(() => import(`@node_modules/@openedx-plugins/${plugin.name}`));
setPluginComponents(previousPluginComponents => ({
...previousPluginComponents,
[plugin.id]: (
<PluginComponent {...pluggableComponentProps} />
),
}));
}
} catch (error) {
console.error(`Failed to load plugin ${plugin.name}:`, error);

Check warning on line 40 in src/react/PluggableComponent/MultiplePlugins.jsx

View workflow job for this annotation

GitHub Actions / tests

Unexpected console statement

Check warning on line 40 in src/react/PluggableComponent/MultiplePlugins.jsx

View check run for this annotation

Codecov / codecov/patch

src/react/PluggableComponent/MultiplePlugins.jsx#L40

Added line #L40 was not covered by tests
// Set to null in case of an error
setPluginComponents(previousPluginComponents => ({

Check warning on line 42 in src/react/PluggableComponent/MultiplePlugins.jsx

View check run for this annotation

Codecov / codecov/patch

src/react/PluggableComponent/MultiplePlugins.jsx#L42

Added line #L42 was not covered by tests
...previousPluginComponents,
[plugin.id]: null,
}));
} finally {
const isLastPlugin = index === pluginsList.length - 1;
if (isLastPlugin) {
loadedAllPluginsRef.current = true;
}
}
};

loadPlugin();
});
};

const pluginsToLoad = prefix ? getPluginsByPrefix(prefix) : plugins;

if (pluginsToLoad.length) {
loadPlugins(pluginsToLoad);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useDeepCompareEffect(() => {
const updatePluginsWithNewProps = () => {
const updatedComponents = Object.keys(pluginComponents).reduce((previousPluginComponents, pluginKey) => {
const PluginComponent = pluginComponents[pluginKey];
// Check if the component is a valid React element and not a loading or error state
if (React.isValidElement(PluginComponent)) {
const UpdatedComponent = React.cloneElement(PluginComponent, pluggableComponentProps);
return {
...previousPluginComponents,
[pluginKey]: UpdatedComponent,
};
}
return previousPluginComponents;

Check warning on line 78 in src/react/PluggableComponent/MultiplePlugins.jsx

View check run for this annotation

Codecov / codecov/patch

src/react/PluggableComponent/MultiplePlugins.jsx#L78

Added line #L78 was not covered by tests
}, {});

setPluginComponents(updatedComponents);
};

if (loadedAllPluginsRef.current) {
updatePluginsWithNewProps();
}
}, [pluggableComponentProps]);

return (
<div {...containerPluginsProps}>
{Object.entries(pluginComponents).map(([pluginKey, Component]) => (
<React.Fragment key={pluginKey}>
{Component}
</React.Fragment>
))}
</div>
);
}

MultiplePlugins.defaultProps = {
plugins: [],
pluggableComponentProps: {},
prefix: '',
loadingComponent: null,
containerPluginsProps: {},
};

MultiplePlugins.propTypes = {
plugins: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
})),
pluggableComponentProps: PropTypes.shape({}),
prefix: PropTypes.string,
loadingComponent: PropTypes.node,
containerPluginsProps: PropTypes.shape({}),
};

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

describe('MultiplePlugins', () => {
const mockPlugins = [
{ id: 'plugin1', name: 'Plugin1' },
{ id: 'plugin2', name: 'Plugin2' },
];

beforeEach(() => {
jest.resetModules();
jest.clearAllMocks();
});

/* eslint-disable react/prop-types */
test('initializes with loading components for each plugin', async () => {
const { getAllByText } = render(
<MultiplePlugins
plugins={mockPlugins}
pluggableComponentProps={{}}
loadingComponent={<div>Loading...</div>}
/>,
);

const [pluginLoading1, pluginLoading2] = getAllByText('Loading...');

expect(pluginLoading1).toBeInTheDocument();
expect(pluginLoading1).toHaveTextContent('Loading...');
expect(pluginLoading2).toBeInTheDocument();
expect(pluginLoading2).toHaveTextContent('Loading...');
});

test('loads a plugins list successfully', async () => {
const mockValidPlugins = [
{ id: 'plugin1', name: 'any-mfe-plugins-test' },
];

function MockPluginComponent() {
return <div data-testid="plugin1">Mocked Plugin Component</div>;
}

jest.mock(
'@node_modules/@openedx-plugins/any-mfe-plugins-test',
() => MockPluginComponent,
);

const { getByTestId } = render(
<MultiplePlugins plugins={mockValidPlugins} pluggableComponentProps={{}} />,
);

await waitFor(() => {
const pluginComponent = getByTestId('plugin1');
expect(pluginComponent).toBeInTheDocument();
expect(pluginComponent).toHaveTextContent('Mocked Plugin Component');
});
});

test('loads a plugin successfully with prefix', async () => {
function MockPluginComponent() {
return <div data-testid="any-mfe-plugins-test">Mocked Plugin Component</div>;
}

jest.mock(
'@node_modules/@openedx-plugins/any-mfe-plugins-test',
() => MockPluginComponent,
);

const { getByTestId } = render(
<MultiplePlugins
pluggableComponentProps={{}}
prefix="any-mfe-plugins-test"
/>,
);

await waitFor(() => {
const pluginComponent = getByTestId('any-mfe-plugins-test');
expect(pluginComponent).toBeInTheDocument();
expect(pluginComponent).toHaveTextContent('Mocked Plugin Component');
});
});

test('loads a plugin successfully with prefix changing component props', async () => {
function MockPluginComponent(props) {
return <div data-testid="mock-plugin-props">{props.title}</div>;
}

jest.mock(
'@node_modules/@openedx-plugins/any-mfe-plugins-test',
() => MockPluginComponent,
);

const { getByTestId, rerender } = render(
<MultiplePlugins
prefix="any-mfe-plugins-test"
pluggableComponentProps={{ title: 'Initial Title' }}
/>,
);

// Wait for the component to be in the document with initial props
await waitFor(() => {
const pluginComponent = getByTestId('mock-plugin-props');
expect(pluginComponent).toBeInTheDocument();
expect(pluginComponent).toHaveTextContent('Initial Title');
});

rerender(
<MultiplePlugins
prefix="any-mfe-plugins-test"
pluggableComponentProps={{ title: 'Title updated' }}
/>,
);

await waitFor(() => {
const pluginComponent = getByTestId('mock-plugin-props');
expect(pluginComponent).toBeInTheDocument();
expect(pluginComponent).toHaveTextContent('Title updated');
});
});
});
Loading
Loading