diff --git a/docs/developer-guide/mapstore-migration-guide.md b/docs/developer-guide/mapstore-migration-guide.md index c013eeb03d..d371132894 100644 --- a/docs/developer-guide/mapstore-migration-guide.md +++ b/docs/developer-guide/mapstore-migration-guide.md @@ -22,6 +22,37 @@ This is a list of things to check if you want to update from a previous version ## Migration from 2024.02.00 to 2025.01.00 +### Footer plugin configuration changes + +The Footer plugin has been refactored and some properties have been removed: + +- `cfg.logo` is not available anymore in favor of translation html snippet +- translation message identifier `home.footerDescription` is not used anymore in the footer by default + +It is possible to replicate the old footer structure for existing project that want to keep the homepage footer information as before with the following configurations: + +1. configure the new Footer plugin in `localConfig.json` as follow: + + ```js + { + "name": "Footer", + "cfg": { + "hideMenuItems": true, + "customFooter": true, + "customFooterMessageId": "home.footerDescription" // by default is using home.footerCustomHTML + } + } + ``` + +2. update the `home.footerDescription` translation by adding the desired html structure, eg: + + ```js + { + "home": { + "footerDescription": "" + } + } + ### HomeDescription plugin configuration changes The HomeDescription plugin has been refactored and a property has been removed: diff --git a/web/client/configs/localConfig.json b/web/client/configs/localConfig.json index 40af3fa1a6..981990842a 100644 --- a/web/client/configs/localConfig.json +++ b/web/client/configs/localConfig.json @@ -828,6 +828,7 @@ } }, { "name": "Footer"}, + { "name": "About" }, { "name": "Cookie", "cfg": { @@ -1094,7 +1095,7 @@ } } ], - "manager": ["Header", "Redirect", "Manager", "Home", "UserManager", "GroupManager", "Footer"], - "context-manager": ["Header", "Redirect", "Home", "ContextManager", "Footer"] + "manager": ["Header", "Redirect", "Manager", "Home", "UserManager", "GroupManager", "Footer", { "name": "About" }], + "context-manager": ["Header", "Redirect", "Home", "ContextManager", "Footer", { "name": "About" }] } } diff --git a/web/client/plugins/ResourcesCatalog/Footer.jsx b/web/client/plugins/ResourcesCatalog/Footer.jsx index c05361b243..5b713b415c 100644 --- a/web/client/plugins/ResourcesCatalog/Footer.jsx +++ b/web/client/plugins/ResourcesCatalog/Footer.jsx @@ -6,25 +6,207 @@ * LICENSE file in the root directory of this source tree. */ -import React from 'react'; +import React, { useRef } from 'react'; +import PropTypes from 'prop-types'; import { createPlugin } from "../../utils/PluginsUtils"; +import Menu from './components/Menu'; +import Button from './components/Button'; +import Spinner from './components/Spinner'; +import Icon from './components/Icon'; import HTML from '../../components/I18N/HTML'; -import Text from './components/Text'; +import Message from '../../components/I18N/Message'; +import FlexBox from './components/FlexBox'; +import usePluginItems from '../../hooks/usePluginItems'; +import { withResizeDetector } from 'react-resize-detector'; +function FooterMenuItem({ + className, + loading, + glyph, + iconType, + labelId, + onClick, + label +}) { + return ( +
  • + +
  • + ); +} -function Footer({ +FooterMenuItem.propTypes = { + className: PropTypes.string, + loading: PropTypes.bool, + glyph: PropTypes.string, + iconType: PropTypes.string, + labelId: PropTypes.string, + onClick: PropTypes.func +}; -}) { +FooterMenuItem.defaultProps = { + iconType: 'glyphicon', + onClick: () => { } +}; +/** + * This plugin shows the footer + * @memberof plugins + * @class + * @name Footer + * @prop {boolean} cfg.customFooter params that can be used to render a custom html to be used instead of the default one + * @prop {string} cfg.customFooterMessageId replace custom footer translations message identifier + * @prop {object[]} cfg.menuItems list of menu items objects + * @prop {boolean} cfg.hideMenuItems hide menu items menu + * @prop {object[]} items this property contains the items injected from the other plugins, + * using the `containers` option in the plugin that want to inject new menu items. + * ```javascript + * const MyMenuItemComponent = connect(selector, { onActivateTool })(({ + * component, // default component that provides a consistent UI (see BrandNavbarMenuItem in BrandNavbar plugin for props) + * variant, // one of style variant (primary, success, danger or warning) + * size, // button size + * className, // custom class name provided by configuration + * onActivateTool, // example of a custom connected action + * }) => { + * const ItemComponent = component; + * return ( + * onActivateTool()} + * /> + * ); + * }); + * createPlugin( + * 'MyPlugin', + * { + * containers: { + * Footer: { + * name: "TOOLNAME", // a name for the current tool. + * target: 'menu', + * Component: MyMenuItemComponent + * }, + * // ... + * ``` + * @example + * { + * "name": "Footer", + * "cfg": { + * "menuItems": [ + * { + * "type": "link", + * "href": "/my-link", + * "target": "blank", + * "glyph": "heart", + * "labelId": "myMessageId", + * "variant": "default" + * }, + * { + * "type": "logo", + * "href": "/my-link", + * "target": "blank", + * "src": "/my-image.jpg", + * "style": {} + * }, + * { + * "type": "button", + * "href": "/my-link", + * "target": "blank", + * "glyph": "heart", + * "iconType": "glyphicon", + * "tooltipId": "myMessageId", + * "variant": "default", + * "square": true + * }, + * { + * "type": "divider" + * }, + * { + * "type": "message", + * "labelId": "myTranslationMessageId" + * } + * ] + * } + * } + */ +function Footer({ + menuItems: menuItemsProp, + hideMenuItems, + items, + customFooter, + customFooterMessageId +}, context) { + const { loadedPlugins } = context; + const ref = useRef(); + const configuredItems = usePluginItems({ items, loadedPlugins }); + const pluginMenuItems = configuredItems.filter(({ target }) => target === 'menu').map(item => ({ ...item, type: 'plugin' })); + const menuItems = [ + ...menuItemsProp.map((menuItem, idx) => ({ ...menuItem, position: idx + 1 })), + ...pluginMenuItems + ].sort((a, b) => a.position - b.position); return ( -
    - - - -
    + <> + {customFooter ? : null} + {!hideMenuItems || menuItems.length === 0 ? <> +
    + + + + : false} + ); } +Footer.propTypes = { + menuItems: PropTypes.array, + hideMenuItems: PropTypes.bool, + items: PropTypes.array, + customFooter: PropTypes.bool, + customFooterMessageId: PropTypes.string +}; + +Footer.contextTypes = { + loadedPlugins: PropTypes.object +}; + +Footer.defaultProps = { + menuItems: [ + { + type: 'link', + href: "https://docs.mapstore.geosolutionsgroup.com/", + target: 'blank', + glyph: 'book', + labelId: 'resourcesCatalog.documentation' + }, + { + type: 'link', + href: 'https://github.com/geosolutions-it/MapStore2', + target: 'blank', + label: 'GitHub', + glyph: 'github' + } + ], + customFooter: false, + customFooterMessageId: 'home.footerCustomHTML' +}; + export default createPlugin('Footer', { - component: Footer + component: withResizeDetector(Footer) }); diff --git a/web/client/plugins/ResourcesCatalog/ResourceDetails.jsx b/web/client/plugins/ResourcesCatalog/ResourceDetails.jsx index 57aea54329..5cd1df9f22 100644 --- a/web/client/plugins/ResourcesCatalog/ResourceDetails.jsx +++ b/web/client/plugins/ResourcesCatalog/ResourceDetails.jsx @@ -111,7 +111,7 @@ function ResourceDetails({ targetSelector, headerNodeSelector = '#ms-brand-navbar', navbarNodeSelector = '', - footerNodeSelector = '', + footerNodeSelector = '#ms-footer', width, height, show, diff --git a/web/client/plugins/ResourcesCatalog/ResourcesFiltersForm.jsx b/web/client/plugins/ResourcesCatalog/ResourcesFiltersForm.jsx index 81e3e55601..c519b479ea 100644 --- a/web/client/plugins/ResourcesCatalog/ResourcesFiltersForm.jsx +++ b/web/client/plugins/ResourcesCatalog/ResourcesFiltersForm.jsx @@ -120,7 +120,7 @@ function ResourcesFiltersForm({ targetSelector, headerNodeSelector = '#ms-brand-navbar', navbarNodeSelector = '', - footerNodeSelector = '', + footerNodeSelector = '#ms-footer', width, height, user diff --git a/web/client/plugins/ResourcesCatalog/components/Menu.jsx b/web/client/plugins/ResourcesCatalog/components/Menu.jsx index b842d8fd28..8ead46f44a 100644 --- a/web/client/plugins/ResourcesCatalog/components/Menu.jsx +++ b/web/client/plugins/ResourcesCatalog/components/Menu.jsx @@ -11,32 +11,21 @@ import PropTypes from 'prop-types'; import MenuItem from './MenuItem'; import FlexBox from './FlexBox'; -/** -* @module components/Menu -*/ - /** * Menu component * @name Menu - * @prop {array} items list of menu item - * @prop {string} containerClass css class of list container - * @prop {string} childrenClass css class of item in list - * @prop {string} query string to build the query url in case of link item - * @prop {function} formatHref function to format the href in case of link item - * @example - * - * + * @prop {object[]} items list of menu item + * @prop {string} className custom class name + * @prop {string} size button size, one of `xs`, `sm`, `md` or `xl` + * @prop {bool} alignRight align the dropdown menu to the right + * @prop {string} variant style for the button, one of `undefined`, `default` or `primary` + * @prop {any} menuItemComponent a default component to be passed as a prop to a custom `item.Component` */ const Menu = forwardRef(({ items, - containerClass, - childrenClass, - query, - formatHref, size, alignRight, variant, - resourceName, className, menuItemComponent, ...props @@ -50,7 +39,6 @@ const Menu = forwardRef(({ component="ul" ref={ref} className={className} - classNames={[containerClass]} > {items .map((item, idx) => { @@ -58,15 +46,9 @@ const Menu = forwardRef(({ ); @@ -76,21 +58,16 @@ const Menu = forwardRef(({ }); Menu.propTypes = { - items: PropTypes.array.isRequired, - containerClass: PropTypes.string, - childrenClass: PropTypes.string, - query: PropTypes.object, - formatHref: PropTypes.func - + items: PropTypes.array, + size: PropTypes.string, + alignRight: PropTypes.bool, + variant: PropTypes.string, + className: PropTypes.string, + menuItemComponent: PropTypes.any }; Menu.defaultProps = { - items: [], - query: {}, - user: undefined, - formatHref: () => '#', - containerClass: '' + items: [] }; - export default Menu; diff --git a/web/client/plugins/ResourcesCatalog/components/MenuDropdownList.jsx b/web/client/plugins/ResourcesCatalog/components/MenuDropdownList.jsx deleted file mode 100644 index 140d2662b0..0000000000 --- a/web/client/plugins/ResourcesCatalog/components/MenuDropdownList.jsx +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright 2024, GeoSolutions Sas. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -import React from 'react'; -import { createPortal } from 'react-dom'; -import PropTypes from 'prop-types'; -import Message from '../../../components/I18N/Message'; -import NavLink from './MenuNavLink'; -import Icon from './Icon'; -import { Dropdown, MenuItem, Badge } from 'react-bootstrap'; - -const isValidBadgeValue = (badge) => !!badge || badge === 0; - -const itemsList = (items) => (items && items.map((item, idx) => { - - const { labelId, href, badge, target, type, Component, className } = item; - - if (type === 'plugin' && Component) { - return (
  • ); - } - - return ( - {labelId && } - { isValidBadgeValue(badge) && {badge}} - - ); -} )); - -/** - * DropdownList component - * @name DropdownList - * @memberof components.Menu.DropdownList - * @prop {number} id to apply to toogle - * @prop {array} items list od items of Dropdown - * @prop {string} label label to apply to toogle - * @prop {string} labelId alternative to label - * @prop {string} labelId alternative to labe - * @prop {object} toggleStyle inline style to apply to toogle comp - * @prop {string} toggleImage image to apply to toogle comp - * @prop {string} toggleIcon icon to apply to toogle comp - * @prop {string} dropdownClass the css class to apply to the comp - * @prop {number} tabIndex define navigation order - * @prop {boolean} noCaret hide/show caret icon on the dropdown - * @prop {number} badgeValue to apply the value to the item in list - * @prop {node} containerNode the node to append the child element into a DOM - * @example - * - * - */ - - -const MenuDropdownList = ({ - id, - items = [], - label, - labelId, - toggleStyle, - toggleImage, - toggleIcon, - dropdownClass, - tabIndex, - badgeValue, - containerNode, - size, - noCaret, - alignRight, - variant, - responsive -}) => { - - const dropdownItems = items - .map((itm, idx) => { - - if (itm.type === 'plugin' && itm.Component) { - return (
  • ); - } - if (itm.type === 'divider') { - return ; - } - return ( - - - {itm.labelId && || itm.label} - {isValidBadgeValue(itm.badge) && {itm.badge}} - - - {itm?.items &&
    - {itemsList(itm?.items)} -
    } -
    - ); - }); - - const DropdownToggle = ( - - {toggleImage - ? - : undefined - } - { - toggleIcon ? - : undefined - } - { - (labelId && !responsive) && - || label - } - { - (labelId && responsive) && -
    - - -
    - } - {isValidBadgeValue(badgeValue) && {badgeValue}} -
    - - ); - - - return ( - - {DropdownToggle} - {containerNode - ? createPortal( - {dropdownItems} - , containerNode.parentNode) - : - {dropdownItems} - } - - ); - -}; - -MenuDropdownList.propTypes = { - items: PropTypes.array.isRequired, - label: PropTypes.string, - labelId: PropTypes.string, - toggleStyle: PropTypes.object, - toggleImage: PropTypes.string, - state: PropTypes.object, - noCaret: PropTypes.bool, - dropdownClass: PropTypes.string, - tabIndex: PropTypes.number, - containerNode: PropTypes.element - -}; - -export default MenuDropdownList; diff --git a/web/client/plugins/ResourcesCatalog/components/MenuItem.jsx b/web/client/plugins/ResourcesCatalog/components/MenuItem.jsx index 8e53aacd18..dc911850aa 100644 --- a/web/client/plugins/ResourcesCatalog/components/MenuItem.jsx +++ b/web/client/plugins/ResourcesCatalog/components/MenuItem.jsx @@ -7,42 +7,93 @@ */ import React from 'react'; +import { createPortal } from 'react-dom'; import PropTypes from 'prop-types'; -import castArray from 'lodash/castArray'; -import { Badge } from 'react-bootstrap'; import Message from '../../../components/I18N/Message'; - -import DropdownList from './MenuDropdownList'; +import HTML from '../../../components/I18N/HTML'; +import { Dropdown, MenuItem as RBMenuItem } from 'react-bootstrap'; import MenuNavLink from './MenuNavLink'; import Icon from './Icon'; import Button from './Button'; -const isValidBadgeValue = (badge) => !!badge || badge === 0; +/** + * List of menu items for the `dropdown` type + * @name DropdownMenuItems + * @prop {object[]} items list of items + */ +const DropdownMenuItems = ({ + items +}) => { + return <> + {items + .map((itm, idx) => { + if (itm.Component) { + return (); + } + if (itm.type === 'divider') { + return ; + } + const labelNode = itm.labelId ? : itm.label; + return ( + + + {itm.glyph ? : null} + {itm.glyph && labelNode ? ' ' : null} + {labelNode} + + + {itm?.items &&
    + +
    } +
    + ); + })} + ; +}; /** * Menu item component * @name MenuItem - * @memberof components.Menu.MenuItem * @prop {object} item the item menu - * @prop {object} menuItemsProps contains pros to apply to items, to manage single permissions, build href and query url + * @prop {string} item.type menu type, one of `dropdown`, `link`, `logo`, `button`, `divider`, `placeholder` and `message` + * @prop {string} item.Component custom component for the menu item, it has priority over the `type` + * @prop {object[]} item.items list of items (`dropdown` type) + * @prop {string} item.id menu item identifier (`dropdown` type) + * @prop {string} item.noCaret hide the caret (`dropdown` type) + * @prop {string} item.label label rendered as menu item content + * @prop {string} item.labelId a message id rendered as menu item content, it has priority over label + * @prop {string} item.href a url link for the menu item + * @prop {string} item.target link html target attribute + * @prop {string} item.style custom inline style + * @prop {string} item.className custom class name + * @prop {string} item.glyph glyph name + * @prop {string} item.iconType glyph types (see `Icon` component) + * @prop {string} item.square square style for button + * @prop {string} item.tooltipId tooltip message id + * @prop {string} item.src image source * @prop {node} containerNode the node to append the child element into a DOM * @prop {number} tabIndex define navigation order - * @prop {boolean} draggable is element is draggable - * @prop {function} classItem class to apply to the Item - * @example - * - * + * @prop {string} size button size, one of `xs`, `sm`, `md` or `xl` + * @prop {bool} alignRight align the dropdown menu to the right + * @prop {string} variant style for the button, one of `undefined`, `default` or `primary` + * @prop {any} menuItemComponent a default component to be passed as a prop to a custom `item.Component` */ +const MenuItem = ({ + item, + containerNode, + tabIndex, + size, + alignRight, + variant, + menuItemComponent +}) => { -const MenuItem = ({ item, menuItemsProps, containerNode, tabIndex, classItem = '', size, alignRight, variant, resourceName, menuItemComponent }) => { - - const { formatHref, query } = menuItemsProps || {}; const { id, type, @@ -51,12 +102,9 @@ const MenuItem = ({ item, menuItemsProps, containerNode, tabIndex, classItem = ' items = [], href, style, - badge = '', - image, Component, target, className, - responsive, noCaret, glyph, iconType, @@ -64,38 +112,52 @@ const MenuItem = ({ item, menuItemsProps, containerNode, tabIndex, classItem = ' tooltipId, src } = item || {}; - const btnClassName = `btn${variant && ` btn-${variant}` || ''}${size && ` btn-${size}` || ''}${className ? ` ${className}` : ''} _border-transparent`; - - const labelNode = labelId ? : label; - const badgeValue = badge; - if (type === 'dropdown') { - return (
  • ); + if (Component) { + return ; } - if ((type === 'custom' || type === 'plugin') && Component) { - return ; + const labelNode = labelId ? : label; + + if (type === 'dropdown') { + return (
  • + + + {src + ? + : ( + <> + {glyph ? : null} + {glyph && labelNode ? ' ' : null} + {labelNode} + + )} + + {containerNode + ? createPortal( + + , containerNode.parentNode) + : + + } + +
  • ); } if (type === 'link') { return (
  • - + {glyph ? : null} {glyph && labelNode ? ' ' : null} {labelNode} @@ -140,36 +202,22 @@ const MenuItem = ({ item, menuItemsProps, containerNode, tabIndex, classItem = ' return
  • ; } - if (type === 'filter') { - const active = castArray(query.f || []).find(value => value === item.id); - return (
  • - - {glyph ? : null} - {glyph && labelNode ? ' ' : null} - {labelNode} - {isValidBadgeValue(badgeValue) && {badgeValue}} - -
  • ); + if (type === 'message' && labelId) { + return
  • ; } + return null; }; MenuItem.propTypes = { - item: PropTypes.object.isRequired, - menuItemsProps: PropTypes.object.isRequired, + item: PropTypes.object, containerNode: PropTypes.element, tabIndex: PropTypes.number, draggable: PropTypes.bool, - classItem: PropTypes.string - + size: PropTypes.string, + alignRight: PropTypes.bool, + variant: PropTypes.string, + menuItemComponent: PropTypes.any }; export default MenuItem; diff --git a/web/client/plugins/ResourcesCatalog/components/__tests__/MenuDropdownList-test.jsx b/web/client/plugins/ResourcesCatalog/components/__tests__/MenuDropdownList-test.jsx deleted file mode 100644 index 332926fbf1..0000000000 --- a/web/client/plugins/ResourcesCatalog/components/__tests__/MenuDropdownList-test.jsx +++ /dev/null @@ -1,30 +0,0 @@ - -/* - * Copyright 2025, GeoSolutions Sas. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -import React from 'react'; -import ReactDOM from 'react-dom'; -import expect from 'expect'; -import MenuDropdownList from '../MenuDropdownList'; - -describe('MenuDropdownList component', () => { - beforeEach((done) => { - document.body.innerHTML = '
    '; - setTimeout(done); - }); - afterEach((done) => { - ReactDOM.unmountComponentAtNode(document.getElementById('container')); - document.body.innerHTML = ''; - setTimeout(done); - }); - it('should render with default', () => { - ReactDOM.render(, document.getElementById('container')); - const dropdown = document.querySelector('.dropdown'); - expect(dropdown).toBeTruthy(); - }); -}); diff --git a/web/client/plugins/ResourcesCatalog/components/__tests__/MenuItem-test.jsx b/web/client/plugins/ResourcesCatalog/components/__tests__/MenuItem-test.jsx index 1ecbe0830b..475000df72 100644 --- a/web/client/plugins/ResourcesCatalog/components/__tests__/MenuItem-test.jsx +++ b/web/client/plugins/ResourcesCatalog/components/__tests__/MenuItem-test.jsx @@ -27,4 +27,167 @@ describe('MenuItem component', () => { const container = document.getElementById('container'); expect(container.children.length).toBe(0); }); + it('should render a message', () => { + ReactDOM.render(, document.getElementById('container')); + const li = document.querySelector('li'); + expect(li.innerText).toBe('myMessageId'); + }); + it('should render a placeholder', () => { + ReactDOM.render(, document.getElementById('container')); + const li = document.querySelector('li'); + expect(li.innerHTML).toBe(''); + }); + it('should render a divider', () => { + ReactDOM.render(, document.getElementById('container')); + const divider = document.querySelector('.ms-menu-divider'); + expect(divider).toBeTruthy(); + }); + it('should render a square button', () => { + ReactDOM.render(, document.getElementById('container')); + const button = document.querySelector('.square-button-md'); + expect(button).toBeTruthy(); + expect(button.getAttribute('class')).toBe('square-button-md _border-transparent btn btn-default'); + expect(button.getAttribute('href')).toBe('/'); + expect(button.getAttribute('target')).toBe('_blank'); + expect(button.innerHTML).toBe(''); + }); + + it('should render a button', () => { + ReactDOM.render(, document.getElementById('container')); + const button = document.querySelector('.btn'); + expect(button).toBeTruthy(); + expect(button.getAttribute('class')).toBe(' _border-transparent btn btn-default'); + expect(button.getAttribute('href')).toBe('/'); + expect(button.getAttribute('target')).toBe('_blank'); + expect(button.innerHTML).toBe(' labelId'); + }); + + it('should render a logo', () => { + ReactDOM.render(, document.getElementById('container')); + const img = document.querySelector('img'); + expect(img).toBeTruthy(); + expect(img.getAttribute('src')).toBe('img'); + const link = document.querySelector('a'); + expect(link).toBeTruthy(); + expect(link.getAttribute('href')).toBe('/'); + expect(link.getAttribute('target')).toBe('_blank'); + }); + + it('should render a link', () => { + ReactDOM.render(, document.getElementById('container')); + const link = document.querySelector('a'); + expect(link).toBeTruthy(); + expect(link.getAttribute('href')).toBe('/'); + expect(link.getAttribute('target')).toBe('_blank'); + expect(link.innerHTML).toBe(' labelId'); + }); + + it('should render a dropdown', () => { + const Component = () =>
  • Custom Component
  • ; + ReactDOM.render(, document.getElementById('container')); + const dropdown = document.querySelector('.dropdown-01'); + expect(dropdown).toBeTruthy(); + + const dropdownToggle = document.querySelector('.dropdown-toggle'); + expect(dropdownToggle).toBeTruthy(); + expect(dropdownToggle.innerHTML).toBe(' labelId '); + + const dropdownMenuItems = document.querySelectorAll('.dropdown-menu li'); + expect(dropdownMenuItems.length).toBe(3); + + expect(dropdownMenuItems[0].innerText).toBe('Custom Component'); + expect(dropdownMenuItems[1].getAttribute('class')).toBe('divider'); + expect(dropdownMenuItems[2].innerText).toBe('labelIdItem'); + }); + + it('should render a custom component', () => { + const Component = ({ component }) => { + const ItemComponent = component; + return ; + }; + const CustomMenuItemComponent = ({ label }) =>
  • {label}
  • ; + ReactDOM.render(, document.getElementById('container')); + const customComponent = document.querySelector('.custom-menu-item'); + expect(customComponent).toBeTruthy(); + expect(customComponent.innerText).toBe('Custom Component'); + }); }); diff --git a/web/client/plugins/ResourcesCatalog/containers/ResourcesGrid.jsx b/web/client/plugins/ResourcesCatalog/containers/ResourcesGrid.jsx index 4896e6e543..90b720b23e 100644 --- a/web/client/plugins/ResourcesCatalog/containers/ResourcesGrid.jsx +++ b/web/client/plugins/ResourcesCatalog/containers/ResourcesGrid.jsx @@ -75,7 +75,7 @@ function ResourcesGrid({ monitoredState, headerNodeSelector = '#ms-brand-navbar', navbarNodeSelector = '', - footerNodeSelector = '', + footerNodeSelector = '#ms-footer', width, height, error, diff --git a/web/client/plugins/ScrollTop.jsx b/web/client/plugins/ScrollTop.jsx index c02780e4c5..1dad74b425 100644 --- a/web/client/plugins/ScrollTop.jsx +++ b/web/client/plugins/ScrollTop.jsx @@ -33,7 +33,7 @@ class ScrollTop extends React.Component { showUnder: 200, btnClassName: 'square-button shadow-far', style: { - zIndex: 10, + zIndex: 5000, position: 'fixed', bottom: 12, right: 8, diff --git a/web/client/product/plugins/About.jsx b/web/client/product/plugins/About.jsx index 5b4e4394aa..70157d21e3 100644 --- a/web/client/product/plugins/About.jsx +++ b/web/client/product/plugins/About.jsx @@ -34,6 +34,17 @@ const About = connect((state) => ({ onClose: toggleControl.bind(null, 'about', null) })(AboutComp); +const AboutFooterButton = connect(() => ({}), { onClick: toggleControl.bind(null, 'about', null) })(({ component, onClick }) => { + const Component = component; + return ( + onClick()} + /> + ); +}); + /** * Plugin for the "About" window in mapstore. @@ -74,6 +85,13 @@ export default { priority: 1, doNotHide: true, toggle: true + }, + Footer: { + target: 'menu', + doNotHide: true, + priority: 3, + position: 0, + Component: AboutFooterButton } }), reducers: { diff --git a/web/client/product/plugins/Footer.jsx b/web/client/product/plugins/Footer.jsx deleted file mode 100644 index 43cc03b039..0000000000 --- a/web/client/product/plugins/Footer.jsx +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2017, GeoSolutions Sas. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -import React from 'react'; -import PropTypes from 'prop-types'; -import { Grid, Row, Col } from 'react-bootstrap'; -import src from "./attribution/geosolutions-brand.png"; -import HTML from '../../components/I18N/HTML'; -import {createPlugin} from "../../utils/PluginsUtils"; - -/** - * Footer plugin, section of the homepage. - * description of footer can be overridden by - * `home.footerDescription` message id in the translations - * @deprecated - * @prop {boolean} cfg.customFooter params that can be used to render a custom html to be used instead of the default one - * @prop {object} cfg.logo logo data to change image and href, set to null to hide the logo - * @prop {string} cfg.logo.src source of the logo - * @prop {number|string} cfg.logo.width width of the logo image - * @prop {number|string} cfg.logo.height height of the logo image - * @prop {string} cfg.logo.title title of the logo image - * @prop {string} cfg.logo.alt alternative text of the logo image - * @memberof plugins - * @class - * @name Footer - */ - -class Footer extends React.Component { - - static propTypes = { - customFooter: PropTypes.bool, - logo: PropTypes.object - }; - - static defaultProps = { - customFooter: false, - logo: { - src, - width: 140, - height: 'auto', - href: 'https://www.geosolutionsgroup.com/', - title: 'GeoSolutions', - alt: 'GeoSolutions' - } - }; - - render() { - const { href, ...logo } = this.props.logo || {}; - const image = ( - {logo.alt - ); - return this.props.customFooter ? : ( - - {logo && logo.src && - -
    - {href ? - {image} - : image} -
    - -
    } - - - - - -
    - ); - } -} - -const FooterPlugin = createPlugin('Footer', { - component: Footer -}); - -export default FooterPlugin; diff --git a/web/client/themes/default/less/resources-catalog/_footer.less b/web/client/themes/default/less/resources-catalog/_footer.less new file mode 100644 index 0000000000..0f12ab72e9 --- /dev/null +++ b/web/client/themes/default/less/resources-catalog/_footer.less @@ -0,0 +1,39 @@ +// ************** +// Theme +// ************** + +#ms-components-theme(@theme-vars) { + #ms-footer { + .color-var(@theme-vars[footer-color]); + .background-color-var(@theme-vars[footer-bg]); + .border-top-color-var(@theme-vars[main-border-color]); + .btn { + border: transparent; + background: transparent; + } + a { + .color-var(@theme-vars[footer-link-color]); + &:hover { + .color-var(@theme-vars[footer-link-hover-color]); + } + } + } +} + +// ************** +// Layout +// ************** + +#ms-footer { + position: fixed; + width: 100%; + bottom: 0; + left: 0; + font-size: 0.75rem; + border-top: 1px solid transparent; + z-index: 2000; + .nav-link, + .btn { + font-size: 0.75rem; + } +} diff --git a/web/client/themes/default/less/resources-catalog/index.less b/web/client/themes/default/less/resources-catalog/index.less index 26f1f61271..d957fc88ce 100644 --- a/web/client/themes/default/less/resources-catalog/index.less +++ b/web/client/themes/default/less/resources-catalog/index.less @@ -4,6 +4,7 @@ @import "_base.less"; @import "_brand-navbar.less"; @import "_details-panel.less"; +@import "_footer.less"; @import "_home-description.less"; @import "_permissions.less"; @import "_resource-card.less"; diff --git a/web/client/themes/default/ms-variables.less b/web/client/themes/default/ms-variables.less index 8ce8abc0e5..a476e63cca 100644 --- a/web/client/themes/default/ms-variables.less +++ b/web/client/themes/default/ms-variables.less @@ -63,6 +63,12 @@ @ms-selected-hover-bg: rgba(@ms-focus-color, 0.1); // #ecf3ff; @ms-placeholder-color: lighten(@ms-main-color, 30%); +// footer theme colors +@ms-footer-color: @ms-main-variant-color; +@ms-footer-bg: @ms-main-variant-bg; +@ms-footer-link-color: @ms-main-variant-color; +@ms-footer-link-hover-color: @ms-link-hover-color; + @ms-jumbotron-color: @ms-main-color; @ms-jumbotron-bg: rgba(@ms-main-bg, 0.75); @@ -128,6 +134,13 @@ selected-bg: --ms-selected-bg, @ms-selected-bg; selected-hover-bg: --ms-selected-hover-bg, @ms-selected-hover-bg; + // ************** + // text color for footer + footer-color: --ms-footer-color, @ms-footer-color; + footer-bg: --ms-footer-bg, @ms-footer-bg; + footer-link-color: --ms-footer-link-color, @ms-footer-link-color; + footer-link-hover-color: --ms-footer-link-hover-color, @ms-footer-link-hover-color; + // ************** // text color for placeholders placeholder-color: --ms-placeholder-color, @ms-placeholder-color; diff --git a/web/client/translations/data.da-DK.json b/web/client/translations/data.da-DK.json index 68fcde53c9..43a2955db5 100644 --- a/web/client/translations/data.da-DK.json +++ b/web/client/translations/data.da-DK.json @@ -336,8 +336,7 @@ "Applications": "Applications", "Examples": "Examples", "LinkedinGroup": "Mapstore Linkedin Group", - "scrollTop": "Scroll to the top of the page", - "footerDescription": "GeoSolutions sales@geosolutionsgroup.com" + "scrollTop": "Scroll to the top of the page" }, "cookiesPolicyNotification": { "title": "This website uses cookies", @@ -3964,7 +3963,8 @@ "errorDeletingTag": "It is not possible to delete the tag", "removeFromFavorites": "Remove from favorites", "addToFavorites": "Add to favorites", - "favorites": "Favorites" + "favorites": "Favorites", + "documentation": "Documentation" } } } diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json index 43599d21c5..5a3717f40c 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -386,7 +386,6 @@ "Examples": "Beispiele", "LinkedinGroup": "Mapstore Linkedin Gruppe", "scrollTop": "An den Anfang der Seite scrollen", - "footerDescription": "GeoSolutions sales@geosolutionsgroup.com", "footerCustomHTML": "

    Bitte fügen Sie benutzerdefiniertes HTML in die Übersetzungen ein, um eine benutzerdefinierte Fußzeile darzustellen

    ", "examples":{ "viewer":{ @@ -4383,7 +4382,8 @@ "errorDeletingTag": "Es ist nicht möglich, das Tag zu löschen", "removeFromFavorites": "Aus Favoriten entfernen", "addToFavorites": "Zu Favoriten hinzufügen", - "favorites": "Favoriten" + "favorites": "Favoriten", + "documentation": "Dokumentation" } } } diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json index 921039edd4..a3c7c63f38 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -386,7 +386,6 @@ "Examples": "Examples", "LinkedinGroup": "Mapstore Linkedin Group", "scrollTop": "Scroll to the top of the page", - "footerDescription": "GeoSolutions sales@geosolutionsgroup.com", "footerCustomHTML": "

    Please insert custom HTML into the translations to render a custom footer

    " }, "cookiesPolicyNotification": { @@ -4356,7 +4355,8 @@ "errorDeletingTag": "It is not possible to delete the tag", "removeFromFavorites": "Remove from favorites", "addToFavorites": "Add to favorites", - "favorites": "Favorites" + "favorites": "Favorites", + "documentation": "Documentation" } } } diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json index 1af52bf1d0..795b740d8d 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -386,7 +386,6 @@ "Examples": "Ejemplos", "LinkedinGroup": "Grupo Linkedin de Mapstore", "scrollTop": "Volver al principio de la página", - "footerDescription": "GeoSolutions sales@geosolutionsgroup.com", "footerCustomHTML": "

    Inserte HTML personalizado en las traducciones para generar un pie de página personalizado.

    " }, "cookiesPolicyNotification": { @@ -4345,7 +4344,8 @@ "errorDeletingTag": "No es posible eliminar la etiqueta", "removeFromFavorites": "Eliminar de favoritos", "addToFavorites": "Añadir a favoritos", - "favorites": "Favoritos" + "favorites": "Favoritos", + "documentation": "Documentación" } } } diff --git a/web/client/translations/data.fi-FI.json b/web/client/translations/data.fi-FI.json index 066f6e43f3..800761e848 100644 --- a/web/client/translations/data.fi-FI.json +++ b/web/client/translations/data.fi-FI.json @@ -194,7 +194,6 @@ "Examples": "Esimerkit", "LinkedinGroup": "Mapstore Linkedin Group", "scrollTop": "Vieritä sivun yläosaan", - "footerDescription": "GeoSolutions sales@geosolutionsgroup.com", "examples": { "viewer": { "html": "

    Viewer

    Simple Viewer

    " @@ -2788,7 +2787,8 @@ "errorDeletingTag": "It is not possible to delete the tag", "removeFromFavorites": "Remove from favorites", "addToFavorites": "Add to favorites", - "favorites": "Favorites" + "favorites": "Favorites", + "documentation": "Documentation" } } } diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json index 0ad1644fea..08d16d4eef 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -386,7 +386,6 @@ "Examples": "Exemples", "LinkedinGroup": "Groupe Linkedin Mapstore", "scrollTop": "Retour en haut de la page", - "footerDescription": "GeoSolutions sales@geosolutionsgroup.com", "footerCustomHTML": "

    Veuillez insérer du HTML personnalisé dans les traductions pour afficher un pied de page personnalisé

    " }, "cookiesPolicyNotification": { @@ -4345,7 +4344,8 @@ "errorDeletingTag": "Il n'est pas possible de supprimer la balise", "removeFromFavorites": "Supprimer des favoris", "addToFavorites": "Ajouter aux favoris", - "favorites": "Favoris" + "favorites": "Favoris", + "documentation": "Documentation" } } } diff --git a/web/client/translations/data.hr-HR.json b/web/client/translations/data.hr-HR.json index d738615a8a..0bb0a1be59 100644 --- a/web/client/translations/data.hr-HR.json +++ b/web/client/translations/data.hr-HR.json @@ -193,7 +193,6 @@ "Examples": "Primjeri", "LinkedinGroup": "Mapstore Linkedin grupa", "scrollTop": "Pomakni gore na vrh stranice", - "footerDescription": "GeoSolutions sales@geosolutionsgroup.com", "examples":{ "viewer":{ "html":"

    Preglednik

    Jednostavni preglednik

    " @@ -2186,7 +2185,8 @@ "errorDeletingTag": "It is not possible to delete the tag", "removeFromFavorites": "Remove from favorites", "addToFavorites": "Add to favorites", - "favorites": "Favorites" + "favorites": "Favorites", + "documentation": "Documentation" } } } diff --git a/web/client/translations/data.is-IS.json b/web/client/translations/data.is-IS.json index ee08572109..683d18bee1 100644 --- a/web/client/translations/data.is-IS.json +++ b/web/client/translations/data.is-IS.json @@ -340,8 +340,7 @@ "Applications": "Applications", "Examples": "Examples", "LinkedinGroup": "Mapstore Linkedin Group", - "scrollTop": "Scroll to the top of the page", - "footerDescription": "GeoSolutions sales@geosolutionsgroup.com" + "scrollTop": "Scroll to the top of the page" }, "cookiesPolicyNotification": { "title": "This website uses cookies", @@ -3997,7 +3996,8 @@ "errorDeletingTag": "It is not possible to delete the tag", "removeFromFavorites": "Remove from favorites", "addToFavorites": "Add to favorites", - "favorites": "Favorites" + "favorites": "Favorites", + "documentation": "Documentation" } } } diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json index 2b98751224..52c1890a3a 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -384,7 +384,6 @@ "Examples": "Esempi", "LinkedinGroup": "Gruppo Linkedin Mapstore", "scrollTop": "Torna all'inizio della pagina", - "footerDescription": "GeoSolutions sales@geosolutionsgroup.com", "footerCustomHTML": "

    Per favore, inserisci un HTML custom nelle traduzioni per renderizzare un footer personalizzato

    " }, "cookiesPolicyNotification": { @@ -4343,7 +4342,8 @@ "errorDeletingTag": "Impossibile eliminare il tag", "removeFromFavorites": "Rimuovi dai preferiti", "addToFavorites": "Aggiungi ai preferiti", - "favorites": "Preferiti" + "favorites": "Preferiti", + "documentation": "Documentazione" } } } diff --git a/web/client/translations/data.nl-NL.json b/web/client/translations/data.nl-NL.json index 2c15c8f07a..1225d80ad0 100644 --- a/web/client/translations/data.nl-NL.json +++ b/web/client/translations/data.nl-NL.json @@ -373,8 +373,7 @@ "Applications": "Toepassingen", "Examples": "Voorbeelden", "LinkedinGroup": "Mapstore Linkedin groep", - "scrollTop": "Terug naar boven", - "footerDescription": "GeoSolutions sales@geosolutionsgroup.com" + "scrollTop": "Terug naar boven" }, "cookiesPolicyNotification": { "title": "Deze site maakt gebruik van cookies", @@ -4331,7 +4330,8 @@ "errorDeletingTag": "It is not possible to delete the tag", "removeFromFavorites": "Remove from favorites", "addToFavorites": "Add to favorites", - "favorites": "Favorites" + "favorites": "Favorites", + "documentation": "Documentation" } } } diff --git a/web/client/translations/data.pt-PT.json b/web/client/translations/data.pt-PT.json index 311f65bd05..1608d59e29 100644 --- a/web/client/translations/data.pt-PT.json +++ b/web/client/translations/data.pt-PT.json @@ -195,7 +195,6 @@ "Examples": "Exemplos", "LinkedinGroup": "Grupo Mapstore no Linkedin", "scrollTop": "Navegar para o topo da página", - "footerDescription": "GeoSolutions sales@geosolutionsgroup.com", "examples":{ "viewer":{ "html":"

    Viewer

    Visualizador Simples

    " @@ -2143,7 +2142,8 @@ "errorDeletingTag": "It is not possible to delete the tag", "removeFromFavorites": "Remove from favorites", "addToFavorites": "Add to favorites", - "favorites": "Favorites" + "favorites": "Favorites", + "documentation": "Documentation" } } } diff --git a/web/client/translations/data.sk-SK.json b/web/client/translations/data.sk-SK.json index 2f75397ca7..ff4f7e261e 100644 --- a/web/client/translations/data.sk-SK.json +++ b/web/client/translations/data.sk-SK.json @@ -284,7 +284,6 @@ "Examples": "Ukážky", "LinkedinGroup": "Linkedin skupina MapStore", "scrollTop": "Prejsť do hornej časti stránky", - "footerDescription": "GeoSolutions sales@geosolutionsgroup.com", "examples":{ "viewer":{ "html":"

    Viewer

    Simple Viewer

    " @@ -3503,7 +3502,8 @@ "errorDeletingTag": "It is not possible to delete the tag", "removeFromFavorites": "Remove from favorites", "addToFavorites": "Add to favorites", - "favorites": "Favorites" + "favorites": "Favorites", + "documentation": "Documentation" } } } diff --git a/web/client/translations/data.sv-SE.json b/web/client/translations/data.sv-SE.json index 363d452513..3da35bfece 100644 --- a/web/client/translations/data.sv-SE.json +++ b/web/client/translations/data.sv-SE.json @@ -291,8 +291,7 @@ "Applications": "Applikationer", "Exempel": "Exempel", "LinkedinGroup": "Mapstore på Linkedin", - "scrollTop": "Bläddra till toppen av sidan", - "footerDescription": " GeoSolutions sales@geosolutionsgroup.com " + "scrollTop": "Bläddra till toppen av sidan" }, "cookiesPolicyNotification": { "title": "Denna webbplats använder cookies", @@ -3542,7 +3541,8 @@ "errorDeletingTag": "It is not possible to delete the tag", "removeFromFavorites": "Remove from favorites", "addToFavorites": "Add to favorites", - "favorites": "Favorites" + "favorites": "Favorites", + "documentation": "Documentation" } } } diff --git a/web/client/translations/data.vi-VN.json b/web/client/translations/data.vi-VN.json index eab62a4ff9..90c59d2a76 100644 --- a/web/client/translations/data.vi-VN.json +++ b/web/client/translations/data.vi-VN.json @@ -566,7 +566,6 @@ "html": "

    Trình xem

    Trình xem đơn giản

    " } }, - "footerDescription": "GeoSolutions sales@geosolutionsgroup.com", "forkMeOnGitHub": "Chia đôi tôi trên GitHub", "open": "Mở", "scrollTop": "Cuộn lên đầu trang", @@ -2172,7 +2171,8 @@ "errorDeletingTag": "It is not possible to delete the tag", "removeFromFavorites": "Remove from favorites", "addToFavorites": "Add to favorites", - "favorites": "Favorites" + "favorites": "Favorites", + "documentation": "Documentation" } } } diff --git a/web/client/translations/data.zh-ZH.json b/web/client/translations/data.zh-ZH.json index 0954b59f49..ac1a0d0140 100644 --- a/web/client/translations/data.zh-ZH.json +++ b/web/client/translations/data.zh-ZH.json @@ -194,7 +194,6 @@ "Examples": "例子", "LinkedinGroup": "Mapstore Linkedin Group", "scrollTop": "滚动到页面顶部", - "footerDescription": "GeoSolutions sales@geosolutionsgroup.com", "examples":{ "viewer":{ "html":"

    Viewer

    Simple Viewer

    " @@ -2119,7 +2118,8 @@ "errorDeletingTag": "It is not possible to delete the tag", "removeFromFavorites": "Remove from favorites", "addToFavorites": "Add to favorites", - "favorites": "Favorites" + "favorites": "Favorites", + "documentation": "Documentation" } } }