diff --git a/razzle.extend.js b/razzle.extend.js new file mode 100644 index 0000000..539f434 --- /dev/null +++ b/razzle.extend.js @@ -0,0 +1,15 @@ +const plugins = (defaultPlugins) => { + return defaultPlugins; +}; +const modify = (config, { target, dev }, webpack) => { + const themeConfigPath = `${__dirname}/theme/theme.config`; + config.resolve.alias['../../theme.config$'] = themeConfigPath; + config.resolve.alias['../../theme.config'] = themeConfigPath; + + return config; +}; + +module.exports = { + plugins, + modify, +}; diff --git a/src/actions/index.js b/src/actions/index.js new file mode 100644 index 0000000..88c9e07 --- /dev/null +++ b/src/actions/index.js @@ -0,0 +1,113 @@ +import { + GET_FRONTPAGESLIDES, + // SET_FOLDER_HEADER, + GET_DEFAULT_HEADER_IMAGE, + SET_FOLDER_TABS, + GET_PARENT_FOLDER_DATA, + GET_LOCALNAVIGATION, + GET_CHART_DATA_FROM_VISUALIZATION, + GET_NAVSITEMAP, + SET_CURRENT_VERSION, +} from '~/constants/ActionTypes'; + +export function setCurrentVersion(payload) { + return { + type: SET_CURRENT_VERSION, + payload: payload, + }; +} + +export function getFrontpageSlides() { + return { + type: GET_FRONTPAGESLIDES, + request: { + op: 'get', + path: `/frontpage_slides?fullobjects`, + }, + }; +} + +export function getDefaultHeaderImage() { + return { + type: GET_DEFAULT_HEADER_IMAGE, + request: { + op: 'get', + path: `/default_header_image?fullobjects`, + }, + }; +} + +export function getLocalnavigation(folder) { + return { + type: GET_LOCALNAVIGATION, + request: { + op: 'get', + path: `${folder}/@localnavigation`, + }, + }; +} + +// export function setFolderHeader(payload) { +// const actualPayload = {}; +// for (const key in payload) { +// if (payload[key] !== null && payload[key] !== undefined) { +// actualPayload[key] = payload[key]; +// } +// } + +// if (Object.keys(actualPayload)) { +// return { +// type: SET_FOLDER_HEADER, +// payload: actualPayload, +// }; +// } +// return; +// } + +export function setFolderTabs(payload) { + return { + type: SET_FOLDER_TABS, + payload: payload, + }; +} + +export function getParentFolderData(url) { + return { + type: GET_PARENT_FOLDER_DATA, + request: { + op: 'get', + path: `/${url}?fullobjects`, + }, + }; +} + +// export function getDataProviders() { +// return { +// type: GET_DATA_PROVIDERS, +// request: { +// op: 'get', +// path: `/@mosaic-settings`, +// }, +// }; +// } + +export function getChartDataFromVisualization(path) { + return { + type: GET_CHART_DATA_FROM_VISUALIZATION, + request: { + op: 'get', + path, + }, + }; +} + +export function getNavSiteMap(url, depth) { + // Note: Depth can't be 0 in plone.restapi + return { + type: GET_NAVSITEMAP, + request: { + op: 'get', + path: `${url}/@navigation?expand.navigation.depth=${depth || 3}`, + }, + }; +} diff --git a/src/components/index.js b/src/components/index.js new file mode 100644 index 0000000..04a1ff3 --- /dev/null +++ b/src/components/index.js @@ -0,0 +1,11 @@ +/** + * Add your components here. + * @module components + * @example + * import Footer from './Footer/Footer'; + * + * export { + * Footer, + * }; + */ +export CountryView from '~/components/theme/CountryView/CountryView'; diff --git a/src/components/manage/Blocks/NavigationBlock/Edit.jsx b/src/components/manage/Blocks/NavigationBlock/Edit.jsx new file mode 100644 index 0000000..a05b968 --- /dev/null +++ b/src/components/manage/Blocks/NavigationBlock/Edit.jsx @@ -0,0 +1,93 @@ +import React, { useState, useEffect } from 'react'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; +import _uniqueId from 'lodash/uniqueId'; +import RenderFields from '@eeacms/volto-datablocks/Utils/RenderFields'; +import View from './View'; +import config from '@plone/volto/registry'; + +const getSchema = (props) => { + return { + parent: { + title: 'Parent page', + widget: 'object_by_path', + }, + className: { + title: 'Classname', + type: 'text', + }, + navFromParent: { + title: 'Show navigation from parent', + type: 'boolean', + }, + pages: { + title: 'Specific pages', + type: 'schema', + fieldSetTitle: 'specific pages', + fieldSetId: 'specific-pages', + fieldSetSchema: { + fieldsets: [ + { + id: 'default', + title: 'Default', + fields: ['title', 'url'], + }, + ], + properties: { + title: { + title: 'Title', + type: 'text', + }, + url: { + title: 'Url', + widget: 'text', + }, + }, + required: ['title', 'url'], + }, + editFieldset: false, + deleteFieldset: false, + }, + }; +}; + +const Edit = (props) => { + const [state, setState] = useState({ + schema: getSchema({ ...props, providerUrl: config.settings.providerUrl }), + id: _uniqueId('block_'), + }); + useEffect(() => { + setState({ + ...state, + schema: getSchema({ + ...props, + }), + }); + /* eslint-disable-next-line */ + }, [state.item, props.data.components]); + return ( +
+ + +
+
+ ); +}; + +export default compose( + connect((state, props) => ({ + pathname: state.router.location.pathname, + })), +)(Edit); diff --git a/src/components/manage/Blocks/NavigationBlock/View.jsx b/src/components/manage/Blocks/NavigationBlock/View.jsx new file mode 100644 index 0000000..61474c2 --- /dev/null +++ b/src/components/manage/Blocks/NavigationBlock/View.jsx @@ -0,0 +1,131 @@ +/* REACT */ +import React, { useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import { compose } from 'redux'; +import { connect } from 'react-redux'; +/* SEMANTIC UI */ +import { Menu } from 'semantic-ui-react'; +/* HELPERS */ +import cx from 'classnames'; +import { isActive, getNavigationByParent, getBasePath } from './helpers'; +import { + deleteQueryParam, + setQueryParam, +} from '@eeacms/volto-datablocks/actions'; +import { useEffect } from 'react'; + +const View = ({ content, ...props }) => { + const { data } = props; + const [state, setState] = useState({ + activeItem: '', + }); + const [navigationItems, setNavigationItems] = useState([]); + const [pages, setPages] = useState([]); + const parent = + data?.navFromParent?.value && props.properties?.parent + ? getBasePath(props.properties?.parent?.['@id']) + : data.parent?.value; + const history = useHistory(); + + useEffect(() => { + const pagesProperties = data.pages?.value + ? data.pages?.value?.properties || {} + : {}; + const newPages = + Object.keys(pagesProperties).map((page) => pagesProperties[page]) || []; + setPages(newPages); + setNavigationItems([...(props.navigation?.items || []), ...newPages]); + }, [props.navigation, data.pages?.value]); + + if (navigationItems.length < 2 && props.mode !== 'edit') return null; + return (props.navigation?.items?.length && parent) || pages.length ? ( +
+ + {navigationItems.map((item, index) => { + const url = getBasePath(item.url); + const name = item.title; + if ( + props.navigation?.items?.filter( + (navItem) => navItem.title === item.title, + ).length + ) { + if (isActive(url, props.pathname) && url !== state.activeItem) { + setState({ + ...state, + activeItem: url, + }); + } else if ( + !isActive(url, props.pathname) && + url === state.activeItem + ) { + setState({ + ...state, + activeItem: '', + }); + } + } + + return ( + 0 ? 'sibling-on-left' : '', + index < navigationItems.length - 1 ? 'sibling-on-right' : '', + )} + name={name} + key={url} + active={ + state.activeItem + ? state.activeItem === url + : !url + ? isActive(url, props.pathname) + : false + } + onClick={() => { + history.push(`${url}${props.query}`); + }} + /> + ); + })} + +
+ ) : props.mode === 'edit' ? ( +

+ There are no pages inside of selected page. Make sure you add pages or + delete the block +

+ ) : ( + '' + ); +}; + +export default compose( + connect( + (state, props) => ({ + query: state.router.location.search, + content: state.content.data, + pathname: state.router.location.pathname, + discodata_query: state.discodata_query, + discodata_resources: state.discodata_resources, + navItems: state.navigation?.items, + flags: state.flags, + navigation: props.properties?.parent + ? getNavigationByParent( + state.navigation?.items, + props.data?.navFromParent?.value + ? getBasePath(props.properties?.parent?.['@id']) + : props.data?.parent?.value, + ) + : {}, + }), + { deleteQueryParam, setQueryParam }, + ), +)(View); diff --git a/src/components/manage/Blocks/NavigationBlock/helpers.js b/src/components/manage/Blocks/NavigationBlock/helpers.js new file mode 100644 index 0000000..2c924ae --- /dev/null +++ b/src/components/manage/Blocks/NavigationBlock/helpers.js @@ -0,0 +1,92 @@ +/* PLUGINS */ +import { isMatch } from 'lodash'; +/* ROOT */ +import config from '@plone/volto/registry'; +/* PLONE VOLTO */ +import { getBaseUrl } from '@plone/volto/helpers'; + +export const isActive = (url, pathname) => { + return ( + (url === '' && pathname === '/') || + (url !== '' && isMatch(pathname?.split('/'), url?.split('/'))) + ); +}; + +export const getNavigationByParent = (items, parent) => { + if (items && parent !== undefined && typeof parent === 'string') { + const pathnameArray = removeValue(parent.split('/'), ''); + const location = pathnameArray; + const depth = pathnameArray.length; + if (!depth) { + return items; + } + + return deepSearch({ + inputArray: items, + location, + depth, + }); + } + return {}; +}; + +const formatNavUrl = (nav) => { + return nav.map((navItem) => ({ + ...navItem, + url: navItem.url ? getBasePath(navItem.url) : '', + items: navItem.items ? formatNavUrl(navItem.items) : false, + })); +}; + +export const deepSearch = ({ inputArray = [], location, depth, start = 1 }) => { + // if (inputArray[0]?.url?.contains('http')) { + inputArray = formatNavUrl(inputArray); + // } + + for (let index = 0; index < inputArray.length; index++) { + if ( + depth === 1 && + removeValue(inputArray[index].url?.split('/'), '')[start - 1] === + location[start - 1] + ) { + return inputArray[index] || {}; + } + if ( + removeValue(inputArray[index].url?.split('/'), '')[start - 1] === + location[start - 1] + ) { + return deepSearch({ + inputArray: inputArray[index].items, + location, + depth: depth - 1, + start: start + 1, + }); + } + } + + return null; +}; + +export function removeValue(arr) { + if (!arr || arr.length === 0) return []; + let what, + a = arguments, + L = a.length, + ax; + while (L > 1 && arr.length) { + what = a[--L]; + while ((ax = arr.indexOf(what)) !== -1) { + arr.splice(ax, 1); + } + } + return arr; +} + +export function getBasePath(url) { + return ( + url && + getBaseUrl(url) + .replace(config.settings.apiPath, '') + .replace(config.settings.internalApiPath, '') + ); +} diff --git a/src/components/manage/PositionToolbar.jsx b/src/components/manage/PositionToolbar.jsx new file mode 100644 index 0000000..ee57544 --- /dev/null +++ b/src/components/manage/PositionToolbar.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { Button } from 'semantic-ui-react'; +import circleLeft from '@plone/volto/icons/circle-left.svg'; +import circleRight from '@plone/volto/icons/circle-right.svg'; +import check from '@plone/volto/icons/check.svg'; +import { Icon } from '@plone/volto/components'; + +function PositionToolbar({ data, onChangeBlock, block }) { + return ( +
+ + + + + +
+ ); +} + +export default PositionToolbar; diff --git a/src/components/manage/Widgets/ObjectListInlineWidget.jsx b/src/components/manage/Widgets/ObjectListInlineWidget.jsx new file mode 100644 index 0000000..2d42a72 --- /dev/null +++ b/src/components/manage/Widgets/ObjectListInlineWidget.jsx @@ -0,0 +1,161 @@ +import { Accordion, Button, Segment, Modal, Grid } from 'semantic-ui-react'; + +import React, { useState } from 'react'; +import { Icon as VoltoIcon, FormFieldWrapper } from '@plone/volto/components'; +import { DragDropList } from '@plone/volto/components'; +import ObjectWidget from './ObjectWidget'; + +import deleteSVG from '@plone/volto/icons/delete.svg'; +import addSVG from '@plone/volto/icons/add.svg'; +import dragSVG from '@plone/volto/icons/drag.svg'; +import pencilSVG from '@plone/volto/icons/pencil.svg'; +import clearSVG from '@plone/volto/icons/clear.svg'; +import { v4 as uuid } from 'uuid'; + +const ObjectListInlineWidget = (props) => { + const [open, setOpen] = useState(false); + const [active, setActive] = useState(null); + const { + id, + schema, + value = [], + onChange, + schemaExtender, + defaultData = {}, + } = props; + + const openModal = (index) => { + setOpen(true); + setActive(index); + }; + + const closeModal = () => { + setOpen(false); + setActive(null); + }; + + return ( + <> + +
+ +
+
+ [o['@id'], o])} + onMoveItem={(result) => { + const { source, destination } = result; + if (!destination) { + return; + } + const first = value[source.index]; + const second = value[destination.index]; + value[destination.index] = first; + value[source.index] = second; + onChange(id, value); + return true; + }} + > + {({ child, childId, index, draginfo }) => { + return ( +
+ + + + + + {`${schema.title} #${index + 1}`} +
+ + +
+
+ {child.title ? ( + + +

{child.title}

+
+
+ ) : ( + '' + )} +
+
+
+ ); + }} +
+ + + + + + { + const newvalue = value.map((v, i) => (i !== active ? v : fv)); + onChange(id, newvalue); + }} + /> + + + + + ); +}; +export default ObjectListInlineWidget; diff --git a/src/components/manage/Widgets/ObjectWidget.jsx b/src/components/manage/Widgets/ObjectWidget.jsx new file mode 100644 index 0000000..e4ea118 --- /dev/null +++ b/src/components/manage/Widgets/ObjectWidget.jsx @@ -0,0 +1,132 @@ +/** + * A generic widget for an object. If multiple + * + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import { Tab } from 'semantic-ui-react'; + +import Field from '@plone/volto/components/manage/Form/Field'; + +/** + * Renders a field set. Passes some of the values in the schema to the Field + * component used inside. Shows the current value, the errors, the required + * status of the fields inside. + * + * @param {object} data + * @param {number} index + * @param {object} schema + * @param {object} value + * @param {object} errors + * @param {function} onChange + * @param {string} id + */ +const FieldSet = ({ data, index, schema, value, errors, onChange, id }) => { + return data.fields.map((field, idx) => { + const v = schema.properties[field].defaultValue + ? value?.[field] || schema.properties[field].defaultValue + : value?.[field]; + return ( + { + return onChange(id, { ...value, [field]: fieldvalue }); + }} + key={field} + error={errors?.[field]} + title={schema.properties[field].title} + /> + ); + }); +}; + +/** + * + * Provides an automatic form for complex JS objects, based on a schema + * + * Creates an object widget with the given onChange handler and an ID. If there + * are multiple field sets, it renders a Tab component with multiple tab panes. + * Each tab has the title of the fieldset it renders. + * + * @param {object} schema Schema, follows Plone dexterity serialized schema + * @param {object} value Object value, a JS object + * @param {function} onChange Callback for object changed + * @param {object} errors A list errors + * @param {string} id Field id + */ +const ObjectWidget = ({ + schema, + value, // not checked to not contain unknown fields + onChange, + errors = {}, + id, + ...props +}) => { + const createTab = React.useCallback( + (fieldset, index) => { + return { + menuItem: fieldset.title, + render: () => ( + +
+ + ), + }; + }, + [errors, id, onChange, schema, value], + ); + + return schema.fieldsets.length === 1 ? ( + <> +
+ + ) : ( + // lazy loading + ); +}; + +/** + * Property types. + * @property {Object} propTypes Property types. + * @static + */ +ObjectWidget.propTypes = { + id: PropTypes.string.isRequired, + schema: PropTypes.object.isRequired, + errors: PropTypes.object, + value: PropTypes.object, + onChange: PropTypes.func.isRequired, +}; + +/** + * Default properties. + * @property {Object} defaultProps Default properties. + * @static + */ +ObjectWidget.defaultProps = { + value: null, +}; + +export default ObjectWidget; diff --git a/src/components/theme/CatalogueViews/AppFooter.jsx b/src/components/theme/CatalogueViews/AppFooter.jsx new file mode 100644 index 0000000..3fb3b2f --- /dev/null +++ b/src/components/theme/CatalogueViews/AppFooter.jsx @@ -0,0 +1,16 @@ +/** + * App container. + * @module components/theme/App/App + */ + +import React, { Component } from 'react'; + +import { Footer } from '@plone/volto/components'; + +class App extends Component { + render() { + return