Compare commits

...

16 Commits

Author SHA1 Message Date
jacobo-dominguez-wgu
db201af910 feat: updating help buttons links and texts (#535) 2025-06-03 13:15:58 -04:00
renovate[bot]
281b817084 chore(deps): update dependency @openedx/paragon to v23.10.0 (#537)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 11:18:45 +00:00
renovate[bot]
08b69b8096 chore(deps): update dependency @edx/frontend-platform to v8.3.8 (#536)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-02 06:14:10 +00:00
jacobo-dominguez-wgu
7c8876490d feat: updating help section and new slots created (#530) 2025-05-30 11:56:53 -04:00
renovate[bot]
ba2e46bfa9 chore(deps): update dependency react-router-dom to v6.30.1 (#534)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-26 12:46:00 +00:00
renovate[bot]
09cc332306 chore(deps): update dependency @edx/frontend-platform to v8.3.7 (#533)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-26 06:30:37 +00:00
renovate[bot]
1f91a60b6d chore(deps): update dependency @openedx/paragon to v23.6.0 (#532)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 07:49:18 +00:00
renovate[bot]
490da867df fix(deps): update dependency ts-jest to v29.3.4 (#531)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-19 04:14:02 +00:00
renovate[bot]
fd723e6063 chore(deps): update dependency @openedx/paragon to v23.5.1 (#529)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 11:38:45 +00:00
renovate[bot]
664ca3e78f chore(deps): update dependency @edx/frontend-platform to v8.3.6 (#528)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-12 06:10:48 +00:00
renovate[bot]
18a8a76105 chore(deps): update dependency @openedx/frontend-build to v14.6.0 (#527)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-05-05 06:41:24 +00:00
renovate[bot]
0ad89f88a1 fix(deps): update dependency ts-jest to v29.3.2 (#525)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 06:11:35 +00:00
renovate[bot]
b0391866a3 chore(deps): update dependency @openedx/frontend-build to v14.5.0 (#524)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-28 04:15:28 +00:00
Brian Smith
3a9e3f8fba feat: add another FooterSlot id alias (#523) 2025-04-24 13:53:20 -04:00
renovate[bot]
b2c55a09e5 chore(deps): update dependency @openedx/paragon to v23.4.5 (#522)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-21 06:00:42 +00:00
Brian Smith
6795147255 feat: move FooterSlot and StudioFooterSlot in from slot-footer (#521)
Co-authored-by: Muhammad Anas <88967643+Anas12091101@users.noreply.github.com>
2025-04-17 17:35:52 -04:00
35 changed files with 794 additions and 132 deletions

View File

@@ -95,9 +95,9 @@ This library has the following exports:
Plugin
======
The footer can be replaced using using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
The footer can be replaced or modified using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
Information on how to utilize the ``FooterSlot`` component to do so is available in the `frontend-slot-footer repository <https://github.com/openedx/frontend-slot-footer/>`_.
Information on how to replace or modify the footer is available `here </src/plugin-slots>`_.
Examples
========

85
package-lock.json generated
View File

@@ -14,7 +14,7 @@
"@fortawesome/free-regular-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "0.2.2",
"@openedx/frontend-plugin-framework": "^1.5.0",
"@openedx/frontend-plugin-framework": "^1.7.0",
"classnames": "^2.5.1",
"jest-environment-jsdom": "^29.7.0",
"lodash": "^4.17.21",
@@ -35,7 +35,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-redux": "8.1.3",
"react-router-dom": "6.30.0",
"react-router-dom": "6.30.1",
"react-test-renderer": "18.3.1",
"redux": "4.2.1",
"semantic-release": "21.1.2"
@@ -2262,16 +2262,16 @@
}
},
"node_modules/@edx/frontend-platform": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-8.3.4.tgz",
"integrity": "sha512-V3XtTo3KP8QSmId+Vvi4+qzpOVkxvTMNA6jH/i3Bfz+/jHjHBRnmp/Cc2pjTxiTgGNoKX4D1twiZkOBO+kWw1Q==",
"version": "8.3.8",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-8.3.8.tgz",
"integrity": "sha512-wr3HKzDcYGNuHcM7HuZ/mqBdqnY/A7eYa3JDVZEsceyjy0PJyVKHWFu3yneGpD1zufguQCvS0q53C8gJbVzIgw==",
"license": "AGPL-3.0",
"dependencies": {
"@cospired/i18n-iso-languages": "4.2.0",
"@formatjs/intl-pluralrules": "4.3.3",
"@formatjs/intl-relativetimeformat": "10.0.1",
"axios": "1.8.4",
"axios-cache-interceptor": "1.6.2",
"axios": "1.9.0",
"axios-cache-interceptor": "1.8.0",
"form-urlencoded": "4.1.4",
"glob": "7.2.3",
"history": "4.10.1",
@@ -3896,9 +3896,9 @@
}
},
"node_modules/@openedx/frontend-build": {
"version": "14.4.2",
"resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-14.4.2.tgz",
"integrity": "sha512-RWAsaYq88cGlqO4eDDo/ylY6dJsbeBsI+4LxmKPrW4MPh0rIFZILvH+X3z/t9SVTlGTx4UkUQV9LXPGLKdcA1g==",
"version": "14.6.0",
"resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-14.6.0.tgz",
"integrity": "sha512-lQn/IYC2xZxmYtm9AsrEXm7o3Ei0voZNTKFDGiBbZARsh27b2/e8hx3ToJ4B3rwxnSHLNYbxVYqAWSiAXG06dw==",
"license": "AGPL-3.0",
"dependencies": {
"@babel/cli": "7.24.8",
@@ -4048,9 +4048,9 @@
}
},
"node_modules/@openedx/frontend-plugin-framework": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@openedx/frontend-plugin-framework/-/frontend-plugin-framework-1.6.0.tgz",
"integrity": "sha512-zgP+/hs/cvcPmFOgVm2xt/qgX1nheNsfipzCO7I3bON4hHyOhmOyzwFZJ7pz7GzCJwKlMVguh3HcJgf4p/BPKQ==",
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@openedx/frontend-plugin-framework/-/frontend-plugin-framework-1.7.0.tgz",
"integrity": "sha512-8tGkuHvtzhbqb9dU4sXUtR0K44+Hjh1uGR6DvhZAt9wSKQC1v4RBk34ef8DFzQhoNQa/Jtn6BJuta4Un6MmHmw==",
"license": "AGPL-3.0",
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
@@ -4114,9 +4114,9 @@
}
},
"node_modules/@openedx/paragon": {
"version": "23.4.3",
"resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-23.4.3.tgz",
"integrity": "sha512-twzFgkVZBZKDUhepqvdH1er+0uQSBGs/r1TDIGmN/zNjmc87g7RMbeDWF+j96eOaiTet9Wcv4JHoB3zE6VycFQ==",
"version": "23.10.0",
"resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-23.10.0.tgz",
"integrity": "sha512-Y+uEuutGkdEAtqrIdL29sWFz2BtxkFhjEPGfIpDOcKt8qsInkuDyXAC21BLYGjl2ZRdza8z1apv/vlbbwqHrLQ==",
"license": "Apache-2.0",
"workspaces": [
"example",
@@ -7375,9 +7375,9 @@
}
},
"node_modules/axios": {
"version": "1.8.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz",
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
@@ -7386,9 +7386,9 @@
}
},
"node_modules/axios-cache-interceptor": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/axios-cache-interceptor/-/axios-cache-interceptor-1.6.2.tgz",
"integrity": "sha512-YLbAODIHZZIcD4b3WYFVQOa5W2TY/WnJ6sBHqAg6Z+hx+RVj8/OcjQyRopO6awn7/kOkGL5X9TP16AucnlJ/lw==",
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/axios-cache-interceptor/-/axios-cache-interceptor-1.8.0.tgz",
"integrity": "sha512-cTNnPGJyQkxnWp0EWvE3NRvgURU5cWw/Qx3dIhXyHSM4Ip0c7EEe0I3an0Jwa549m1CAOg57ibj27YRNLmQCcg==",
"license": "MIT",
"dependencies": {
"cache-parser": "1.2.5",
@@ -22765,9 +22765,9 @@
}
},
"node_modules/react-router": {
"version": "6.30.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz",
"integrity": "sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==",
"version": "6.30.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
"integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.23.0"
@@ -22780,13 +22780,13 @@
}
},
"node_modules/react-router-dom": {
"version": "6.30.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.0.tgz",
"integrity": "sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==",
"version": "6.30.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
"integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.23.0",
"react-router": "6.30.0"
"react-router": "6.30.1"
},
"engines": {
"node": ">=14.0.0"
@@ -26112,9 +26112,9 @@
}
},
"node_modules/ts-jest": {
"version": "29.2.6",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.6.tgz",
"integrity": "sha512-yTNZVZqc8lSixm+QGVFcPe6+yj7+TWZwIesuOWvfcn4B9bz5x4NDzVCQQjOs7Hfouu36aEqfEbo9Qpo+gq8dDg==",
"version": "29.3.4",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz",
"integrity": "sha512-Iqbrm8IXOmV+ggWHOTEbjwyCf2xZlUMv5npExksXohL+tk8va4Fjhb+X2+Rt9NBmgO7bJ8WpnMLOwih/DnMlFA==",
"license": "MIT",
"dependencies": {
"bs-logger": "^0.2.6",
@@ -26124,7 +26124,8 @@
"json5": "^2.2.3",
"lodash.memoize": "^4.1.2",
"make-error": "^1.3.6",
"semver": "^7.7.1",
"semver": "^7.7.2",
"type-fest": "^4.41.0",
"yargs-parser": "^21.1.1"
},
"bin": {
@@ -26160,9 +26161,9 @@
}
},
"node_modules/ts-jest/node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -26171,6 +26172,18 @@
"node": ">=10"
}
},
"node_modules/ts-jest/node_modules/type-fest": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ts-jest/node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",

View File

@@ -10,6 +10,7 @@
"build": "make build",
"i18n_extract": "fedx-scripts formatjs extract",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"start:with-theme": "paragon install-theme && npm start && npm install",
@@ -44,7 +45,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-redux": "8.1.3",
"react-router-dom": "6.30.0",
"react-router-dom": "6.30.1",
"react-test-renderer": "18.3.1",
"redux": "4.2.1",
"semantic-release": "21.1.2"
@@ -55,7 +56,7 @@
"@fortawesome/free-regular-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "0.2.2",
"@openedx/frontend-plugin-framework": "^1.5.0",
"@openedx/frontend-plugin-framework": "^1.7.0",
"classnames": "^2.5.1",
"jest-environment-jsdom": "^29.7.0",
"lodash": "^4.17.21",

View File

@@ -7,6 +7,8 @@ import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import Footer from './Footer';
import FooterSlot from '../plugin-slots/FooterSlot';
import StudioFooterHelpSectionSlot from '../plugin-slots/StudioFooterHelpSectionSlot';
const FooterWithContext = ({ locale = 'es' }) => {
const contextValue = useMemo(() => ({
@@ -22,7 +24,7 @@ const FooterWithContext = ({ locale = 'es' }) => {
<AppContext.Provider
value={contextValue}
>
<Footer />
<FooterSlot />
</AppContext.Provider>
</IntlProvider>
);
@@ -89,3 +91,32 @@ describe('<Footer />', () => {
});
});
});
describe('<StudioFooterHelpSectionSlot />', () => {
const SectionWithContext = ({ locale = 'es' }) => {
const contextValue = useMemo(() => ({
authenticatedUser: null,
config: {
LOGO_TRADEMARK_URL: process.env.LOGO_TRADEMARK_URL,
LMS_BASE_URL: process.env.LMS_BASE_URL,
},
}), []);
return (
<IntlProvider locale={locale}>
<AppContext.Provider
value={contextValue}
>
<StudioFooterHelpSectionSlot />
</AppContext.Provider>
</IntlProvider>
);
};
it('renders correctly', () => {
const tree = renderer
.create(<SectionWithContext />)
.toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -128,3 +128,68 @@ exports[`<Footer /> renders correctly renders without a language selector in es
</div>
</footer>
`;
exports[`<StudioFooterHelpSectionSlot /> renders correctly 1`] = `
[
<div
className="m-0 mt-6 row align-items-center justify-content-center"
>
<div
className="col border-top mr-2"
/>
<button
className="btn btn-outline-primary btn-sm"
data-testid="helpToggleButton"
disabled={false}
onClick={[Function]}
type="button"
>
<span
className="pgn__icon pgn__icon__sm btn-icon-before"
>
<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="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm1 17h-2v-2h2v2Zm2.07-7.75-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25Z"
fill="currentColor"
/>
</svg>
</span>
Looking for help with Studio?
<span
className="pgn__icon pgn__icon__sm btn-icon-after"
>
<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="M16.59 8.59 12 13.17 7.41 8.59 6 10l6 6 6-6-1.41-1.41Z"
fill="currentColor"
/>
</svg>
</span>
</button>
<div
className="col border-top ml-2"
/>
</div>,
<div
className="px-4 container-mw-xl container-fluid"
/>,
]
`;

View File

@@ -1,21 +1,19 @@
import React, { useContext, useState } from 'react';
import React, { useContext } from 'react';
import isEmpty from 'lodash/isEmpty';
import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n';
import { ensureConfig } from '@edx/frontend-platform';
import { AppContext } from '@edx/frontend-platform/react';
import {
ActionRow,
Button,
Container,
Hyperlink,
TransitionReplace,
} from '@openedx/paragon';
import { ExpandLess, ExpandMore, Help } from '@openedx/paragon/icons';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import messages from './messages';
import StudioFooterLogoSlot from '../../plugin-slots/StudioFooterLogoSlot';
import StudioFooterHelpSectionSlot from '../../plugin-slots/StudioFooterHelpSectionSlot';
ensureConfig([
'LMS_BASE_URL',
@@ -32,76 +30,18 @@ const StudioFooter = ({
containerProps,
}) => {
const intl = useIntl();
const [isOpen, setIsOpen] = useState(false);
const { config } = useContext(AppContext);
const { containerClassName, ...restContainerProps } = containerProps || {};
return (
<>
<div className="m-0 mt-6 row align-items-center justify-content-center">
<div className="col border-top mr-2" />
<Button
data-testid="helpToggleButton"
variant="outline-primary"
onClick={() => setIsOpen(!isOpen)}
iconBefore={Help}
iconAfter={isOpen ? ExpandLess : ExpandMore}
size="sm"
>
{isOpen ? intl.formatMessage(messages.closeHelpButtonLabel)
: intl.formatMessage(messages.openHelpButtonLabel)}
</Button>
<div className="col border-top ml-2" />
</div>
<StudioFooterHelpSectionSlot containerProps={containerProps} />
<Container
size="xl"
className={classNames('px-4', containerClassName)}
{...restContainerProps}
>
<TransitionReplace>
{isOpen ? (
<ActionRow key="help-link-button-row" className="py-4" data-testid="helpButtonRow">
<ActionRow.Spacer />
<Button as="a" href="https://docs.openedx.org/" size="sm">
<FormattedMessage {...messages.edxDocumentationButtonLabel} />
</Button>
<Button
as="a"
href="https://openedx.org/"
size="sm"
data-testid="openEdXPortalButton"
>
<FormattedMessage {...messages.openEdxPortalButtonLabel} />
</Button>
<Button
as="a"
href="https://www.edx.org/course/edx101-overview-of-creating-an-edx-course#.VO4eaLPF-n1"
size="sm"
>
<FormattedMessage {...messages.edx101ButtonLabel} />
</Button>
<Button
as="a"
href="https://www.edx.org/course/studiox-creating-a-course-with-edx-studio"
size="sm"
>
<FormattedMessage {...messages.studioXButtonLabel} />
</Button>
{!isEmpty(config.SUPPORT_EMAIL) && (
<Button
as="a"
href={`mailto:${config.SUPPORT_EMAIL}`}
size="sm"
data-testid="contactUsButton"
>
<FormattedMessage {...messages.contactUsButtonLabel} />
</Button>
)}
<ActionRow.Spacer />
</ActionRow>
) : null}
</TransitionReplace>
<ActionRow className="pt-3 m-0 x-small">
© {new Date().getFullYear()} <Hyperlink destination={config.MARKETING_SITE_BASE_URL} target="_blank" className="ml-2">{config.SITE_NAME}</Hyperlink>
<ActionRow.Spacer />

View File

@@ -3,9 +3,10 @@ import React, { useMemo } from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom/extend-expect';
import '@testing-library/jest-dom';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import StudioFooter from './StudioFooter';
import StudioFooterSlot from '../../plugin-slots/StudioFooterSlot';
import messages from './messages';
const config = {
@@ -36,7 +37,7 @@ const Component = ({ updateVariable }) => {
return (
<IntlProvider locale="en">
<AppContext.Provider value={contextValue}>
<StudioFooter />
<StudioFooterSlot />
</AppContext.Provider>
</IntlProvider>
);
@@ -77,7 +78,7 @@ describe('Footer', () => {
render(<Component />);
const helpToggleButton = screen.getByText(messages.openHelpButtonLabel.defaultMessage);
await user.click(helpToggleButton);
expect(screen.getByTestId('openEdXPortalButton')).toBeVisible();
expect(screen.getByTestId('openEdXDemoCourseButton')).toBeVisible();
});
it('should not show contact us button', async () => {
const user = userEvent.setup();

View File

@@ -0,0 +1,32 @@
import React from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Button,
} from '@openedx/paragon';
import { ExpandLess, ExpandMore, Help } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import messages from '../messages';
const HelpButton = ({ isOpen, setIsOpen }) => {
const intl = useIntl();
return (
<Button
data-testid="helpToggleButton"
variant="outline-primary"
onClick={() => setIsOpen(!isOpen)}
iconBefore={Help}
iconAfter={isOpen ? ExpandLess : ExpandMore}
size="sm"
>
{isOpen ? intl.formatMessage(messages.closeHelpButtonLabel)
: intl.formatMessage(messages.openHelpButtonLabel)}
</Button>
);
};
HelpButton.propTypes = {
setIsOpen: PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
};
export default HelpButton;

View File

@@ -0,0 +1,55 @@
import React, { useMemo } from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import HelpButton from './HelpButton';
import '@testing-library/jest-dom';
// eslint-disable-next-line react/prop-types
const ButtonWithContext = ({ locale = 'en', isOpen, setIsOpen }) => {
const contextValue = useMemo(() => ({
authenticatedUser: null,
config: { },
}), []);
return (
<IntlProvider locale={locale}>
<AppContext.Provider
value={contextValue}
>
<HelpButton isOpen={isOpen} setIsOpen={setIsOpen} />
</AppContext.Provider>
</IntlProvider>
);
};
describe('HelpButton', () => {
const mockSetIsOpen = jest.fn();
beforeEach(() => {
mockSetIsOpen.mockClear();
});
it('renders with "open" label when isOpen is false', () => {
render(<ButtonWithContext isOpen={false} setIsOpen={mockSetIsOpen} />);
expect(screen.getByTestId('helpToggleButton')).toBeInTheDocument();
expect(screen.getByRole('button')).toHaveTextContent(/help|open/i);
});
it('renders with "close" label when isOpen is true', () => {
render(<ButtonWithContext isOpen setIsOpen={mockSetIsOpen} />);
expect(screen.getByTestId('helpToggleButton')).toBeInTheDocument();
expect(screen.getByRole('button')).toHaveTextContent(/close|help/i);
});
it('calls setIsOpen with the toggled value when clicked', async () => {
const user = userEvent.setup();
const { rerender } = render(<ButtonWithContext isOpen={false} setIsOpen={mockSetIsOpen} />);
await user.click(screen.getByTestId('helpToggleButton'));
expect(mockSetIsOpen).toHaveBeenCalledWith(true);
rerender(<ButtonWithContext isOpen setIsOpen={mockSetIsOpen} />);
await user.click(screen.getByTestId('helpToggleButton'));
expect(mockSetIsOpen).toHaveBeenCalledWith(false);
});
});

View File

@@ -0,0 +1,62 @@
import React, { useContext } from 'react';
import { AppContext } from '@edx/frontend-platform/react';
import isEmpty from 'lodash/isEmpty';
import { ensureConfig } from '@edx/frontend-platform';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import {
ActionRow,
Button,
} from '@openedx/paragon';
import messages from '../messages';
ensureConfig([
'SUPPORT_EMAIL',
], 'Studio Footer Help Content component');
const BUTTONS = [
{
as: 'a',
href: 'https://docs.openedx.org/en/latest/educators/quickstarts/build_a_course.html',
size: 'sm',
message: messages.educatorsDocsButtonLabel,
dataTestid: null,
},
{
as: 'a',
href: 'https://sandbox.openedx.org/courses/course-v1:OpenedX+01-2024+2024-1/about',
size: 'sm',
message: messages.openEdxDemoCourseButtonLabel,
dataTestid: 'openEdXDemoCourseButton',
},
];
const HelpContent = () => {
const { config } = useContext(AppContext);
return (
<ActionRow key="help-link-button-row" className="py-4" data-testid="helpButtonRow">
<ActionRow.Spacer />
{BUTTONS.map(({
as, href, size, message, dataTestid,
}) => (
<Button as={as} href={href} size={size} key={message.id} data-testid={dataTestid}>
<FormattedMessage {...message} />
</Button>
))}
{!isEmpty(config.SUPPORT_EMAIL) && (
<Button
as="a"
href={`mailto:${config.SUPPORT_EMAIL}`}
size="sm"
data-testid="contactUsButton"
>
<FormattedMessage {...messages.contactUsButtonLabel} />
</Button>
)}
<ActionRow.Spacer />
</ActionRow>
);
};
export default HelpContent;

View File

@@ -0,0 +1,44 @@
import React, { useMemo } from 'react';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import HelpContent from './HelpContent';
import '@testing-library/jest-dom';
import messages from '../messages';
// eslint-disable-next-line react/prop-types
const ContentWithContext = ({ locale = 'en', config = {} }) => {
const contextValue = useMemo(() => ({
authenticatedUser: null,
config,
}), [config]);
return (
<IntlProvider locale={locale}>
<AppContext.Provider
value={contextValue}
>
<HelpContent />
</AppContext.Provider>
</IntlProvider>
);
};
describe('HelpContent', () => {
it('renders all help buttons', () => {
const config = { SUPPORT_EMAIL: 'support@example.com' };
render(<ContentWithContext config={config} />);
expect(screen.getByText(messages.educatorsDocsButtonLabel.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(messages.openEdxDemoCourseButtonLabel.defaultMessage)).toBeInTheDocument();
});
it('does not render contact button if SUPPORT_EMAIL is empty', () => {
render(<ContentWithContext config={{ SUPPORT_EMAIL: null }} />);
expect(screen.queryByTestId('contactUsButton')).not.toBeInTheDocument();
});
it('renders ActionRow with correct test id', () => {
render(<ContentWithContext />);
expect(screen.getByTestId('helpButtonRow')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,43 @@
import React from 'react';
import {
Container,
TransitionReplace,
} from '@openedx/paragon';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import StudioFooterHelpButtonSlot from '../../../plugin-slots/StudioFooterHelpButtonSlot';
import StudioFooterHelpContentlot from '../../../plugin-slots/StudioFooterHelpContentSlot';
const HelpSection = ({ containerProps }) => {
const [isOpen, setIsOpen] = React.useState(false);
const { containerClassName, ...restContainerProps } = containerProps || {};
return (
<>
<div className="m-0 mt-6 row align-items-center justify-content-center">
<div className="col border-top mr-2" />
<StudioFooterHelpButtonSlot
setIsOpen={setIsOpen}
isOpen={isOpen}
/>
<div className="col border-top ml-2" />
</div>
<Container
size="xl"
className={classNames('px-4', containerClassName)}
{...restContainerProps}
>
{isOpen && <TransitionReplace><StudioFooterHelpContentlot /></TransitionReplace> }
</Container>
</>
);
};
HelpSection.propTypes = {
containerProps: PropTypes.shape(Container.propTypes),
};
HelpSection.defaultProps = {
containerProps: {},
};
export default HelpSection;

View File

@@ -0,0 +1,52 @@
import React, { useMemo } from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import HelpSection from './HelpSection';
import '@testing-library/jest-dom';
// eslint-disable-next-line react/prop-types
const HelpSectionWithContext = ({ locale = 'en', config = {}, containerProps = null }) => {
const contextValue = useMemo(() => ({
authenticatedUser: null,
config,
}), [config]);
return (
<IntlProvider locale={locale}>
<AppContext.Provider
value={contextValue}
>
<HelpSection containerProps={containerProps} />
</AppContext.Provider>
</IntlProvider>
);
};
describe('HelpSection', () => {
it('renders the HelpButton', () => {
render(<HelpSectionWithContext />);
expect(screen.getByTestId('helpToggleButton')).toBeInTheDocument();
});
it('does not show HelpContent by default', () => {
render(<HelpSectionWithContext />);
expect(screen.queryByTestId('helpButtonRow')).not.toBeInTheDocument();
});
it('shows HelpContent when HelpButton is clicked', () => {
render(<HelpSectionWithContext />);
const button = screen.getByTestId('helpToggleButton');
fireEvent.click(button);
expect(screen.getByTestId('helpButtonRow')).toBeInTheDocument();
});
it('hides HelpContent when HelpButton is clicked twice', () => {
render(<HelpSectionWithContext containerProps={{ containerClassName: 'container', size: 'xl' }} />);
const button = screen.getByTestId('helpToggleButton');
fireEvent.click(button);
expect(screen.queryByTestId('helpButtonRow')).toBeInTheDocument();
fireEvent.click(button);
expect(screen.queryByTestId('helpButtonRow')).not.toBeInTheDocument();
});
});

View File

@@ -11,25 +11,15 @@ const messages = defineMessages({
defaultMessage: 'Hide Studio help',
description: 'Label for button that closes the collapsed section with help buttons',
},
edxDocumentationButtonLabel: {
id: 'authoring.footer.help.edxDocumentation.button.label',
defaultMessage: 'edX documentation',
description: 'Label for button that links to the edX documentation site',
educatorsDocsButtonLabel: {
id: 'authoring.footer.help.educatorsDocs.button.label',
defaultMessage: 'Open edX Educators Docs',
description: 'Label for button that links to the build a course quickstart site',
},
openEdxPortalButtonLabel: {
id: 'authoring.footer.help.openEdxPortal.button.label',
defaultMessage: 'Open edX portal',
description: 'Label for button that links to the Open edX portal',
},
edx101ButtonLabel: {
id: 'authoring.footer.help.edx101.button.label',
defaultMessage: 'Enroll in edX 101',
description: 'Label for button that links to the edX 101 course',
},
studioXButtonLabel: {
id: 'authoring.footer.help.studioX.button.label',
defaultMessage: 'Enroll in StudioX',
description: 'Label for button that links to the edX StudioX course',
openEdxDemoCourseButtonLabel: {
id: 'authoring.footer.help.openEdxDemoCourse.button.label',
defaultMessage: 'Open edX Demo Course',
description: 'Label for button that links to the open edX demo course portal',
},
contactUsButtonLabel: {
id: 'authoring.footer.help.contactUs.button.label',

View File

@@ -1,8 +1,10 @@
import Footer, { EVENT_NAMES } from './components/Footer';
import messages from './i18n/index';
import StudioFooter from './components/studio-footer';
import FooterSlot from './plugin-slots/FooterSlot';
import StudioFooterSlot from './plugin-slots/StudioFooterSlot';
export default Footer;
export {
messages, EVENT_NAMES, StudioFooter,
messages, EVENT_NAMES, StudioFooter, FooterSlot, StudioFooterSlot,
};

View File

@@ -0,0 +1,51 @@
# FooterSlot
### Slot ID: `org.openedx.frontend.layout.footer.v1`
### Slot ID Aliases
* `footer_slot`
## Description
This slot is used to repace/modify/hide the entire footer.
## Example
The following `env.config.jsx` will replace the default footer.
![Screenshot of default footer](./images/default_footer.png)
with a simple custom footer
![Screenshot of custom footer](./images/custom_footer.png)
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.layout.footer.v1': {
plugins: [
{
// Hide the default footer
op: PLUGIN_OPERATIONS.Hide,
widgetId: 'default_contents',
},
{
// Insert a custom footer
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_footer',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🦶</h1>
),
},
},
]
}
},
}
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

@@ -0,0 +1,14 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import Footer from '../../components/Footer';
const FooterSlot = () => (
<PluginSlot
id="org.openedx.frontend.layout.footer.v1"
idAliases={['footer_slot', 'footer_plugin_slot']}
>
<Footer />
</PluginSlot>
);
export default FooterSlot;

View File

@@ -1,3 +1,5 @@
# `frontend-component-footer` Plugin Slots
* [`studio_footer_logo_slot`](./StudioFooterLogoSlot/)
* [`org.openedx.frontend.layout.footer.v1`](./FooterSlot/)
* [`org.openedx.frontend.layout.studio_footer.v1`](./StudioFooterSlot/)
* [`org.openedx.frontend.layout.studio_footer_logo.v1`](./StudioFooterLogoSlot/)

View File

@@ -0,0 +1,47 @@
# StudioFooterLogoSlot
### Slot ID: `org.openedx.frontend.layout.studio_footer_help_button.v1`
## Description
This slot is used to repace/modify/hide the help button to the studio footer.
## Examples
### Add your custom button.
The following `env.config.jsx` will add a custom help button to the studio footer.
![Screenshot of modified help button](./images/custom_help_button.png)
```jsx
import { PLUGIN_OPERATIONS, DIRECT_PLUGIN } from '@openedx/frontend-plugin-framework';
import {
Button,
} from '@openedx/paragon';
const config = {
pluginSlots: {
'org.openedx.frontend.layout.studio_footer_help_button.v1': {
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'studio_footer_helpbutton_addition',
type: DIRECT_PLUGIN,
priority: 40,
RenderWidget: ({isOpen, setIsOpen}) => {
return (
<Button className="button button1" onClick={() => setIsOpen(!isOpen)}>Custom Help Button</Button>
)
}
}
},
],
}
},
};
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import PropTypes from 'prop-types';
import HelpButton from '../../components/studio-footer/help-components/HelpButton';
const StudioFooterHelpButtonSlot = ({ isOpen, setIsOpen }) => (
<PluginSlot id="org.openedx.frontend.layout.studio_footer_help_button.v1" pluginProps={{ isOpen, setIsOpen }}>
<HelpButton isOpen={isOpen} setIsOpen={setIsOpen} />
</PluginSlot>
);
StudioFooterHelpButtonSlot.propTypes = {
setIsOpen: PropTypes.func.isRequired,
isOpen: PropTypes.bool.isRequired,
};
export default StudioFooterHelpButtonSlot;

View File

@@ -0,0 +1,50 @@
# StudioFooterLogoSlot
### Slot ID: `org.openedx.frontend.layout.studio_footer_help-content.v1`
## Description
This slot is used to repace/modify/hide the help content to the studio footer.
## Examples
### Add a custom help content.
The following `env.config.jsx` will add a custom help content to the studio footer.
![Screenshot of modified help content](./images/custom_help_content.png)
```jsx
import { PLUGIN_OPERATIONS, DIRECT_PLUGIN } from '@openedx/frontend-plugin-framework';
import {
Hyperlink,
} from '@openedx/paragon';
const config = {
pluginSlots: {
'org.openedx.frontend.layout.studio_footer_help-content.v1': {
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'studio_footer_helpcontent_addition',
type: DIRECT_PLUGIN,
priority: 40,
RenderWidget: () => {
return (
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', padding: '20px 5px' }}>
<p>Custom Help content</p>
<Hyperlink destination="https://example.com/">more help</Hyperlink>
</div>
)
}
}
},
],
}
},
};
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

View File

@@ -0,0 +1,13 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import HelpContent from '../../components/studio-footer/help-components/HelpContent';
const StudioFooterHelpContentSlot = () => (
<PluginSlot
id="org.openedx.frontend.layout.studio_footer_help-content.v1"
>
<HelpContent />
</PluginSlot>
);
export default StudioFooterHelpContentSlot;

View File

@@ -0,0 +1,53 @@
# StudioFooterLogoSlot
### Slot ID: `org.openedx.frontend.layout.studio_footer_help_section.v1`
## Description
This slot is used to repace/modify/hide the help section to the studio footer.
![Screenshot of modified help section](./images/custom_help_section.png)
## Examples
### Customize help section.
The following `env.config.jsx` will add a custom help section to the studio footer.
```jsx
import { PLUGIN_OPERATIONS, DIRECT_PLUGIN } from '@openedx/frontend-plugin-framework';
import {
Hyperlink,
Button,
} from '@openedx/paragon';
const config = {
pluginSlots: {
'org.openedx.frontend.layout.studio_footer_help_section.v1': {
keepDefault: false,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'studio_footer_helpsection_addition',
type: DIRECT_PLUGIN,
priority: 40,
RenderWidget: () => {
return (
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', padding: '20px 5px' }}>
<p>Custom Help Section custizable as needed </p>
<Button className="button button1">Go to home</Button>
<Hyperlink destination="https://example.com/">About</Hyperlink>
<Hyperlink destination="https://example.com/">Info</Hyperlink>
</div>
)
}
}
},
],
}
},
};
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import PropTypes from 'prop-types';
import {
Container,
} from '@openedx/paragon';
import HelpSection from '../../components/studio-footer/help-components/HelpSection';
const StudioFooterHelpSectionSlot = ({ containerProps }) => (
<PluginSlot id="org.openedx.frontend.layout.studio_footer_help_section.v1">
<HelpSection containerProps={containerProps} />
</PluginSlot>
);
StudioFooterHelpSectionSlot.propTypes = {
containerProps: PropTypes.shape(Container.propTypes),
};
export default StudioFooterHelpSectionSlot;

View File

@@ -1,6 +1,9 @@
# StudioFooterLogo Slot
# StudioFooterLogoSlot
### Slot ID: `studio_footer_logo_slot`
### Slot ID: `org.openedx.frontend.layout.studio_footer_logo.v1`
### Slot ID Aliases
* `studio_footer_logo_slot`
## Description
@@ -23,13 +26,13 @@ import {
const config = {
pluginSlots: {
studio_footer_logo_slot: {
'org.openedx.frontend.layout.studio_footer_logo.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'studio_footer_logo_slot',
id: 'studio_footer_logo_addition',
type: DIRECT_PLUGIN,
priority: 40,
RenderWidget: () => {

View File

@@ -3,7 +3,7 @@ import { PluginSlot } from '@openedx/frontend-plugin-framework';
import { Hyperlink, Image } from '@openedx/paragon';
const StudioFooterLogoSlot = () => (
<PluginSlot id="studio_footer_logo_slot">
<PluginSlot id="org.openedx.frontend.layout.studio_footer_logo.v1" idAliases={['studio_footer_logo_slot']}>
<Hyperlink destination="https://openedx.org" className="float-right">
<Image
width="120px"

View File

@@ -0,0 +1,51 @@
# StudioFooterSlot
### Slot ID: `org.openedx.frontend.layout.studio_footer.v1`
### Slot ID Aliases
* `studio_footer_slot`
## Description
This slot is used to repace/modify/hide the entire studio footer.
## Example
The following `env.config.jsx` will replace the default studio footer.
![Screenshot of default studio footer](./images/default_studio_footer.png)
with a simple custom footer
![Screenshot of custom footer](./images/custom_footer.png)
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.layout.studio_footer.v1': {
plugins: [
{
// Hide the default footer
op: PLUGIN_OPERATIONS.Hide,
widgetId: 'default_contents',
},
{
// Insert a custom footer
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_footer',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🦶</h1>
),
},
},
]
}
},
}
export default config;
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

@@ -0,0 +1,11 @@
import React from 'react';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import StudioFooter from '../../components/studio-footer';
const StudioFooterSlot = () => (
<PluginSlot id="org.openedx.frontend.layout.studio_footer.v1" idAliases={['studio_footer_slot']}>
<StudioFooter />
</PluginSlot>
);
export default StudioFooterSlot;