-
Notifications
You must be signed in to change notification settings - Fork 97
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Implement Footer using UI Plugin Approach
- Loading branch information
1 parent
1aa6b3e
commit c3466c4
Showing
11 changed files
with
621 additions
and
2,159 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import React from 'react'; | ||
import PropTypes from 'prop-types'; | ||
import { UISlot } from './UISlot'; | ||
|
||
export const defaultRender = (widget) => ( | ||
<React.Fragment key={widget.id}>{widget.content}</React.Fragment> | ||
); | ||
|
||
export const DefaultUISlot = (props) => ( | ||
<UISlot | ||
slotId={props.slotId} | ||
renderWidget={defaultRender} | ||
defaultContents={ | ||
props.children | ||
? [{ id: 'content', priority: 1, content: props.children }] | ||
: [] | ||
} | ||
/> | ||
); | ||
|
||
DefaultUISlot.propTypes = { | ||
slotId: PropTypes.string.isRequired, | ||
children: PropTypes.element.isRequired, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
/* eslint-disable no-restricted-syntax */ | ||
import React, { createContext, useContext, useMemo } from 'react'; | ||
import PropTypes from 'prop-types'; | ||
|
||
const UiChangeOperation = { | ||
INSERT: 'insert', | ||
HIDE: 'hide', | ||
MODIFY: 'modify', | ||
WRAP: 'wrap', | ||
}; | ||
|
||
export const UiPluginsContext = createContext([]); | ||
|
||
export const UISlot = (props) => { | ||
const enabledPlugins = useContext(UiPluginsContext); | ||
|
||
const allContents = useMemo(() => { | ||
const contents = [...(props.defaultContents ?? [])]; | ||
|
||
for (const p of enabledPlugins) { | ||
const changes = p.getUiSlotChanges(); | ||
for (const change of changes[props.slotId] ?? []) { | ||
if (change.op === UiChangeOperation.INSERT) { | ||
contents.push(change.widget); | ||
} else if (change.op === UiChangeOperation.HIDE) { | ||
const widget = contents.find((w) => w.id === change.widgetId); | ||
if (widget) { | ||
widget.hidden = true; | ||
} | ||
} else if (change.op === UiChangeOperation.MODIFY) { | ||
const widgetIdx = contents.findIndex((w) => w.id === change.widgetId); | ||
if (widgetIdx >= 0) { | ||
const widget = { ...contents[widgetIdx] }; | ||
contents[widgetIdx] = change.fn(widget); | ||
} | ||
} else if (change.op === UiChangeOperation.WRAP) { | ||
const widgetIdx = contents.findIndex((w) => w.id === change.widgetId); | ||
if (widgetIdx >= 0) { | ||
const newWidget = { wrappers: [], ...contents[widgetIdx] }; | ||
newWidget.wrappers.push(change.wrapper); | ||
contents[widgetIdx] = newWidget; | ||
} | ||
} else { | ||
throw new Error(`unknown plugin UI change operation: ${change.op}`); | ||
} | ||
} | ||
} | ||
|
||
// Sort first by priority, then by ID | ||
contents.sort( | ||
(a, b) => (a.priority - b.priority) * 10_000 + a.id.localeCompare(b.id), | ||
); | ||
return contents; | ||
}, [props.defaultContents, enabledPlugins, props.slotId]); | ||
|
||
return ( | ||
<> | ||
{allContents.map((c) => { | ||
if (c.hidden) { return null; } | ||
|
||
return ( | ||
c.wrappers | ||
? c.wrappers.reduce( | ||
(widget, wrapper) => React.createElement(wrapper, { widget, key: c.id }), | ||
props.renderWidget(c), | ||
) | ||
: props.renderWidget(c)); | ||
})} | ||
</> | ||
); | ||
}; | ||
|
||
UISlot.propTypes = { | ||
slotId: PropTypes.string.isRequired, | ||
defaultContents: PropTypes.arrayOf(PropTypes.shape({ | ||
id: PropTypes.string, | ||
priority: PropTypes.number, | ||
content: PropTypes.node, | ||
})), | ||
renderWidget: PropTypes.element.isRequired, | ||
}; | ||
|
||
UISlot.defaultProps = { | ||
defaultContents: [], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { UISlot, UiPluginsContext } from './UISlot'; | ||
|
||
export { DefaultUISlot, defaultRender } from './DefaultUISlot'; | ||
export default UISlot; | ||
export { UiPluginsContext }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import React from 'react'; | ||
import UISlot from '@edx/frontend-component-footer/src/plugin-template'; | ||
import useFooterLinks from './fetchFooterLinks'; | ||
|
||
const FooterLinks = () => { | ||
const { data, loading } = useFooterLinks(); | ||
|
||
console.log('------data----', data); | ||
|
||
if (loading) { return (<div>Loading...</div>); } | ||
|
||
return ( | ||
<div className="footer-links-container"> | ||
<UISlot | ||
slotId="footer-links" | ||
defaultContents={[ | ||
{ | ||
id: 'footer-navigation-links', | ||
priority: 1, | ||
content: { className: 'navigation-links', links: data.navigation_links }, | ||
}, | ||
{ | ||
id: 'footer-legal-links', | ||
priority: 5, | ||
content: { className: 'legal-links', links: data.legal_links }, | ||
}, | ||
]} | ||
renderWidget={(widget) => ( | ||
<div className={widget.content.className}> | ||
<ul className="nav-list"> | ||
{widget.content.links?.map((link) => ( | ||
<li className="nav-item"> | ||
<a className="nav-link" href={link.url}>{link.title}</a> | ||
</li> | ||
))} | ||
</ul> | ||
</div> | ||
)} | ||
/> | ||
</div> | ||
); | ||
}; | ||
|
||
export default FooterLinks; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import { useEffect, useState } from 'react'; | ||
import { camelCaseObject, getConfig } from '@edx/frontend-platform'; | ||
import { getHttpClient } from '@edx/frontend-platform/auth'; | ||
|
||
const useFooterLinks = () => { | ||
const [data, setData] = useState({}); | ||
const [loading, setLoading] = useState(false); | ||
|
||
useEffect(() => { | ||
const fetchData = async () => { | ||
const url = `${getConfig().LMS_BASE_URL}/api/branding/v1/footer`; | ||
setLoading(true); | ||
const { data: footerLinks } = await getHttpClient().get(url); | ||
const camelCasedData = camelCaseObject(footerLinks); | ||
setData(camelCasedData); | ||
setLoading(false); | ||
}; | ||
|
||
fetchData(); | ||
}, []); | ||
|
||
return { data, loading }; | ||
}; | ||
|
||
export default useFooterLinks; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import FooterLinks from './FooterLinks'; | ||
|
||
const FooterLinksPlugin = { | ||
id: 'footer-links', | ||
getUiSlotChanges() { | ||
return { | ||
footer: [ | ||
{ | ||
op: 'insert', | ||
widget: { | ||
id: 'footer-links', | ||
priority: 2, | ||
content: <FooterLinks />, | ||
}, | ||
}, | ||
], | ||
}; | ||
}, | ||
}; | ||
|
||
export default FooterLinksPlugin; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{ | ||
"name": "@openedx-plugins/footer-links", | ||
"version": "0.1.0", | ||
"description": "Footer Links configuration", | ||
"peerDependencies": { | ||
"@edly-io/indigo-frontend-component-footer": "*", | ||
"@edx/frontend-platform": "*", | ||
"@edx/paragon": "*", | ||
"prop-types": "*", | ||
"react": "*" | ||
}, | ||
"peerDependenciesMeta": { | ||
"@edly-io/indigo-frontend-component-footer": { | ||
"optional": true | ||
} | ||
} | ||
} |
Oops, something went wrong.