diff --git a/README.md b/README.md
index 27af504..28ebdab 100644
--- a/README.md
+++ b/README.md
@@ -104,6 +104,26 @@ Each level requires more work to integrate but makes editing easier.
As the GSoC projects progresses more of these levels will be enabled so you can try them out.
see [Hydra GSoC project progresses](https://github.com/orgs/collective/projects/3/views/4)
+## Managing multiple frontends
+
+To switch to a different frontend in the Volto Hydra AdminUI, follow these steps:
+
+1. **Navigate to Personal Tools**:
+ - In the bottom of the toolbar on the left, click on "Personal Tools".
+
+2. **Go to Preferences**:
+ - From the Personal Tools menu, select "Preferences".
+
+3. **Change Frontend URL**:
+ - In the Preferences section, you will find an option to select the Frontend URL.
+ - You can either select a frontend URL from the available options or type in a custom URL:
+ - To select a URL from the options, simply choose from the dropdown menu.
+ - To enter a custom URL, click on the toggle to make the input field appear and type in your desired URL.
+
+This allows you to switch seamlessly between different frontend URLs for testing or editing purposes.
+
+**Note**: Make sure the frontend URL is correct and accessible to avoid any CORS issues.
+
### Level 1: Show changes after save
This is the most basic form of integration.
diff --git a/packages/volto-hydra/src/actions.js b/packages/volto-hydra/src/actions.js
index 4360841..2f39910 100644
--- a/packages/volto-hydra/src/actions.js
+++ b/packages/volto-hydra/src/actions.js
@@ -1,8 +1,8 @@
-import { SET_SELECTED_BLOCK } from './constants';
+import { SET_FRONTEND_PREVIEW_URL } from './constants';
-export function setSelectedBlock(uid) {
+export function setFrontendPreviewUrl(url) {
return {
- type: SET_SELECTED_BLOCK,
- uid: uid,
+ type: SET_FRONTEND_PREVIEW_URL,
+ url: url,
};
}
diff --git a/packages/volto-hydra/src/components/Iframe/View.jsx b/packages/volto-hydra/src/components/Iframe/View.jsx
index 8470aa1..46fc446 100644
--- a/packages/volto-hydra/src/components/Iframe/View.jsx
+++ b/packages/volto-hydra/src/components/Iframe/View.jsx
@@ -12,20 +12,19 @@ import {
import './styles.css';
import { useIntl } from 'react-intl';
import config from '@plone/volto/registry';
-import usePresetUrls from '../../utils/usePreseturls';
import isValidUrl from '../../utils/isValidUrl';
import { BlockChooser } from '@plone/volto/components';
import { createPortal } from 'react-dom';
import { usePopper } from 'react-popper';
-import UrlInput from '../UrlInput';
-import { useDispatch } from 'react-redux';
+import { useSelector, useDispatch } from 'react-redux';
+import { getURlsFromEnv } from '../../utils/getSavedURLs';
import { setSidebarTab } from '@plone/volto/actions';
/**
- * Format the URL for the Iframe with location, token and enabling edit mode
- * @param {*} url
- * @param {*} token
- * @returns {string} URL with the admin params
+ * Format the URL for the Iframe with location, token and edit mode
+ * @param {URL} url
+ * @param {String} token
+ * @returns {URL} URL with the admin params
*/
const getUrlWithAdminParams = (url, token) => {
return typeof window !== 'undefined'
@@ -51,10 +50,7 @@ const Iframe = (props) => {
type: contentType,
selectedBlock,
} = props;
- // const [ready, setReady] = useState(false);
- // useEffect(() => {
- // setReady(true);
- // }, []);
+
const dispatch = useDispatch();
const [addNewBlockOpened, setAddNewBlockOpened] = useState(false);
const [popperElement, setPopperElement] = useState(null);
@@ -67,7 +63,7 @@ const Iframe = (props) => {
{
name: 'offset',
options: {
- offset: [0, -250],
+ offset: [0, -350],
},
},
{
@@ -88,16 +84,18 @@ const Iframe = (props) => {
}, [selectedBlock]);
//-------------------------
- const [url, setUrl] = useState('');
- const [src, setSrc] = useState('');
- const history = useHistory();
+ const [iframeSrc, setIframeSrc] = useState(null);
+ const urlFromEnv = getURlsFromEnv();
+ const u =
+ useSelector((state) => state.frontendPreviewUrl.url) ||
+ Cookies.get('iframe_url') ||
+ urlFromEnv[0];
- const presetUrls = usePresetUrls();
- const defaultUrl = presetUrls[0];
- const savedUrl = Cookies.get('iframe_url');
- const initialUrl = savedUrl
- ? getUrlWithAdminParams(savedUrl, token)
- : getUrlWithAdminParams(defaultUrl, token);
+ useEffect(() => {
+ setIframeSrc(getUrlWithAdminParams(u, token));
+ u && Cookies.set('iframe_url', u, { expires: 7 });
+ }, [token, u]);
+ const history = useHistory();
//-----Experimental-----
const intl = useIntl();
@@ -132,26 +130,19 @@ const Iframe = (props) => {
const handleNavigateToUrl = useCallback(
(givenUrl = null) => {
- if (!isValidUrl(givenUrl) && !isValidUrl(url)) {
+ if (!isValidUrl(givenUrl)) {
return;
}
// Update adminUI URL with the new URL
- const formattedUrl = givenUrl ? new URL(givenUrl) : new URL(url);
+ const formattedUrl = new URL(givenUrl);
const newOrigin = formattedUrl.origin;
Cookies.set('iframe_url', newOrigin, { expires: 7 });
history.push(`${formattedUrl.pathname}`);
},
- [history, url],
+ [history],
);
- useEffect(() => {
- setUrl(
- `${savedUrl || defaultUrl}${window.location.pathname.replace('/edit', '')}`,
- );
- setSrc(initialUrl);
- }, [savedUrl, defaultUrl, initialUrl]);
-
useEffect(() => {
//----------------Experimental----------------
const onDeleteBlock = (id, selectPrev) => {
@@ -160,7 +151,7 @@ const Iframe = (props) => {
onChangeFormData(newFormData);
onSelectBlock(selectPrev ? previous : null);
- const origin = new URL(src).origin;
+ const origin = new URL(iframeSrc).origin;
document
.getElementById('previewIframe')
.contentWindow.postMessage(
@@ -169,7 +160,7 @@ const Iframe = (props) => {
);
};
//----------------------------------------------
- const initialUrlOrigin = initialUrl ? new URL(initialUrl).origin : '';
+ const initialUrlOrigin = iframeSrc && new URL(iframeSrc).origin;
const messageHandler = (event) => {
if (event.origin !== initialUrlOrigin) {
return;
@@ -210,31 +201,28 @@ const Iframe = (props) => {
window.removeEventListener('message', messageHandler);
};
}, [
+ dispatch,
handleNavigateToUrl,
history.location.pathname,
- initialUrl,
+ iframeSrc,
onChangeFormData,
onSelectBlock,
properties,
- src,
token,
]);
useEffect(() => {
- if (form && Object.keys(form).length > 0 && isValidUrl(src)) {
+ if (form && Object.keys(form).length > 0 && isValidUrl(iframeSrc)) {
// Send the form data to the iframe
- const origin = new URL(src).origin;
+ const origin = new URL(iframeSrc).origin;
document
.getElementById('previewIframe')
.contentWindow.postMessage({ type: 'FORM_DATA', data: form }, origin);
}
- }, [form, initialUrl, src]);
+ }, [form, iframeSrc]);
return (
-
-
-
{addNewBlockOpened &&
createPortal(
{
? (id, value) => {
setAddNewBlockOpened(false);
const newId = onInsertBlock(id, value);
- const origin = new URL(src).origin;
+ const origin = new URL(iframeSrc).origin;
document
.getElementById('previewIframe')
.contentWindow.postMessage(
@@ -284,7 +272,7 @@ const Iframe = (props) => {
diff --git a/packages/volto-hydra/src/components/Iframe/styles.css b/packages/volto-hydra/src/components/Iframe/styles.css
index ae39e71..7758c28 100644
--- a/packages/volto-hydra/src/components/Iframe/styles.css
+++ b/packages/volto-hydra/src/components/Iframe/styles.css
@@ -1,7 +1,7 @@
#iframeContainer {
position: relative;
width: 100%;
- height: calc(100vh - 87px);
+ height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
diff --git a/packages/volto-hydra/src/constants.js b/packages/volto-hydra/src/constants.js
index 71dfa9e..19f0e0a 100644
--- a/packages/volto-hydra/src/constants.js
+++ b/packages/volto-hydra/src/constants.js
@@ -1 +1 @@
-export const SET_SELECTED_BLOCK = 'SET_SELECTED_BLOCK';
+export const SET_FRONTEND_PREVIEW_URL = 'SET_FRONTEND_PREVIEW_URL';
diff --git a/packages/volto-hydra/src/customizations/components/manage/Form/Form.jsx b/packages/volto-hydra/src/customizations/components/manage/Form/Form.jsx
index 00b06a0..96c9274 100644
--- a/packages/volto-hydra/src/customizations/components/manage/Form/Form.jsx
+++ b/packages/volto-hydra/src/customizations/components/manage/Form/Form.jsx
@@ -324,7 +324,7 @@ class Form extends Component {
) {
this.setState(() => ({ sidebarMetadataIsAvailable: true }));
}
- if (this.props.location.pathname !== prevProps.location.pathname) {
+ if (this.props?.location?.pathname !== prevProps?.location?.pathname) {
this.setState({ formData: this.props.formData });
}
}
@@ -663,176 +663,172 @@ class Form extends Component {
navRoot={navRoot}
/>
-
- {
- const newFormData = {
- ...formData,
- ...newBlockData,
- };
- this.setState({
- formData: newFormData,
- });
- if (this.props.global) {
- this.props.setFormData(newFormData);
- }
- }}
- onSetSelectedBlocks={(blockIds) =>
- this.setState({ multiSelected: blockIds })
+ {
+ const newFormData = {
+ ...formData,
+ ...newBlockData,
+ };
+ this.setState({
+ formData: newFormData,
+ });
+ if (this.props.global) {
+ this.props.setFormData(newFormData);
}
- onSelectBlock={this.onSelectBlock}
- />
- {
- if (this.props.global) {
- this.props.setFormData(state.formData);
- }
- return this.setState(state);
- }}
- />
-
+
>
)
) : (
diff --git a/packages/volto-hydra/src/customizations/components/manage/Preferences/PersonalPreferences.jsx b/packages/volto-hydra/src/customizations/components/manage/Preferences/PersonalPreferences.jsx
new file mode 100644
index 0000000..b4ecbad
--- /dev/null
+++ b/packages/volto-hydra/src/customizations/components/manage/Preferences/PersonalPreferences.jsx
@@ -0,0 +1,234 @@
+/**
+ * Personal preferences component.
+ * @module components/manage/Preferences/PersonalPreferences
+ */
+
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { compose } from 'redux';
+import { map, keys } from 'lodash';
+import { withCookies } from 'react-cookie';
+import { defineMessages, injectIntl } from 'react-intl';
+import { toast } from 'react-toastify';
+
+import { Toast } from '@plone/volto/components';
+import { Form } from '@plone/volto/components/manage/Form';
+import languages from '@plone/volto/constants/Languages';
+import { changeLanguage } from '@plone/volto/actions';
+import { toGettextLang } from '@plone/volto/helpers';
+import config from '@plone/volto/registry';
+import getSavedURLs from '../../../../utils/getSavedURLs';
+import isValidUrl from '../../../../utils/isValidUrl';
+import { setFrontendPreviewUrl } from '../../../../actions';
+
+const messages = defineMessages({
+ personalPreferences: {
+ id: 'Personal Preferences',
+ defaultMessage: 'Personal Preferences',
+ },
+ default: {
+ id: 'Default',
+ defaultMessage: 'Default',
+ },
+ language: {
+ id: 'Language',
+ defaultMessage: 'Language',
+ },
+ languageDescription: {
+ id: 'Your preferred language',
+ defaultMessage: 'Your preferred language',
+ },
+ saved: {
+ id: 'Changes saved',
+ defaultMessage: 'Changes saved',
+ },
+ back: {
+ id: 'Back',
+ defaultMessage: 'Back',
+ },
+ success: {
+ id: 'Success',
+ defaultMessage: 'Success',
+ },
+ frontendUrls: {
+ id: 'Frontend URL',
+ defaultMessage: 'Frontend URL',
+ },
+ frontendUrl: {
+ id: 'Custom URL',
+ defaultMessage: 'Custom URL',
+ },
+ urlsDescription: {
+ id: `Changes the site to visit when in edit mode.`,
+ defaultMessage: `Changes the site to visit when in edit mode.`,
+ },
+ urlDescription: {
+ id: `OR Enter your Frontend's base URL`,
+ defaultMessage: `OR Enter your Frontend's base URL`,
+ },
+});
+
+/**
+ * PersonalPreferences class.
+ * @class PersonalPreferences
+ * @extends Component
+ */
+class PersonalPreferences extends Component {
+ /**
+ * Property types.
+ * @property {Object} propTypes Property types.
+ * @static
+ */
+ static propTypes = {
+ changeLanguage: PropTypes.func.isRequired,
+ closeMenu: PropTypes.func.isRequired,
+ };
+
+ /**
+ * Constructor
+ * @method constructor
+ * @param {Object} props Component properties
+ * @constructs PersonalPreferences
+ */
+ constructor(props) {
+ super(props);
+ this.onCancel = this.onCancel.bind(this);
+ this.onSubmit = this.onSubmit.bind(this);
+ this.urls = getSavedURLs();
+ this.state = {
+ hidden: true,
+ };
+ }
+
+ /**
+ * Submit handler
+ * @method onSubmit
+ * @param {object} data Form data.
+ * @returns {undefined}
+ */
+ onSubmit(data) {
+ let language = data.language || 'en';
+ if (config.settings.supportedLanguages.includes(language)) {
+ const langFileName = toGettextLang(language);
+ import('@root/../locales/' + langFileName + '.json').then((locale) => {
+ this.props.changeLanguage(language, locale.default);
+ });
+ }
+ toast.success(
+
,
+ );
+ // Check if the URL is typed in or Selected from dropdown
+ if (data.urlCheck) {
+ if (!isValidUrl(data.url)) { // Check if the URL is valid
+ toast.error(
+
,
+ );
+ return;
+ }
+ const url = new URL(data.url);
+ this.props.setFrontendPreviewUrl(url.origin);
+ const urlList = [...new Set([this.urls, url])];
+ this.props.cookies.set('saved_urls', urlList.join(','), {
+ expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 Days
+ });
+ } else {
+ const url = new URL(data.urls);
+ this.props.setFrontendPreviewUrl(url.origin);
+ }
+ this.props.closeMenu();
+ }
+
+ /**
+ * Cancel handler
+ * @method onCancel
+ * @returns {undefined}
+ */
+ onCancel() {
+ this.props.closeMenu();
+ }
+
+ /**
+ * Render method.
+ * @method render
+ * @returns {string} Markup for the component.
+ */
+ render() {
+ const { cookies } = this.props;
+ const urls = this.urls;
+ return (
+