-
-
{siteName}
-
Updated 2 days ago
+
+
+
+
{siteName}
+
{siteUrl}
+
{lastUpdated}
+
+
+
+ {sidebarContentPathDict.map(({ pathname, title }) => (
+ generateTab(title, siteName, pathname)
+ ))}
+
+
+
+
+
+
+ {sidebarSettingsPathDict.map(({ pathname, title }) => (
+ generateTab(title, siteName, pathname)
+ ))}
+
+
+
+
+ {sidebarUserDict.map(({ pathname, title }) => (
+ generateTab(title, siteName, pathname)
+ ))}
+
+
-
-
- {sidebarPathDict.map(({ pathname, title }) => (
-
- {generateLink(title, siteName, pathname)}
-
- ))}
-
-
-
-);
+ )
+};
export default Sidebar;
diff --git a/src/components/folders/FolderContent.jsx b/src/components/folders/FolderContent.jsx
new file mode 100644
index 000000000..30c89bc12
--- /dev/null
+++ b/src/components/folders/FolderContent.jsx
@@ -0,0 +1,310 @@
+import React, { useState, useRef, useEffect } from 'react';
+import { Link } from 'react-router-dom';
+import { Droppable, Draggable } from 'react-beautiful-dnd';
+import { DragDropContext } from 'react-beautiful-dnd';
+import update from 'immutability-helper';
+import PropTypes from 'prop-types';
+
+import { deslugifyPage } from '../../utils'
+import { MenuDropdown } from '../MenuDropdown'
+import FileMoveMenuDropdown from '../FileMoveMenuDropdown'
+
+// Import styles
+import elementStyles from '../../styles/isomer-cms/Elements.module.scss';
+import contentStyles from '../../styles/isomer-cms/pages/Content.module.scss';
+
+const FolderContentItem = ({
+ title,
+ isFile,
+ numItems,
+ link,
+ itemIndex,
+ allCategories,
+ setSelectedPage,
+ setSelectedPath,
+ setIsPageSettingsActive,
+ setIsFolderModalOpen,
+ setIsMoveModalActive,
+ setIsDeleteModalActive,
+ moveDropdownQuery,
+ setMoveDropdownQuery,
+ clearMoveDropdownQueryState,
+}) => {
+ const [showDropdown, setShowDropdown] = useState(false)
+ const [showFileMoveDropdown, setShowFileMoveDropdown] = useState(false)
+ const dropdownRef = useRef(null)
+ const fileMoveDropdownRef = useRef(null)
+
+ useEffect(() => {
+ if (showDropdown) dropdownRef.current.focus()
+ if (showFileMoveDropdown) fileMoveDropdownRef.current.focus()
+ }, [showDropdown, showFileMoveDropdown])
+
+ const handleBlur = (event) => {
+ // if the blur was because of outside focus
+ // currentTarget is the parent element, relatedTarget is the clicked element
+ if (!event.currentTarget.contains(event.relatedTarget)) {
+ setShowFileMoveDropdown(false)
+ clearMoveDropdownQueryState()
+ }
+ }
+
+ const toggleDropdownModals = () => {
+ setShowFileMoveDropdown(!showFileMoveDropdown)
+ setShowDropdown(!showDropdown)
+ }
+
+ const generateDropdownItems = () => {
+ const dropdownItems = [
+ {
+ type: 'edit',
+ handler: () => {
+ if (isFile) setIsPageSettingsActive(true)
+ else {
+ setIsFolderModalOpen(true)
+ }
+ }
+ },
+ {
+ type: 'move',
+ handler: toggleDropdownModals
+ },
+ {
+ type: 'delete',
+ handler: () => setIsDeleteModalActive(true)
+ },
+ ]
+ if (isFile) return dropdownItems
+ return dropdownItems.filter(item => item.type !== 'move')
+ }
+
+ const FolderItemContent = (
+
+
+ {
+ isFile
+ ?
+ :
+ }
+
{deslugifyPage(title)}
+ {
+ numItems !== null
+ ?
{numItems} item{numItems === 1 ? '' : 's'}
+ : null
+ }
+
+ {
+ e.stopPropagation()
+ e.preventDefault()
+ setSelectedPage(title)
+ setShowDropdown(true)
+ }}
+ >
+
+
+ { showDropdown &&
+ setShowDropdown(false)}
+ />
+ }
+ { showFileMoveDropdown &&
+ {
+ setSelectedPath(`${moveDropdownQuery.folderName ? moveDropdownQuery.folderName : 'pages'}${moveDropdownQuery.subfolderName ? `/${moveDropdownQuery.subfolderName}` : ''}`)
+ setIsMoveModalActive(true)
+ }}
+ />
+ }
+
+
+
+ )
+
+ return (
+ !showFileMoveDropdown && !showDropdown
+ ?
+
+ {FolderItemContent}
+
+ :
+
+ {FolderItemContent}
+
+ )
+}
+
+const FolderContent = ({
+ folderOrderArray,
+ setFolderOrderArray,
+ siteName,
+ folderName,
+ enableDragDrop,
+ allCategories,
+ setSelectedPath,
+ setSelectedPage,
+ setIsPageSettingsActive,
+ setIsFolderModalOpen,
+ setIsMoveModalActive,
+ setIsDeleteModalActive,
+ moveDropdownQuery,
+ setMoveDropdownQuery,
+ clearMoveDropdownQueryState,
+}) => {
+ const generateLink = (folderContentItem) => {
+ if (folderContentItem.type === 'dir') return `/sites/${siteName}/folder/${folderName}/subfolder/${folderContentItem.fileName}`
+ return `/sites/${siteName}/folder/${folderName}/${folderContentItem.path.includes('/') ? `subfolder/` : ''}${folderContentItem.path}`
+ }
+
+ const onDragEnd = (result) => {
+ const { source, destination } = result;
+
+ // If the user dropped the draggable to no known droppable
+ if (!destination) return;
+
+ // The draggable elem was returned to its original position
+ if (
+ destination.droppableId === source.droppableId
+ && destination.index === source.index
+ ) return;
+
+ const elem = folderOrderArray[source.index]
+ const newFolderOrderArray = update(folderOrderArray, {
+ $splice: [
+ [source.index, 1], // Remove elem from its original position
+ [destination.index, 0, elem], // Splice elem into its new position
+ ],
+ });
+ setFolderOrderArray(newFolderOrderArray)
+ }
+
+ return (
+
+
+ {(droppableProvided) => (
+
+ {
+ folderOrderArray.map((folderContentItem, folderContentIndex) => (
+
+ {(draggableProvided) => (
+
+ !name.includes('.keep')).length : null}
+ isFile={folderContentItem.type === 'dir' ? false: true}
+ link={generateLink(folderContentItem)}
+ allCategories={allCategories}
+ itemIndex={folderContentIndex}
+ setSelectedPage={setSelectedPage}
+ setSelectedPath={setSelectedPath}
+ setIsPageSettingsActive={setIsPageSettingsActive}
+ setIsFolderModalOpen={setIsFolderModalOpen}
+ setIsMoveModalActive={setIsMoveModalActive}
+ setIsDeleteModalActive={setIsDeleteModalActive}
+ moveDropdownQuery={moveDropdownQuery}
+ setMoveDropdownQuery={setMoveDropdownQuery}
+ clearMoveDropdownQueryState={clearMoveDropdownQueryState}
+ />
+
+ )}
+
+ ))
+ }
+ {droppableProvided.placeholder}
+
+ )}
+
+
+ )
+}
+
+export default FolderContent
+
+FolderContentItem.propTypes = {
+ title: PropTypes.string.isRequired,
+ isFile: PropTypes.bool.isRequired,
+ numItems: PropTypes.number,
+ link: PropTypes.string.isRequired,
+ itemIndex: PropTypes.number.isRequired,
+ allCategories: PropTypes.arrayOf(
+ PropTypes.string.isRequired
+ ),
+ setSelectedPage: PropTypes.func.isRequired,
+ setSelectedPath: PropTypes.func.isRequired,
+ setIsPageSettingsActive: PropTypes.func.isRequired,
+ setIsFolderModalOpen: PropTypes.func.isRequired,
+ setIsMoveModalActive: PropTypes.func.isRequired,
+ setIsDeleteModalActive: PropTypes.func.isRequired,
+ moveDropdownQuery: PropTypes.shape({
+ folderName: PropTypes.string.isRequired,
+ subfolderName: PropTypes.string.isRequired,
+ }).isRequired,
+ setMoveDropdownQuery: PropTypes.func.isRequired,
+ clearMoveDropdownQueryState: PropTypes.func.isRequired,
+};
+
+
+FolderContent.propTypes = {
+ folderOrderArray: PropTypes.arrayOf(
+ PropTypes.shape({
+ fileName: PropTypes.string.isRequired,
+ path: PropTypes.string.isRequired,
+ sha: PropTypes.string,
+ title: PropTypes.string,
+ }),
+ ).isRequired,
+ setFolderOrderArray: PropTypes.func.isRequired,
+ siteName: PropTypes.string.isRequired,
+ folderName: PropTypes.string.isRequired,
+ enableDragDrop: PropTypes.func.isRequired,
+ allCategories: PropTypes.arrayOf(
+ PropTypes.string.isRequired
+ ),
+ setSelectedPath: PropTypes.func.isRequired,
+ setSelectedPage: PropTypes.func.isRequired,
+ setIsPageSettingsActive: PropTypes.func.isRequired,
+ setIsFolderModalOpen: PropTypes.func.isRequired,
+ setIsMoveModalActive: PropTypes.func.isRequired,
+ setIsDeleteModalActive: PropTypes.func.isRequired,
+ moveDropdownQuery: PropTypes.shape({
+ folderName: PropTypes.string.isRequired,
+ subfolderName: PropTypes.string.isRequired,
+ }).isRequired,
+ setMoveDropdownQuery: PropTypes.func.isRequired,
+ clearMoveDropdownQueryState: PropTypes.func.isRequired,
+};
diff --git a/src/components/folders/FolderOptionButton.jsx b/src/components/folders/FolderOptionButton.jsx
new file mode 100644
index 000000000..0924d8282
--- /dev/null
+++ b/src/components/folders/FolderOptionButton.jsx
@@ -0,0 +1,37 @@
+import React from 'react';
+
+// Import styles
+import elementStyles from '../../styles/isomer-cms/Elements.module.scss';
+import contentStyles from '../../styles/isomer-cms/pages/Content.module.scss';
+
+const iconSelection = {
+ 'rearrange': 'bx-sort',
+ 'create-page': 'bx-file-blank',
+ 'create-sub': 'bx-folder',
+}
+
+
+
+const FolderOptionButton = ({ title, isSelected, onClick, option, isDisabled, id }) => {
+ return (
+
+
+
+ {title}
+
+
+ )
+}
+
+export default FolderOptionButton
\ No newline at end of file
diff --git a/src/components/media/MediaModal.jsx b/src/components/media/MediaModal.jsx
index 621eee1d3..07f6caebf 100644
--- a/src/components/media/MediaModal.jsx
+++ b/src/components/media/MediaModal.jsx
@@ -6,34 +6,61 @@ import mediaStyles from '../../styles/isomer-cms/pages/Media.module.scss';
import elementStyles from '../../styles/isomer-cms/Elements.module.scss';
import MediaCard from './MediaCard';
import MediaUploadCard from './MediaUploadCard';
+import { MediaSearchBar } from './MediaSearchBar';
import LoadingButton from '../LoadingButton';
-export default class MediasModal extends Component {
+export default class MediaModal extends Component {
constructor(props) {
super(props);
this.state = {
medias: '',
+ filteredMedias: '',
selectedFile: null,
};
}
async componentDidMount() {
- const { siteName, type } = this.props;
+ const { siteName, type, mediaSearchTerm } = this.props;
const mediaRoute = type === 'file' ? 'documents' : 'images';
try {
const { data: { documents, images } } = await axios.get(`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/${mediaRoute}`, {
withCredentials: true,
});
if (_.isEmpty(documents || images)) {
- this.setState({ medias: [] });
+ this.setState({ medias: [], filteredMedias: [], searchTerm: mediaSearchTerm });
} else {
- this.setState({ medias: documents || images });
+ let filteredMedias
+ if (type === 'file') {
+ filteredMedias = this.filterMediaByFileName(documents, mediaSearchTerm)
+ } else {
+ filteredMedias = this.filterMediaByFileName(images, mediaSearchTerm)
+ }
+
+ this.setState({ medias: documents || images, filteredMedias, searchTerm: mediaSearchTerm });
}
} catch (err) {
console.error(err);
}
}
+ filterMediaByFileName = (medias, filterTerm) => {
+ const filteredMedias = medias.filter((media) => {
+ if (media.fileName.toLowerCase().includes(filterTerm.toLowerCase())) return true
+ return false
+ })
+
+ return filteredMedias
+ }
+
+ searchChangeHandler = (event) => {
+ const { setMediaSearchTerm } = this.props
+ const { medias } = this.state
+ const { target: { value } } = event
+ const filteredMedias = this.filterMediaByFileName(medias, value)
+ setMediaSearchTerm(value)
+ this.setState({ filteredMedias })
+ }
+
render() {
const {
siteName,
@@ -41,15 +68,19 @@ export default class MediasModal extends Component {
onMediaSelect,
type,
readFileToStageUpload,
+ mediaSearchTerm,
+ selectedFile,
+ setSelectedFile,
} = this.props;
- const { medias, selectedFile } = this.state;
- return (medias
+ const { filteredMedias } = this.state;
+ return (filteredMedias
&& (
<>
-
{`Select ${type === 'file' ? 'File' : 'Image'}`}
+
{`Select ${type === 'file' ? 'File' : 'Image'}`}
+
@@ -69,16 +100,16 @@ export default class MediasModal extends Component {
type="file"
id="file-upload"
accept={type === 'file' ? `application/msword, application/vnd.ms-excel, application/vnd.ms-powerpoint,
- text/plain, application/pdf` : 'image*'}
+ text/plain, application/pdf` : 'image/*'}
hidden
/>
{/* Render medias */}
- {medias.map((media) => (
+ {filteredMedias.map((media) => (
this.setState({ selectedFile: media })}
+ onClick={() => setSelectedFile(media)}
key={media.path}
isSelected={media.path === selectedFile?.path}
/>
@@ -107,9 +138,10 @@ export default class MediasModal extends Component {
}
}
-MediasModal.propTypes = {
+MediaModal.propTypes = {
onClose: PropTypes.func.isRequired,
siteName: PropTypes.string.isRequired,
onMediaSelect: PropTypes.func.isRequired,
type: PropTypes.oneOf(['file', 'image']).isRequired,
+ setMediaSearchTerm: PropTypes.func.isRequired,
};
diff --git a/src/components/media/MediaSearchBar.jsx b/src/components/media/MediaSearchBar.jsx
new file mode 100644
index 000000000..0721bc699
--- /dev/null
+++ b/src/components/media/MediaSearchBar.jsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import elementStyles from '../../styles/isomer-cms/Elements.module.scss';
+
+
+export const MediaSearchBar = ({
+ value,
+ onSearchChange,
+}) => {
+ return (
+
+
+
+ )
+}
+
+MediaSearchBar.propTypes = {
+ value: PropTypes.string,
+ onSearchChange: PropTypes.func.isRequired,
+ };
\ No newline at end of file
diff --git a/src/components/media/MediaSettingsModal.jsx b/src/components/media/MediaSettingsModal.jsx
index 6d3e1a027..cf12cbfe6 100644
--- a/src/components/media/MediaSettingsModal.jsx
+++ b/src/components/media/MediaSettingsModal.jsx
@@ -1,17 +1,19 @@
import React, { Component } from 'react';
import axios from 'axios';
import PropTypes from 'prop-types';
-import mediaStyles from '../../styles/isomer-cms/pages/Media.module.scss';
-import elementStyles from '../../styles/isomer-cms/Elements.module.scss';
+
import FormField from '../FormField';
import DeleteWarningModal from '../DeleteWarningModal';
import SaveDeleteButtons from '../SaveDeleteButtons';
+
import { validateFileName } from '../../utils/validators';
-import { toast } from 'react-toastify';
-import Toast from '../Toast';
import {
- DEFAULT_ERROR_TOAST_MSG,
+ DEFAULT_RETRY_MSG,
} from '../../utils'
+import { errorToast } from '../../utils/toasts';
+
+import mediaStyles from '../../styles/isomer-cms/pages/Media.module.scss';
+import elementStyles from '../../styles/isomer-cms/Elements.module.scss';
export default class MediaSettingsModal extends Component {
constructor(props) {
@@ -86,25 +88,16 @@ export default class MediaSettingsModal extends Component {
withCredentials: true,
});
}
- onSave()
+ onSave(newFileName)
} catch (err) {
if (err?.response?.status === 409) {
// Error due to conflict in name
- toast(
- ,
- {className: `${elementStyles.toastError} ${elementStyles.toastLong}`}
- );
+ errorToast(`Another ${type === 'image' ? 'image' : 'file'} with the same name exists. Please choose a different name.`)
} else if (err?.response?.status === 413 || err?.response === undefined) {
// Error due to file size too large - we receive 413 if nginx accepts the payload but it is blocked by our express settings, and undefined if it is blocked by nginx
- toast(
- ,
- {className: `${elementStyles.toastError} ${elementStyles.toastLong}`}
- );
+ errorToast(`Unable to upload as the ${type === 'image' ? 'image' : 'file'} size exceeds 5MB. Please reduce your ${type === 'image' ? 'image' : 'file'} size and try again.`)
} else {
- toast(
- ,
- {className: `${elementStyles.toastError} ${elementStyles.toastLong}`}
- );
+ errorToast(`There was a problem trying to save this ${type === 'image' ? 'image' : 'file'}. ${DEFAULT_RETRY_MSG}`)
}
console.log(err);
}
@@ -125,10 +118,7 @@ export default class MediaSettingsModal extends Component {
window.location.reload();
} catch (err) {
- toast(
- ,
- {className: `${elementStyles.toastError} ${elementStyles.toastLong}`}
- );
+ errorToast(`There was a problem trying to delete this ${type === 'image' ? 'image' : 'file'}. ${DEFAULT_RETRY_MSG}`)
console.log(err);
}
}
@@ -137,6 +127,7 @@ export default class MediaSettingsModal extends Component {
const {
onClose, media, type, isPendingUpload, siteName,
} = this.props;
+ const { fileName } = media
const {
newFileName,
sha,
@@ -150,9 +141,7 @@ export default class MediaSettingsModal extends Component {
- Edit
- { ' ' }
- { type }
+ {isPendingUpload ? `Upload new ${type}` : `Edit ${type} details`}
@@ -188,11 +177,14 @@ export default class MediaSettingsModal extends Component {
onFieldChange={this.setFileName}
/>
-
this.setState({ canShowDeleteWarningModal: true })}
+ isLoading={isPendingUpload ? false : !sha}
/>
diff --git a/src/components/navbar/NavSection.jsx b/src/components/navbar/NavSection.jsx
new file mode 100644
index 000000000..ce6ca947d
--- /dev/null
+++ b/src/components/navbar/NavSection.jsx
@@ -0,0 +1,345 @@
+import React, { useState, useRef } from 'react';
+import PropTypes from 'prop-types';
+import { Droppable, Draggable } from 'react-beautiful-dnd';
+import Select from 'react-select';
+import styles from '../../styles/App.module.scss';
+import elementStyles from '../../styles/isomer-cms/Elements.module.scss';
+import FormField from '../FormField';
+import NavSublinkSection from './NavSublinkSection'
+import { isEmpty } from '../../utils';
+
+/* eslint
+ react/no-array-index-key: 0
+ */
+
+const defaultText = '--Choose a collection--'
+
+const NavElem = ({
+ title,
+ options,
+ collection,
+ url,
+ linkIndex,
+ sublinks,
+ isResource,
+ onFieldChange,
+ createHandler,
+ deleteHandler,
+ displayHandler,
+ shouldDisplay,
+ displaySublinks,
+ linkErrors,
+ sublinkErrors,
+}) => {
+ const collectionDropdownHandler = (newValue) => {
+ let event;
+ if (!newValue) {
+ // Field was cleared
+ event = {
+ target: {
+ id: `link-${linkIndex}-collection`,
+ value: '',
+ }
+ }
+ } else {
+ const { value } = newValue
+ event = {
+ target: {
+ id: `link-${linkIndex}-collection`,
+ value,
+ }
+ }
+ }
+
+ onFieldChange(event);
+ };
+
+ const generateTitle = () => {
+ if (collection) {
+ return `${title}`
+ }
+ if (sublinks) {
+ return `${title}`
+ }
+ if (isResource) {
+ return `${title}`
+ }
+ return `${title}`
+ }
+
+ return (
+
+
+
+ {generateTitle()}
+
+
+
+
+
+ { shouldDisplay
+ ? (
+ <>
+
+
+ {
+ collection &&
+ <>
+
Folder
+
+ >
+ }
+ {
+ (!collection && !isResource) &&
+
+ }
+ {
+ sublinks &&
+
+ }
+
+
+ Delete Menu
+
+ >
+ )
+ : null}
+
+ )
+};
+
+const NavSection = ({
+ links,
+ options,
+ createHandler,
+ deleteHandler,
+ onFieldChange,
+ displayHandler,
+ displayLinks,
+ displaySublinks,
+ hasResourceRoom,
+ hasResources,
+ errors,
+}) => {
+ const [newSectionType, setNewSectionType] = useState()
+ const selectInputRef = useRef()
+ const sectionCreationHandler = () => {
+ selectInputRef.current.select.clearValue()
+ const event = {
+ target: {
+ id: `link-create`,
+ value: newSectionType
+ }
+ }
+ createHandler(event)
+ }
+ const sectionCreationOptions = [
+ ... options.length > 0
+ ? [{
+ value: 'collectionLink',
+ label: 'Folder',
+ }]
+ : [],
+ ... hasResourceRoom && !hasResources
+ ? [{
+ value: 'resourceLink',
+ label: 'Resource Room',
+ }]
+ : [],
+ {
+ value: 'pageLink',
+ label: 'Single Page',
+ },
+ {
+ value: 'sublinkLink',
+ label: 'Submenu',
+ },
+ ]
+ return (
+ <>
+
+ {(droppableProvided) => (
+ /* eslint-disable react/jsx-props-no-spreading */
+
+ { (links && links.length > 0)
+ ? <>
+
Menu
+ {links.map((link, linkIndex) => (
+
+ {(draggableProvided) => (
+
+
+
+ )}
+
+ ))}
+ >
+ : null}
+ {droppableProvided.placeholder}
+
+ )}
+
+
+ setNewSectionType(option ? option.value : '')}
+ placeholder={"Select link type..."}
+ options={sectionCreationOptions}
+ />
+ Create New Menu
+
+
+ {`Note: you can specify a folder ${hasResourceRoom ? `or resource room ` : ``}to automatically populate its links. ${hasResourceRoom ? `Only one resource room link is allowed. ` : ``}Select "Submenu" if you want to specify your own links.`}
+
+ >
+ )
+};
+
+export default NavSection;
+
+NavElem.propTypes = {
+ title: PropTypes.string,
+ url: PropTypes.string,
+ collection: PropTypes.string,
+ sublinks: PropTypes.arrayOf(
+ PropTypes.shape({
+ title: PropTypes.string,
+ url: PropTypes.string,
+ }),
+ ),
+ options: PropTypes.arrayOf(
+ PropTypes.shape({
+ value: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ }),
+ ).isRequired,
+ isResource: PropTypes.bool,
+ linkIndex: PropTypes.number.isRequired,
+ onFieldChange: PropTypes.func.isRequired,
+ createHandler: PropTypes.func.isRequired,
+ deleteHandler: PropTypes.func.isRequired,
+ displayHandler: PropTypes.func.isRequired,
+ shouldDisplay: PropTypes.bool,
+ displaySublinks: PropTypes.arrayOf(PropTypes.bool),
+ linkErrors: PropTypes.shape({
+ title: PropTypes.string,
+ url: PropTypes.string,
+ }),
+ sublinkErrors: PropTypes.arrayOf(
+ PropTypes.shape({
+ title: PropTypes.string,
+ url: PropTypes.string,
+ }),
+ )
+};
+
+NavSection.propTypes = {
+ links: PropTypes.arrayOf(
+ PropTypes.shape({
+ title: PropTypes.string,
+ url: PropTypes.string,
+ collection: PropTypes.string,
+ resource_room: PropTypes.bool,
+ sublinks: PropTypes.arrayOf(
+ PropTypes.shape({
+ title: PropTypes.string,
+ url: PropTypes.string,
+ }),
+ )
+ }),
+ ).isRequired,
+ options: PropTypes.arrayOf(
+ PropTypes.shape({
+ value: PropTypes.string.isRequired,
+ label: PropTypes.string.isRequired,
+ }),
+ ).isRequired,
+ createHandler: PropTypes.func.isRequired,
+ deleteHandler: PropTypes.func.isRequired,
+ onFieldChange: PropTypes.func.isRequired,
+ displayHandler: PropTypes.func.isRequired,
+ displayLinks: PropTypes.arrayOf(PropTypes.bool).isRequired,
+ displaySublinks: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.bool)).isRequired,
+ hasResourceRoom: PropTypes.bool.isRequired,
+ hasResources: PropTypes.bool.isRequired,
+ errors: PropTypes.shape({
+ links: PropTypes.arrayOf(
+ PropTypes.shape({
+ title: PropTypes.string,
+ url: PropTypes.string,
+ }),
+ ),
+ sublinks: PropTypes.arrayOf(
+ PropTypes.arrayOf(
+ PropTypes.shape({
+ title: PropTypes.string,
+ url: PropTypes.string,
+ }),
+ )
+ ),
+ })
+
+};
diff --git a/src/components/navbar/NavSublinkSection.jsx b/src/components/navbar/NavSublinkSection.jsx
new file mode 100644
index 000000000..88fd8ca92
--- /dev/null
+++ b/src/components/navbar/NavSublinkSection.jsx
@@ -0,0 +1,155 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Droppable, Draggable } from 'react-beautiful-dnd';
+import styles from '../../styles/App.module.scss';
+import elementStyles from '../../styles/isomer-cms/Elements.module.scss';
+import FormField from '../FormField';
+import { isEmpty } from '../../utils';
+
+const SublinkElem = ({
+ title,
+ url,
+ linkIndex,
+ sublinkIndex,
+ onFieldChange,
+ deleteHandler,
+ shouldDisplay,
+ displayHandler,
+ errors,
+}) => (
+
+
+
+ {title}
+
+
+
+
+
+ { shouldDisplay
+ ? (
+ <>
+
+
+
+
+
+ Delete Submenu
+
+ >
+ )
+ : null}
+
+);
+
+const NavSublinkSection = ({
+ linkIndex,
+ sublinks,
+ createHandler,
+ deleteHandler,
+ onFieldChange,
+ displayHandler,
+ displaySublinks,
+ errors,
+}) => (
+
+ {(droppableProvided) => (
+ /* eslint-disable react/jsx-props-no-spreading */
+
+ { (sublinks && sublinks.length > 0)
+ ? <>
+
Submenu
+ {sublinks.map((sublink, sublinkIndex) => (
+
+ {(draggableProvided) => (
+
+
+
+ )}
+
+ ))}
+ >
+ : null}
+ {droppableProvided.placeholder}
+
Create Submenu
+
+ )}
+
+)
+
+export default NavSublinkSection
+
+SublinkElem.propTypes = {
+ title: PropTypes.string,
+ url: PropTypes.string,
+ linkIndex: PropTypes.number.isRequired,
+ sublinkIndex: PropTypes.number.isRequired,
+ onFieldChange: PropTypes.func.isRequired,
+ deleteHandler: PropTypes.func.isRequired,
+ shouldDisplay: PropTypes.bool,
+ displayHandler: PropTypes.func.isRequired,
+ errors: PropTypes.shape({
+ title: PropTypes.string,
+ url: PropTypes.string,
+ }),
+};
+
+NavSublinkSection.propTypes = {
+ linkIndex: PropTypes.number,
+ sublinks: PropTypes.arrayOf(
+ PropTypes.shape({
+ title: PropTypes.string,
+ url: PropTypes.string,
+ }),
+ ),
+ createHandler: PropTypes.func.isRequired,
+ deleteHandler: PropTypes.func.isRequired,
+ onFieldChange: PropTypes.func.isRequired,
+ displayHandler: PropTypes.func.isRequired,
+ displaySublinks: PropTypes.arrayOf(PropTypes.bool),
+ errors: PropTypes.arrayOf(
+ PropTypes.shape({
+ title: PropTypes.string,
+ url: PropTypes.string,
+ }),
+ )
+};
diff --git a/src/constants.js b/src/constants.js
new file mode 100644
index 000000000..3565f9423
--- /dev/null
+++ b/src/constants.js
@@ -0,0 +1,11 @@
+/**
+ * React query keys
+ */
+export const PAGE_CONTENT_KEY = 'page-contents';
+export const DIR_CONTENT_KEY = 'dir-contents';
+export const CSP_CONTENT_KEY = 'csp-contents';
+export const NAVIGATION_CONTENT_KEY = 'navigation-contents';
+export const RESOURCE_CATEGORY_CONTENT_KEY = 'resource-category-contents'
+export const RESOURCE_ROOM_CONTENT_KEY = 'resource-room-contents'
+export const FOLDERS_CONTENT_KEY = 'folders-contents'
+export const LAST_UPDATED_KEY = 'last-updated'
diff --git a/src/hooks/useRedirectHook.jsx b/src/hooks/useRedirectHook.jsx
new file mode 100644
index 000000000..5f7735174
--- /dev/null
+++ b/src/hooks/useRedirectHook.jsx
@@ -0,0 +1,57 @@
+import { useContext, useEffect, useState } from "react"
+import { useHistory } from "react-router-dom"
+import axios from 'axios'
+
+// Import context
+const { LoginContext } = require('../contexts/LoginContext')
+
+// constants
+const userIdKey = "userId"
+const BACKEND_URL = process.env.REACT_APP_BACKEND_URL
+
+const useRedirectHook = () => {
+ const [shouldRedirect, setShouldRedirect] = useState(false)
+ const [redirectUrl, setRedirectUrl] = useState('')
+ const [redirectComponentState, setRedirectComponentState] = useState({})
+ const history = useHistory()
+ const { setLogoutState } = useContext(LoginContext)
+
+ useEffect(() => {
+ if (shouldRedirect) {
+ setShouldRedirect(false)
+ history.push({
+ pathname: redirectUrl,
+ state: redirectComponentState
+ })
+ }
+ }, [shouldRedirect])
+
+ const setRedirectToNotFound = (siteName) => {
+ setRedirectUrl("/not-found")
+ setRedirectComponentState({ siteName })
+ setShouldRedirect(true)
+ }
+
+ const setRedirectToPage = (url) => {
+ setRedirectUrl(url)
+ setShouldRedirect(true)
+ }
+
+ const setRedirectToLogout = async () => {
+ try {
+ // Call the logout endpoint in the API server to clear the browser cookie
+ localStorage.removeItem(userIdKey)
+ await axios.get(`${BACKEND_URL}/auth/logout`)
+ setRedirectUrl("/")
+ setRedirectComponentState({ isFromSignOutButton: true })
+ setShouldRedirect(true)
+ setLogoutState()
+ } catch (err) {
+ console.error(err)
+ }
+ }
+
+ return { setRedirectToNotFound, setRedirectToPage, setRedirectToLogout }
+}
+
+export default useRedirectHook;
\ No newline at end of file
diff --git a/src/hooks/useSiteColorsHook.jsx b/src/hooks/useSiteColorsHook.jsx
new file mode 100644
index 000000000..30e34d8e3
--- /dev/null
+++ b/src/hooks/useSiteColorsHook.jsx
@@ -0,0 +1,61 @@
+// Utils
+import { defaultSiteColors, getSiteColors, createPageStyleSheet } from '../utils/siteColorUtils';
+
+// Constants
+const LOCAL_STORAGE_SITE_COLORS = 'isomercms_colors'
+
+const useSiteColorsHook = () => {
+ const getLocalStorageSiteColors = () => {
+ const localStorageSiteColors = JSON.parse(localStorage.getItem(LOCAL_STORAGE_SITE_COLORS))
+ return localStorageSiteColors
+ }
+
+ const setLocalStorageSiteColors = (newSiteColors) => {
+ localStorage.setItem(LOCAL_STORAGE_SITE_COLORS, JSON.stringify(newSiteColors))
+ }
+
+ const retrieveSiteColors = async (siteName) => {
+ const siteColors = getLocalStorageSiteColors()
+ // if (!siteColors[siteName]) {
+ if (!siteColors || !siteColors[siteName]) {
+
+ const {
+ primaryColor,
+ secondaryColor,
+ } = await getSiteColors(siteName)
+
+ setLocalStorageSiteColors({
+ ...siteColors,
+ [siteName]: {
+ primaryColor,
+ secondaryColor,
+ },
+ })
+ }
+ }
+
+ const generatePageStyleSheet = (siteName) => {
+ const siteColors = getLocalStorageSiteColors()
+
+ let primaryColor = defaultSiteColors.default.primaryColor
+ let secondaryColor = defaultSiteColors.default.secondaryColor
+
+ if (siteColors[siteName]) {
+ const {
+ primaryColor: sitePrimaryColor,
+ secondaryColor: siteSecondaryColor,
+ } = siteColors[siteName]
+ primaryColor = sitePrimaryColor
+ secondaryColor = siteSecondaryColor
+ }
+
+ createPageStyleSheet(siteName, primaryColor, secondaryColor)
+ }
+
+ return {
+ retrieveSiteColors,
+ generatePageStyleSheet,
+ }
+}
+
+export default useSiteColorsHook;
\ No newline at end of file
diff --git a/src/hooks/useSiteUrlHook.jsx b/src/hooks/useSiteUrlHook.jsx
new file mode 100644
index 000000000..07223b4e4
--- /dev/null
+++ b/src/hooks/useSiteUrlHook.jsx
@@ -0,0 +1,48 @@
+import axios from 'axios';
+// Constants
+const LOCAL_STORAGE_SITE_URL = 'isomercms_site_url'
+
+const useSiteUrlHook = () => {
+ const getSiteUrl = (siteName, type) => {
+ const siteUrlObj = JSON.parse(localStorage.getItem(LOCAL_STORAGE_SITE_URL))
+ if (siteUrlObj && siteUrlObj[siteName]) return siteUrlObj[siteName][type]
+ return null
+ }
+
+ const setSiteUrl = (newUrl, siteName, type) => {
+ const siteUrlObj = JSON.parse(localStorage.getItem(LOCAL_STORAGE_SITE_URL)) || {}
+ if (!(siteUrlObj.hasOwnProperty(siteName))) siteUrlObj[siteName] = {}
+ siteUrlObj[siteName][type] = newUrl
+ localStorage.setItem(LOCAL_STORAGE_SITE_URL, JSON.stringify({...siteUrlObj}))
+ }
+
+ const retrieveUrl = async (siteName, type) => {
+ const localStorageSiteUrl = getSiteUrl(siteName, type)
+ if (!localStorageSiteUrl) {
+ let url
+ if (type === 'staging') {
+ const resp = await axios.get(`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/stagingUrl`);
+ url = resp.data.stagingUrl
+ }
+ if (type === 'site') {
+ const settingsResp = await axios.get(`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/settings`);
+ url = settingsResp.data.settings.configFieldsRequired.url;
+ }
+ setSiteUrl(url, siteName, type)
+ return url
+ }
+ return localStorageSiteUrl
+ }
+
+ const retrieveStagingUrl = async(siteName) => {
+ return await retrieveUrl(siteName, 'staging')
+ }
+
+ const retrieveSiteUrl = async(siteName) => {
+ return await retrieveUrl(siteName, 'site')
+ }
+
+ return { retrieveStagingUrl, retrieveSiteUrl }
+}
+
+export default useSiteUrlHook;
\ No newline at end of file
diff --git a/src/layouts/AuthCallback.jsx b/src/layouts/AuthCallback.jsx
index 4331f305c..a6ee9974f 100644
--- a/src/layouts/AuthCallback.jsx
+++ b/src/layouts/AuthCallback.jsx
@@ -1,7 +1,12 @@
-import React, { useEffect } from 'react'
+import React, { useContext, useEffect } from 'react'
import { Redirect } from 'react-router-dom';
-const AuthCallback = ({ setLogin, isLoggedIn }) => {
+// Import contexts
+const { LoginContext } = require('../contexts/LoginContext')
+
+const AuthCallback = () => {
+ const { setLogin, isLoggedIn } = useContext(LoginContext)
+
useEffect(() => {
if (!isLoggedIn) setLogin()
}, [isLoggedIn])
diff --git a/src/layouts/CategoryPages.jsx b/src/layouts/CategoryPages.jsx
index 5c45cb7f6..288d0bd85 100644
--- a/src/layouts/CategoryPages.jsx
+++ b/src/layouts/CategoryPages.jsx
@@ -1,6 +1,8 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import PropTypes from 'prop-types';
+import { useQuery } from 'react-query';
+import { Link } from 'react-router-dom';
// Import components
import Header from '../components/Header';
@@ -12,7 +14,11 @@ import elementStyles from '../styles/isomer-cms/Elements.module.scss';
import contentStyles from '../styles/isomer-cms/pages/Content.module.scss';
//Import utils
-import { retrieveResourceFileMetadata } from '../utils.js'
+import { retrieveResourceFileMetadata, deslugifyDirectory } from '../utils.js'
+import { errorToast } from '../utils/toasts';
+import { getResourcePages } from '../api'
+import { RESOURCE_CATEGORY_CONTENT_KEY } from '../constants'
+import useRedirectHook from '../hooks/useRedirectHook';
// Constants
const BACKEND_URL = process.env.REACT_APP_BACKEND_URL
@@ -33,14 +39,31 @@ const getBackButtonInfo = (pathname) => {
const CategoryPages = ({ match, location, isResource }) => {
const { backButtonLabel, backButtonUrl } = getBackButtonInfo(location.pathname)
const { collectionName, siteName } = match.params;
+ const { setRedirectToPage } = useRedirectHook()
const [categoryPages, setCategoryPages] = useState()
+ const { data: resourcePagesResp, refetch: refetchPages } = useQuery(
+ [RESOURCE_CATEGORY_CONTENT_KEY, siteName, collectionName, isResource],
+ () => getResourcePages(siteName, collectionName),
+ {
+ retry: false,
+ onError: (err) => {
+ console.log(err)
+ if (err.response && err.response.status === 404 && isResource) {
+ setRedirectToPage(`/sites/${siteName}/resources`)
+ } else {
+ errorToast()
+ }
+ }
+ },
+ );
+
useEffect(() => {
let _isMounted = true
const fetchData = async () => {
if (isResource) {
- const resourcePagesResp = await axios.get(`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/resources/${collectionName}`);
+ if (!resourcePagesResp) return
const { resourcePages } = resourcePagesResp.data;
if (resourcePages.length > 0) {
@@ -64,11 +87,12 @@ const CategoryPages = ({ match, location, isResource }) => {
}
fetchData()
return () => { _isMounted = false }
- }, [])
+ }, [resourcePagesResp])
return (
<>
@@ -79,7 +103,19 @@ const CategoryPages = ({ match, location, isResource }) => {
{/* Collection title */}
-
{collectionName}
+ {deslugifyDirectory(collectionName)}
+
+
+
+ Resources >
+ {
+ collectionName
+ ? (
+ {deslugifyDirectory(collectionName)}
+ )
+ : null
+ }
+
{/* Collection pages */}
{
pages={categoryPages}
siteName={siteName}
isResource={isResource}
+ refetchPages={refetchPages}
/>
{/* main section ends here */}
diff --git a/src/layouts/Collections.jsx b/src/layouts/Collections.jsx
deleted file mode 100644
index e070fa20f..000000000
--- a/src/layouts/Collections.jsx
+++ /dev/null
@@ -1,158 +0,0 @@
-import React, { Component } from 'react';
-import { Link } from 'react-router-dom';
-import axios from 'axios';
-import PropTypes from 'prop-types';
-
-export default class Collections extends Component {
- constructor(props) {
- super(props);
- this.state = {
- collections: [],
- newCollectionName: null,
- deleteCollectionName: null,
- oldCollectionName: null,
- renameCollectionName: null,
- };
- }
-
- async componentDidMount() {
- try {
- const { match } = this.props;
- const { siteName } = match.params;
- const resp = await axios.get(`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/collections`, {
- withCredentials: true,
- });
- const { collections } = resp.data;
- this.setState({ collections });
- } catch (err) {
- console.log(err);
- }
- }
-
- createCollection = async () => {
- try {
- const { match } = this.props;
- const { siteName } = match.params;
- const { newCollectionName: collectionName } = this.state;
- const params = {
- collectionName,
- };
- await axios.post(`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/collections`, params, {
- withCredentials: true,
- });
- } catch (err) {
- console.log(err);
- }
- }
-
- deleteCollection = async () => {
- try {
- const { match } = this.props;
- const { siteName } = match.params;
- const { deleteCollectionName: collectionName } = this.state;
- await axios.delete(`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/collections/${collectionName}`, {
- withCredentials: true,
- });
- } catch (err) {
- console.log(err);
- }
- }
-
- renameCollection = async () => {
- try {
- const { match } = this.props;
- const { siteName } = match.params;
- const { oldCollectionName, renameCollectionName } = this.state;
- await axios.post(`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/collections/${oldCollectionName}/rename/${renameCollectionName}`, '', {
- withCredentials: true,
- });
- } catch (err) {
- console.log(err);
- }
- }
-
- updateNewCollectionName = (event) => {
- event.preventDefault();
- this.setState({ newCollectionName: event.target.value });
- }
-
- updateDeleteCollectionName = (event) => {
- event.preventDefault();
- this.setState({ deleteCollectionName: event.target.value });
- }
-
- updateDeleteCollectionName = (event) => {
- event.preventDefault();
- this.setState({ deleteCollectionName: event.target.value });
- }
-
- updateOldCollectionName = (event) => {
- event.preventDefault();
- this.setState({ oldCollectionName: event.target.value });
- }
-
- updateRenameCollectionName = (event) => {
- event.preventDefault();
- this.setState({ renameCollectionName: event.target.value });
- }
-
- render() {
- const { collections } = this.state;
- const { match } = this.props;
- const { siteName } = match.params;
- return (
-
-
Back to Sites
-
-
{siteName}
-
-
- Pages
-
-
- Collections
-
-
- Images
-
-
- Files
-
-
- Menus
-
-
-
-
Collections
- {collections.length > 0
- ? collections.map((collection) => (
-
- {collection}
-
- ))
- : 'No collections'}
-
-
-
-
Create new collection
-
-
-
-
Delete collection
-
-
-
-
-
Rename collection
-
- );
- }
-}
-
-Collections.propTypes = {
- match: PropTypes.shape({
- params: PropTypes.shape({
- siteName: PropTypes.string,
- }),
- }).isRequired,
-};
diff --git a/src/layouts/EditContactUs.jsx b/src/layouts/EditContactUs.jsx
index 4b74aadf0..8c3f035a1 100644
--- a/src/layouts/EditContactUs.jsx
+++ b/src/layouts/EditContactUs.jsx
@@ -1,42 +1,37 @@
// TODO: Clean up formatting, semi-colons, PropTypes etc
-import React, { Component } from 'react';
+import React, { useEffect, useState } from 'react';
import axios from 'axios';
import _ from 'lodash';
-import { Base64 } from 'js-base64';
import PropTypes from 'prop-types';
import update from 'immutability-helper';
import { DragDropContext } from 'react-beautiful-dnd';
-import { Redirect } from 'react-router-dom'
-import { toast } from 'react-toastify';
-import { DEFAULT_ERROR_TOAST_MSG, frontMatterParser, concatFrontMatterMdBody, isEmpty, retrieveResourceFileMetadata } from '../utils';
+import EditorSection from '../components/contact-us/Section';
+import Header from '../components/Header';
+import LoadingButton from '../components/LoadingButton';
+import FormField from '../components/FormField';
+import DeleteWarningModal from '../components/DeleteWarningModal';
+import GenericWarningModal from '../components/GenericWarningModal';
+
+import { DEFAULT_RETRY_MSG, frontMatterParser, concatFrontMatterMdBody, isEmpty } from '../utils';
import { sanitiseFrontMatter } from '../utils/contact-us/dataSanitisers';
import { validateFrontMatter } from '../utils/contact-us/validators';
import { validateContactType, validateLocationType } from '../utils/validators';
-import {
- createPageStyleSheet,
- getSiteColors,
-} from '../utils/siteColorUtils';
-
-
-import EditorSection from '../components/contact-us/Section';
-import Toast from '../components/Toast';
+import { errorToast } from '../utils/toasts';
import '../styles/isomer-template.scss';
import elementStyles from '../styles/isomer-cms/Elements.module.scss';
import editorStyles from '../styles/isomer-cms/pages/Editor.module.scss';
-import Header from '../components/Header';
-import LoadingButton from '../components/LoadingButton';
-import FormField from '../components/FormField';
import TemplateContactUsHeader from '../templates/contact-us/ContactUsHeader';
import TemplateLocationsSection from '../templates/contact-us/LocationsSection'
import TemplateContactsSection from '../templates/contact-us/ContactsSection'
import TemplateFeedbackSection from '../templates/contact-us/FeedbackSection';
-import DeleteWarningModal from '../components/DeleteWarningModal';
-import GenericWarningModal from '../components/GenericWarningModal';
+// Import hooks
+import useSiteColorsHook from '../hooks/useSiteColorsHook';
+import useRedirectHook from '../hooks/useRedirectHook';
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable react/no-array-index-key */
@@ -111,192 +106,159 @@ const displayDeletedFrontMatter = (deletedFrontMatter) => {
return displayText
}
-export default class EditContactUs extends Component {
- _isMounted = false
-
- constructor(props) {
- super(props);
- this.scrollRefs = {
- sectionsScrollRefs: {
- locations: null,
- contacts: null,
- header: null,
- feedback: null,
- },
- contacts: [],
- locations: [],
- };
- this.state = {
- frontMatter: {},
- originalFrontMatter: {},
- deletedFrontMatter: {},
- sanitisedOriginalFrontMatter: {},
- frontMatterSha: null,
- footerContent: {},
- originalFooterContent: {},
- footerSha: null,
- displaySections: {
- sectionsDisplay: {
- locations: false,
- contacts: false,
- },
- contacts: [],
- locations: [],
- },
- errors: {
- contacts: [],
- locations: [],
- },
- itemPendingForDelete: {
- id: null,
- type: '',
- },
- showDeletedText: true,
- shouldRedirectToNotFound: false,
- };
- }
+const EditContactUs = ({ match }) => {
+ const { retrieveSiteColors, generatePageStyleSheet } = useSiteColorsHook()
+ const { setRedirectToNotFound } = useRedirectHook()
+
+ const { siteName } = match.params;
+ const [hasLoaded, setHasLoaded] = useState(false)
+ const [scrollRefs, setScrollRefs] = useState({
+ sectionsScrollRefs: {
+ locations: null,
+ contacts: null,
+ header: null,
+ feedback: null,
+ },
+ contacts: [],
+ locations: [],
+ })
+ const [frontMatter, setFrontMatter] = useState({})
+ const [originalFrontMatter, setOriginalFrontMatter] = useState({})
+ const [deletedFrontMatter, setDeletedFrontMatter] = useState({})
+ const [sanitisedOriginalFrontMatter, setSanitisedOriginalFrontMatter] = useState({})
+ const [frontMatterSha, setFrontMatterSha] = useState(null)
+ const [footerContent, setFooterContent] = useState({})
+ const [originalFooterContent, setOriginalFooterContent] = useState({})
+ const [footerSha, setFooterSha] = useState(null)
+ const [displaySections, setDisplaySections] = useState({
+ sectionsDisplay: {
+ locations: false,
+ contacts: false,
+ },
+ contacts: [],
+ locations: [],
+ })
+ const [errors, setErrors] = useState({
+ contacts: [],
+ locations: [],
+ })
+ const [itemPendingForDelete, setItemPendingForDelete] = useState({
+ id: null,
+ type: '',
+ })
+ const [showDeletedText, setShowDeletedText] = useState(true)
- async componentDidMount() {
- const { match, siteColors, setSiteColors } = this.props;
- const { siteName } = match.params;
- this._isMounted = true
+ useEffect(() => {
+ let _isMounted = true
let content, sha, footerContent, footerSha
- // Set page colors
- try {
- let primaryColor
- let secondaryColor
-
- if (!siteColors[siteName]) {
- const {
- primaryColor: sitePrimaryColor,
- secondaryColor: siteSecondaryColor,
- } = await getSiteColors(siteName)
-
- primaryColor = sitePrimaryColor
- secondaryColor = siteSecondaryColor
-
- if (this._isMounted) setSiteColors((prevState) => ({
- ...prevState,
- [siteName]: {
- primaryColor,
- secondaryColor,
- }
- }))
- } else {
- primaryColor = siteColors[siteName].primaryColor
- secondaryColor = siteColors[siteName].secondaryColor
+ const loadContactUsDetails = async () => {
+ // Set page colors
+ try {
+ await retrieveSiteColors(siteName)
+ generatePageStyleSheet(siteName)
+ } catch (err) {
+ console.log(err);
}
- createPageStyleSheet(siteName, primaryColor, secondaryColor)
-
- } catch (err) {
- console.log(err);
- }
-
- try {
- const contactUsResp = await axios.get(`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/pages/contact-us.md`);
- const { content:pageContent , sha:pageSha } = contactUsResp.data;
- content = pageContent
- sha = pageSha
- } catch (error) {
- if (error?.response?.status === 404) {
- this.setState({ shouldRedirectToNotFound: true })
- } else {
- toast(
- ,
- {className: `${elementStyles.toastError} ${elementStyles.toastLong}`}
- );
+ try {
+ const contactUsResp = await axios.get(`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/pages/contact-us.md`);
+ const { content:pageContent , sha:pageSha } = contactUsResp.data;
+ content = pageContent
+ sha = pageSha
+ } catch (error) {
+ if (error?.response?.status === 404) {
+ setRedirectToNotFound(siteName)
+ } else {
+ errorToast(`There was a problem trying to load your contact us page. ${DEFAULT_RETRY_MSG}`)
+ }
+ console.log(error)
}
- console.log(error)
- }
- if (!content) return
+ if (!content) return
- try {
- const settingsResp = await axios.get(`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/settings`)
- const { footerContent:retrievedContent, footerSha:retrievedSha } = settingsResp.data.settings;
- footerContent = retrievedContent
- footerSha = retrievedSha
- } catch (err) {
- toast(
- ,
- {className: `${elementStyles.toastError} ${elementStyles.toastLong}`}
- );
- console.log(err);
- }
+ try {
+ const settingsResp = await axios.get(`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/settings`)
+ const { footerContent:retrievedContent, footerSha:retrievedSha } = settingsResp.data.settings;
+ footerContent = retrievedContent
+ footerSha = retrievedSha
+ } catch (err) {
+ errorToast(`There was a problem trying to load your contact us page. ${DEFAULT_RETRY_MSG}`)
+ console.log(err);
+ }
- if (!footerContent) return
+ if (!footerContent) return
- // split the markdown into front matter and content
- const { frontMatter } = frontMatterParser(Base64.decode(content));
+ // split the markdown into front matter and content
+ const { frontMatter } = frontMatterParser(content);
- // data cleaning for non-comforming data
- const { sanitisedFrontMatter, deletedFrontMatter } = sanitiseFrontMatter(frontMatter)
- const { contacts, locations } = sanitisedFrontMatter
- const { contactsErrors, locationsErrors } = validateFrontMatter(sanitisedFrontMatter)
+ // data cleaning for non-comforming data
+ const { sanitisedFrontMatter, deletedFrontMatter } = sanitiseFrontMatter(frontMatter)
+ const { contacts, locations } = sanitisedFrontMatter
+ const { contactsErrors, locationsErrors } = validateFrontMatter(sanitisedFrontMatter)
- const contactsDisplay = [], locationsDisplay = []
- const contactsScrollRefs = [], locationsScrollRefs = []
+ const contactsDisplay = [], locationsDisplay = []
+ const contactsScrollRefs = [], locationsScrollRefs = []
- const sectionsDisplay = {
- contacts: false,
- locations: false
- }
-
- const sectionsScrollRefs = {
- header: React.createRef(),
- feedback: React.createRef(),
- contacts: React.createRef(),
- locations: React.createRef(),
- }
+ const sectionsDisplay = {
+ contacts: false,
+ locations: false
+ }
+
+ const sectionsScrollRefs = {
+ header: React.createRef(),
+ feedback: React.createRef(),
+ contacts: React.createRef(),
+ locations: React.createRef(),
+ }
- contacts.forEach(_ => {
- contactsDisplay.push(false)
- contactsScrollRefs.push(React.createRef())
- })
+ contacts.forEach(_ => {
+ contactsDisplay.push(false)
+ contactsScrollRefs.push(React.createRef())
+ })
- locations.forEach(_ => {
- locationsDisplay.push(false)
- locationsScrollRefs.push(React.createRef())
- })
-
- this.scrollRefs = {
- sectionsScrollRefs,
- contacts: contactsScrollRefs,
- locations: locationsScrollRefs,
+ locations.forEach(_ => {
+ locationsDisplay.push(false)
+ locationsScrollRefs.push(React.createRef())
+ })
+
+ if (_isMounted) {
+ setScrollRefs({
+ sectionsScrollRefs,
+ contacts: contactsScrollRefs,
+ locations: locationsScrollRefs,
+ })
+ setFooterContent(footerContent)
+ setOriginalFooterContent(_.cloneDeep(footerContent))
+ setFooterSha(footerSha)
+ setFrontMatter(sanitisedFrontMatter)
+ setOriginalFrontMatter(_.cloneDeep(frontMatter))
+ setDeletedFrontMatter(deletedFrontMatter)
+ setSanitisedOriginalFrontMatter(_.cloneDeep(sanitisedFrontMatter))
+ setFrontMatterSha(sha)
+ setDisplaySections({
+ sectionsDisplay,
+ contacts: contactsDisplay,
+ locations: locationsDisplay,
+ })
+ setErrors({
+ contacts: contactsErrors,
+ locations: locationsErrors,
+ })
+ setHasLoaded(true)
+ }
}
- if (this._isMounted) this.setState({
- originalFooterContent: _.cloneDeep(footerContent),
- footerContent,
- footerSha,
- originalFrontMatter: _.cloneDeep(frontMatter),
- deletedFrontMatter,
- sanitisedOriginalFrontMatter: _.cloneDeep(sanitisedFrontMatter),
- frontMatter: sanitisedFrontMatter,
- frontMatterSha: sha,
- displaySections: {
- sectionsDisplay,
- contacts: contactsDisplay,
- locations: locationsDisplay,
- },
- errors: {
- contacts: contactsErrors,
- locations: locationsErrors,
- },
- });
- }
+ loadContactUsDetails()
- componentWillUnmount() {
- this._isMounted = false;
- }
+ return () => {
+ _isMounted = false
+ }
+ }, [])
- onDragEnd = (result) => {
- const { scrollRefs, state } = this;
+ const onDragEnd = (result) => {
const { source, destination, type } = result;
- const { frontMatter, displaySections, errors } = state;
// If the user dropped the draggable to no known droppable
if (!destination) return;
@@ -346,21 +308,16 @@ export default class EditContactUs extends Component {
})
// scroll to new location of dragged element
- this.scrollRefs[type][destination.index].current.scrollIntoView()
- this.scrollRefs = newScrollRefs
+ scrollRefs[type][destination.index].current.scrollIntoView()
- this.setState({
- frontMatter: newFrontMatter,
- errors: newErrors,
- displaySections: newDisplaySections,
- });
+ setScrollRefs(newScrollRefs)
+ setFrontMatter(newFrontMatter)
+ setErrors(newErrors)
+ setDisplaySections(newDisplaySections)
}
- onFieldChange = async (event) => {
+ const onFieldChange = async (event) => {
try {
- const { scrollRefs, state } = this;
- const { frontMatter, footerContent } = state
- const { errors } = state;
const { id, value } = event.target;
const idArray = id.split('-');
const elemType = idArray[0];
@@ -463,22 +420,17 @@ export default class EditContactUs extends Component {
break;
}
}
- this.setState((currState) => ({ // we check explicitly for undefined
- frontMatter: _.isUndefined(newFrontMatter) ? currState.frontMatter : newFrontMatter,
- footerContent: _.isUndefined(newFooterContent) ? currState.footerContent : newFooterContent,
- errors: _.isUndefined(newErrors) ? currState.errors : newErrors,
- }));
-
+ setFrontMatter(_.isUndefined(newFrontMatter) ? frontMatter : newFrontMatter)
+ setFooterContent(_.isUndefined(newFooterContent) ? footerContent : newFooterContent)
+ setErrors(_.isUndefined(newErrors) ? errors : newErrors)
} catch (err) {
console.log(err);
}
}
- createHandler = async (event) => {
+ const createHandler = async (event) => {
const { id } = event.target;
try {
- const { scrollRefs, state } = this;
- const { frontMatter, displaySections, errors } = state;
const { contacts: contactsDisplay, locations: locationsDisplay } = displaySections
const resetDisplaySections = {
@@ -510,24 +462,17 @@ export default class EditContactUs extends Component {
scrollRefs.sectionsScrollRefs[id].current.scrollIntoView()
}
- this.scrollRefs = newScrollRefs;
-
- this.setState({
- frontMatter: newFrontMatter,
- errors: newErrors,
- displaySections: newDisplaySections,
- });
-
+ setScrollRefs(newScrollRefs)
+ setFrontMatter(newFrontMatter)
+ setErrors(newErrors)
+ setDisplaySections(newDisplaySections)
} catch (err) {
console.log(err);
}
}
- deleteHandler = async (id) => {
+ const deleteHandler = async (id) => {
try {
- const { scrollRefs, state } = this;
- const { frontMatter, displaySections, errors } = state;
-
const idArray = id.split('-');
const elemType = idArray[0];
const sectionIndex = parseInt(idArray[1], RADIX_PARSE_INT);
@@ -545,23 +490,18 @@ export default class EditContactUs extends Component {
[elemType]: {$splice: [[sectionIndex, 1]]},
});
- this.scrollRefs = newScrollRefs;
-
- this.setState({
- frontMatter: newFrontMatter,
- errors: newErrors,
- displaySections: newDisplaySections,
- });
+ setScrollRefs(newScrollRefs)
+ setFrontMatter(newFrontMatter)
+ setErrors(newErrors)
+ setDisplaySections(newDisplaySections)
} catch (err) {
console.log(err);
}
}
- displayHandler = async (event) => {
+ const displayHandler = async (event) => {
try {
- const { state, scrollRefs } = this;
- const { displaySections } = state;
const { contacts: contactsDisplay, locations: locationsDisplay } = displaySections;
const { id } = event.target;
@@ -600,33 +540,27 @@ export default class EditContactUs extends Component {
}
}
- this.setState({
- displaySections: newDisplaySections,
- });
-
+ setDisplaySections(newDisplaySections)
+
} catch (err) {
console.log(err);
}
}
- savePage = async () => {
+ const savePage = async () => {
try {
- const { state } = this;
- const { match } = this.props;
- const { siteName } = match.params;
-
// Update contact-us
// Filter out components which have no input
- let filteredFrontMatter = _.cloneDeep(state.frontMatter)
+ let filteredFrontMatter = _.cloneDeep(frontMatter)
let newContacts = [];
- state.frontMatter.contacts.forEach((contact) => {
+ frontMatter.contacts.forEach((contact) => {
if ( !isEmpty(contact) ) {
newContacts.push(_.cloneDeep(contact))
}
})
let newLocations = [];
- state.frontMatter.locations.forEach((location) => {
+ frontMatter.locations.forEach((location) => {
if ( !isEmpty(location) ) {
let newLocation = _.cloneDeep(location);
let newOperatingHours = [];
@@ -648,29 +582,27 @@ export default class EditContactUs extends Component {
if (!filteredFrontMatter.locations.length) delete filteredFrontMatter.locations
const content = concatFrontMatterMdBody(filteredFrontMatter, '');
- const base64EncodedContent = Base64.encode(content);
const frontMatterParams = {
- content: base64EncodedContent,
- sha: state.frontMatterSha,
+ content,
+ sha: frontMatterSha,
};
- if (JSON.stringify(state.originalFrontMatter) !== JSON.stringify(state.frontMatter)) {
+ if (JSON.stringify(originalFrontMatter) !== JSON.stringify(frontMatter)) {
await axios.post(`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/pages/contact-us.md`, frontMatterParams, {
withCredentials: true,
});
}
-
// // Update settings
- let updatedFooterContents = _.cloneDeep(state.footerContent)
+ let updatedFooterContents = _.cloneDeep(footerContent)
const footerParams = {
footerSettings: updatedFooterContents,
- footerSha: state.footerSha,
+ footerSha: footerSha,
};
- if (JSON.stringify(state.footerContent) !== JSON.stringify(state.originalFooterContent)) {
+ if (JSON.stringify(footerContent) !== JSON.stringify(originalFooterContent)) {
await axios.post(`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/settings`, footerParams, {
withCredentials: true,
});
@@ -678,70 +610,49 @@ export default class EditContactUs extends Component {
window.location.reload();
} catch (err) {
- toast(
- ,
- {className: `${elementStyles.toastError} ${elementStyles.toastLong}`}
- );
+ errorToast(`There was a problem trying to save your contact us page. ${DEFAULT_RETRY_MSG}`)
console.log(err);
}
}
- render() {
- const { state, scrollRefs } = this
- const {
- footerContent,
- originalFooterContent,
- originalFrontMatter,
- deletedFrontMatter,
- sanitisedOriginalFrontMatter,
- frontMatter,
- displaySections,
- frontMatterSha,
- footerSha,
- errors,
- itemPendingForDelete,
- showDeletedText,
- } = state;
- const { match } = this.props;
- const { siteName } = match.params;
-
- const { agency_name: agencyName, contacts, locations } = frontMatter
- const { feedback } = footerContent
- const { sectionsDisplay } = displaySections
- const { sectionsScrollRefs } = scrollRefs
-
- const hasDeletions = !isEmpty(deletedFrontMatter)
- const hasErrors = !isEmpty(errors.contacts) || !isEmpty(errors.locations);
- const hasChanges = JSON.stringify(sanitisedOriginalFrontMatter) === JSON.stringify(frontMatter) && JSON.stringify(footerContent) === JSON.stringify(originalFooterContent);
-
- return (
- <>
- { showDeletedText && hasDeletions
- &&
- ${displayDeletedFrontMatter(deletedFrontMatter)}`}
- onProceed={()=>{this.setState({showDeletedText: false})}}
- proceedText="Acknowledge"
- />
- }
- {
- itemPendingForDelete.id
- && (
- this.setState({ itemPendingForDelete: { id: null, type: '' } })}
- onDelete={() => { this.deleteHandler(itemPendingForDelete.id); this.setState({ itemPendingForDelete: { id: null, type: '' } }); }}
- type={itemPendingForDelete.type}
- />
- )
- }
- {
+ return !isEmpty(errors.contacts) || !isEmpty(errors.locations)
+ }
+
+ const hasChanges = () => {
+ return JSON.stringify(sanitisedOriginalFrontMatter) === JSON.stringify(frontMatter) && JSON.stringify(footerContent) === JSON.stringify(originalFooterContent)
+ }
+
+ return (
+ <>
+ { showDeletedText && !isEmpty(deletedFrontMatter)
+ &&
+ ${displayDeletedFrontMatter(deletedFrontMatter)}`}
+ onProceed={()=>{setShowDeletedText(false)}}
+ proceedText="Acknowledge"
+ />
+ }
+ {
+ itemPendingForDelete.id
+ && (
+ setItemPendingForDelete({ id: null, type: '' })}
+ onDelete={() => { deleteHandler(itemPendingForDelete.id); setItemPendingForDelete({ id: null, type: '' }); }}
+ type={itemPendingForDelete.type}
/>
+ )
+ }
+
+ { hasLoaded &&
@@ -752,39 +663,39 @@ export default class EditContactUs extends Component {
-
+
this.setState({ itemPendingForDelete: { id: event.target.id, type } })}
- shouldDisplay={sectionsDisplay.locations}
+ cards={frontMatter.locations}
+ onFieldChange={onFieldChange}
+ createHandler={createHandler}
+ deleteHandler={(event, type) => setItemPendingForDelete({ id: event.target.id, type })}
+ shouldDisplay={displaySections.sectionsDisplay.locations}
displayCards={displaySections.locations}
- displayHandler={this.displayHandler}
+ displayHandler={displayHandler}
errors={errors.locations}
sectionId={'locations'}
/>
this.setState({ itemPendingForDelete: { id: event.target.id, type } })}
- shouldDisplay={sectionsDisplay.contacts}
+ cards={frontMatter.contacts}
+ onFieldChange={onFieldChange}
+ createHandler={createHandler}
+ deleteHandler={(event, type) => setItemPendingForDelete({ id: event.target.id, type })}
+ shouldDisplay={displaySections.sectionsDisplay.contacts}
displayCards={displaySections.contacts}
- displayHandler={this.displayHandler}
+ displayHandler={displayHandler}
errors={errors.contacts}
sectionId={'contacts'}
/>
@@ -795,8 +706,8 @@ export default class EditContactUs extends Component {
{/* contact-us header */}
{/* contact-us content */}
@@ -804,18 +715,18 @@ export default class EditContactUs extends Component {
@@ -823,36 +734,29 @@ export default class EditContactUs extends Component {
- { hasDeletions &&
+ { !isEmpty(deletedFrontMatter) &&
{this.setState({showDeletedText: true})}}
+ callback={() => {setShowDeletedText(true)}}
/>
}
-
- {
- this.state.shouldRedirectToNotFound &&
-
- }
- >
- );
- }
+
+ }
+ >
+ )
}
+export default EditContactUs
+
EditContactUs.propTypes = {
match: PropTypes.shape({
params: PropTypes.shape({
diff --git a/src/layouts/EditHomepage.jsx b/src/layouts/EditHomepage.jsx
index e7c6b71ea..9cc6b1e56 100644
--- a/src/layouts/EditHomepage.jsx
+++ b/src/layouts/EditHomepage.jsx
@@ -1,34 +1,35 @@
-import React, { Component } from 'react';
+import React, { useEffect, createRef, useState } from 'react';
import axios from 'axios';
import _ from 'lodash';
-import { Base64 } from 'js-base64';
import PropTypes from 'prop-types';
import update from 'immutability-helper';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
-import '../styles/isomer-template.scss';
-import TemplateHeroSection from '../templates/homepage/HeroSection';
-import TemplateInfobarSection from '../templates/homepage/InfobarSection';
-import TemplateInfopicLeftSection from '../templates/homepage/InfopicLeftSection';
-import TemplateInfopicRightSection from '../templates/homepage/InfopicRightSection';
-import TemplateResourcesSection from '../templates/homepage/ResourcesSection';
-import { DEFAULT_ERROR_TOAST_MSG, frontMatterParser, concatFrontMatterMdBody } from '../utils';
-import {
- createPageStyleSheet,
- getSiteColors,
-} from '../utils/siteColorUtils';
+
import EditorInfobarSection from '../components/homepage/InfobarSection';
import EditorInfopicSection from '../components/homepage/InfopicSection';
import EditorResourcesSection from '../components/homepage/ResourcesSection';
import EditorHeroSection from '../components/homepage/HeroSection';
import NewSectionCreator from '../components/homepage/NewSectionCreator';
-import elementStyles from '../styles/isomer-cms/Elements.module.scss';
-import editorStyles from '../styles/isomer-cms/pages/Editor.module.scss';
import Header from '../components/Header';
import LoadingButton from '../components/LoadingButton';
-import { validateSections, validateHighlights, validateDropdownElems } from '../utils/validators';
import DeleteWarningModal from '../components/DeleteWarningModal';
-import { toast } from 'react-toastify';
-import Toast from '../components/Toast';
+
+import TemplateHeroSection from '../templates/homepage/HeroSection';
+import TemplateInfobarSection from '../templates/homepage/InfobarSection';
+import TemplateInfopicLeftSection from '../templates/homepage/InfopicLeftSection';
+import TemplateInfopicRightSection from '../templates/homepage/InfopicRightSection';
+import TemplateResourcesSection from '../templates/homepage/ResourcesSection';
+
+import { frontMatterParser, concatFrontMatterMdBody, DEFAULT_RETRY_MSG } from '../utils';
+import { validateSections, validateHighlights, validateDropdownElems } from '../utils/validators';
+import { errorToast } from '../utils/toasts';
+
+import '../styles/isomer-template.scss';
+import elementStyles from '../styles/isomer-cms/Elements.module.scss';
+import editorStyles from '../styles/isomer-cms/pages/Editor.module.scss';
+
+// Import hooks
+import useSiteColorsHook from '../hooks/useSiteColorsHook';
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable react/no-array-index-key */
@@ -97,189 +98,184 @@ const enumSection = (type, isErrorConstructor) => {
}
};
-export default class EditHomepage extends Component {
- _isMounted = false
-
- constructor(props) {
- super(props);
- this.scrollRefs = [];
- this.state = {
- frontMatter: {
- title: '',
- subtitle: '',
- description: '',
- image: '',
- notification: '',
- sections: [],
- },
- sha: null,
- hasResources: false,
- dropdownIsActive: false,
- displaySections: [],
- displayHighlights: [],
- displayDropdownElems: [],
- errors: {
- sections: [],
- highlights: [],
- dropdownElems: [],
- },
- itemPendingForDelete: {
- id: '',
- type: '',
- },
- savedHeroElems: '',
- savedHeroErrors: '',
- };
- }
-
- async componentDidMount() {
- const { match, siteColors, setSiteColors } = this.props;
- const { siteName } = match.params;
- this._isMounted = true
-
- // Set page colors
- try {
- let primaryColor
- let secondaryColor
-
- if (!siteColors[siteName]) {
- const {
- primaryColor: sitePrimaryColor,
- secondaryColor: siteSecondaryColor,
- } = await getSiteColors(siteName)
-
- primaryColor = sitePrimaryColor
- secondaryColor = siteSecondaryColor
-
- if (this._isMounted) setSiteColors((prevState) => ({
- ...prevState,
- [siteName]: {
- primaryColor,
- secondaryColor,
- }
- }))
- } else {
- primaryColor = siteColors[siteName].primaryColor
- secondaryColor = siteColors[siteName].secondaryColor
+const EditHomepage = ({ match }) => {
+ const { retrieveSiteColors, generatePageStyleSheet } = useSiteColorsHook()
+
+ const { siteName } = match.params;
+ const [hasLoaded, setHasLoaded] = useState(false)
+ const [scrollRefs, setScrollRefs] = useState([])
+ const [hasErrors, setHasErrors] = useState(false)
+ const [frontMatter, setFrontMatter] = useState(
+ {
+ title: '',
+ subtitle: '',
+ description: '',
+ image: '',
+ notification: '',
+ sections: [],
+ }
+ )
+ const [originalFrontMatter, setOriginalFrontMatter] = useState(
+ {
+ title: '',
+ subtitle: '',
+ description: '',
+ image: '',
+ notification: '',
+ sections: [],
+ }
+ )
+ const [sha, setSha] = useState(null)
+ const [hasResources, setHasResources] = useState(false)
+ const [dropdownIsActive, setDropdownIsActive] = useState(false)
+ const [displaySections, setDisplaySections] = useState([])
+ const [displayHighlights, setDisplayHighlights] = useState([])
+ const [displayDropdownElems, setDisplayDropdownElems] = useState([])
+ const [errors, setErrors] = useState(
+ {
+ sections: [],
+ highlights: [],
+ dropdownElems: [],
+ }
+ )
+ const [itemPendingForDelete, setItemPendingForDelete] = useState(
+ {
+ id: '',
+ type: '',
+ }
+ )
+ const [savedHeroElems, setSavedHeroElems] = useState('')
+ const [savedHeroErrors, setSavedHeroErrors] = useState('')
+
+ useEffect(() => {
+ let _isMounted = true
+ const loadPageDetails = async () => {
+ // // Set page colors
+ try {
+ await retrieveSiteColors(siteName)
+ generatePageStyleSheet(siteName)
+ } catch (err) {
+ console.log(err);
}
- createPageStyleSheet(siteName, primaryColor, secondaryColor)
-
- } catch (err) {
- console.log(err);
- }
+ try {
+ const resp = await axios.get(`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/homepage`, {
+ withCredentials: true,
+ });
+ const { content, sha } = resp.data;
+ const { frontMatter } = frontMatterParser(content);
+ // Compute hasResources and set displaySections
+ let hasResources = false;
+ const displaySections = [];
+ let displayHighlights = [];
+ let displayDropdownElems = [];
+ const sectionsErrors = [];
+ let dropdownElemsErrors = [];
+ let highlightsErrors = [];
+ let scrollRefs = []
+ frontMatter.sections.forEach((section) => {
+ scrollRefs.push(createRef())
+ // If this is the hero section, hide all highlights/dropdownelems by default
+ if (section.hero) {
+ const { dropdown, key_highlights: keyHighlights } = section.hero;
+ const hero = { title: '', subtitle: '', background: '', button: '', url: '' }
+ if (dropdown) {
+ hero.dropdown = ''
+ // Go through section.hero.dropdown.options
+ displayDropdownElems = _.fill(Array(dropdown.options.length), false);
+ // Fill in dropdown elem errors array
+ dropdownElemsErrors = _.map(dropdown.options, () => DropdownElemConstructor(true));
+ }
+ if (keyHighlights) {
+ displayHighlights = _.fill(Array(keyHighlights.length), false);
+ // Fill in highlights errors array
+ highlightsErrors = _.map(keyHighlights, () => KeyHighlightConstructor(true));
+ }
+ // Fill in sectionErrors for hero
+ sectionsErrors.push({ hero })
+ }
- try {
- const resp = await axios.get(`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/homepage`, {
- withCredentials: true,
- });
- const { content, sha } = resp.data;
- const base64DecodedContent = Base64.decode(content);
- const { frontMatter } = frontMatterParser(base64DecodedContent);
- // Compute hasResources and set displaySections
- let hasResources = false;
- const displaySections = [];
- let displayHighlights = [];
- let displayDropdownElems = [];
- const sectionsErrors = [];
- let dropdownElemsErrors = [];
- let highlightsErrors = [];
- frontMatter.sections.forEach((section) => {
- // If this is the hero section, hide all highlights/dropdownelems by default
- if (section.hero) {
- const { dropdown, key_highlights: keyHighlights } = section.hero;
- if (dropdown) {
- // Go through section.hero.dropdown.options
- displayDropdownElems = _.fill(Array(dropdown.options.length), false);
- // Fill in dropdown elem errors array
- dropdownElemsErrors = _.map(dropdown.options, () => DropdownElemConstructor(true));
- // Fill in sectionErrors for hero with dropdown
- sectionsErrors.push({
- hero: {
- title: '', subtitle: '', background: '', button: '', url: '', dropdown: '',
- },
- });
+ // Check if there is already a resources section
+ if (section.resources) {
+ sectionsErrors.push(ResourcesSectionConstructor(true));
+ hasResources = true;
}
- if (keyHighlights) {
- displayHighlights = _.fill(Array(keyHighlights.length), false);
- // Fill in highlights errors array
- highlightsErrors = _.map(keyHighlights, () => KeyHighlightConstructor(true));
- // Fill in sectionErrors for hero with key highlights
- sectionsErrors.push({
- hero: {
- title: '', subtitle: '', background: '', button: '', url: '',
- },
- });
+
+ if (section.infobar) {
+ sectionsErrors.push(InfobarSectionConstructor(true));
}
- if (!dropdown && !keyHighlights) {
- sectionsErrors.push({
- hero: {
- title: '', subtitle: '', background: '', button: '', url: '',
- },
- });
+
+ if (section.infopic) {
+ sectionsErrors.push(InfopicSectionConstructor(true));
}
- }
- // Check if there is already a resources section
- if (section.resources) {
- sectionsErrors.push(ResourcesSectionConstructor(true));
- hasResources = true;
- }
+ // Minimize all sections by default
+ displaySections.push(false);
+ });
- if (section.infobar) {
- sectionsErrors.push(InfobarSectionConstructor(true));
+ // Initialize errors object
+ const errors = {
+ sections: sectionsErrors,
+ highlights: highlightsErrors,
+ dropdownElems: dropdownElemsErrors,
+ };
+
+ if (_isMounted) {
+ setFrontMatter(frontMatter)
+ setOriginalFrontMatter(_.cloneDeep(frontMatter))
+ setSha(sha)
+ setHasResources(hasResources)
+ setDisplaySections(displaySections)
+ setDisplayDropdownElems(displayDropdownElems)
+ setDisplayHighlights(displayHighlights)
+ setErrors(errors)
+ setHasLoaded(true)
+ setScrollRefs(scrollRefs)
}
+ } catch (err) {
+ // Set frontMatter to be same to prevent warning message when navigating away
+ if (_isMounted) setFrontMatter(originalFrontMatter)
+ errorToast(`There was a problem trying to load your homepage. ${DEFAULT_RETRY_MSG}`)
+ console.log(err);
+ }
+ }
- if (section.infopic) {
- sectionsErrors.push(InfopicSectionConstructor(true));
- }
+ loadPageDetails()
+ return () => {
+ _isMounted = false
+ }
+ }, [])
- // Minimize all sections by default
- displaySections.push(false);
- });
+ useEffect(() => {
+ if (scrollRefs.length > 0) {
+ scrollRefs[frontMatter.sections.length-1].current.scrollIntoView();
+ }
+ }, [frontMatter.sections.length])
- // Initialize errors object
- const errors = {
- sections: sectionsErrors,
- highlights: highlightsErrors,
- dropdownElems: dropdownElemsErrors,
- };
+ useEffect(() => {
+ const hasSectionErrors = _.some(errors.sections, (section) => {
+ // Section is an object, e.g. { hero: {} }
+ // _.keys(section) produces an array with length 1
+ // The 0th element of the array contains the sectionType
+ const sectionType = _.keys(section)[0];
+ return _.some(section[sectionType], (errorMessage) => errorMessage.length > 0) === true;
+ })
- if (this._isMounted) this.setState({
- frontMatter,
- originalFrontMatter: _.cloneDeep(frontMatter),
- sha,
- hasResources,
- displaySections,
- displayDropdownElems,
- displayHighlights,
- errors,
- });
- } catch (err) {
- // Set frontMatter to be same to prevent warning message when navigating away
- this.setState( {frontMatter: this.state.originalFrontMatter} )
- toast(
- ,
- {className: `${elementStyles.toastError} ${elementStyles.toastLong}`}
- );
- console.log(err);
- }
- }
+ const hasHighlightErrors = _.some(errors.highlights,
+ (highlight) => _.some(highlight,
+ (errorMessage) => errorMessage.length > 0) === true);
- componentWillUnmount() {
- this._isMounted = false;
- }
+ const hasDropdownElemErrors = _.some(errors.dropdownElems,
+ (dropdownElem) => _.some(dropdownElem,
+ (errorMessage) => errorMessage.length > 0) === true);
- componentDidUpdate(_, prevState) {
- if (prevState.frontMatter.sections.length !== 0 && this.state.frontMatter.sections.length !== prevState.frontMatter.sections.length) {
- // Occurs when a new section is created
- this.scrollRefs[this.state.frontMatter.sections.length-1].scrollIntoView();
- }
- }
- onFieldChange = async (event) => {
+ const hasErrors = hasSectionErrors || hasHighlightErrors || hasDropdownElemErrors;
+
+ setHasErrors(hasErrors)
+ }, [errors])
+
+ const onFieldChange = async (event) => {
try {
- const { state } = this;
- const { errors } = state;
const { id, value } = event.target;
const idArray = id.split('-');
const elemType = idArray[0];
@@ -289,18 +285,15 @@ export default class EditHomepage extends Component {
// The field that changed belongs to a site-wide config
const field = idArray[1]; // e.g. "title" or "subtitle"
- this.setState((currState) => ({
- ...currState,
- frontMatter: {
- ...currState.frontMatter,
- [field]: value,
- },
- }));
+ setFrontMatter({
+ ...frontMatter,
+ [field]: value,
+ });
break;
}
case 'section': {
// The field that changed belongs to a homepage section config
- const { sections } = state.frontMatter;
+ const { sections } = frontMatter;
// sectionIndex is the index of the section array in the frontMatter
const sectionIndex = parseInt(idArray[1], RADIX_PARSE_INT);
@@ -322,21 +315,21 @@ export default class EditHomepage extends Component {
// Set special error message if hero button has text but hero url is empty
// This needs to be done separately because it relies on the state of another field
if (
- field === 'url' && !value && this.state.frontMatter.sections[sectionIndex][sectionType].button
- && (this.state.frontMatter.sections[sectionIndex][sectionType].button || value)
+ field === 'url' && !value && frontMatter.sections[sectionIndex][sectionType].button
+ && (frontMatter.sections[sectionIndex][sectionType].button || value)
) {
const errorMessage = 'Please specify a URL for your button'
newSectionError = _.cloneDeep(errors.sections[sectionIndex])
newSectionError[sectionType][field] = errorMessage
} else if (
- field === 'button' && !this.state.frontMatter.sections[sectionIndex][sectionType].url
- && (value || this.state.frontMatter.sections[sectionIndex][sectionType].url) && sectionType !== 'resources'
+ field === 'button' && !frontMatter.sections[sectionIndex][sectionType].url
+ && (value || frontMatter.sections[sectionIndex][sectionType].url) && sectionType !== 'resources'
) {
const errorMessage = 'Please specify a URL for your button'
newSectionError = _.cloneDeep(errors.sections[sectionIndex])
newSectionError[sectionType]['url'] = errorMessage
} else {
- newSectionError = validateSections(errors.sections[sectionIndex], sectionType, field, value)
+ newSectionError = validateSections(_.cloneDeep(errors.sections[sectionIndex]), sectionType, field, value)
if (field === 'button' && !value) {
newSectionError[sectionType]['button'] = ''
@@ -352,21 +345,18 @@ export default class EditHomepage extends Component {
},
});
- this.setState((currState) => ({
- ...currState,
- frontMatter: {
- ...currState.frontMatter,
- sections: newSections,
- },
- errors: newErrors,
- }));
+ setFrontMatter({
+ ...frontMatter,
+ sections: newSections,
+ })
+ setErrors(newErrors)
- this.scrollRefs[sectionIndex].scrollIntoView();
+ scrollRefs[sectionIndex].current.scrollIntoView();
break;
}
case 'highlight': {
// The field that changed belongs to a hero highlight
- const { sections } = state.frontMatter;
+ const { sections } = frontMatter;
// highlightsIndex is the index of the key_highlights array
const highlightsIndex = parseInt(idArray[1], RADIX_PARSE_INT);
@@ -394,21 +384,18 @@ export default class EditHomepage extends Component {
},
});
- this.setState((currState) => ({
- ...currState,
- frontMatter: {
- ...currState.frontMatter,
- sections: newSections,
- },
- errors: newErrors,
- }));
+ setFrontMatter({
+ ...frontMatter,
+ sections: newSections,
+ })
+ setErrors(newErrors)
- this.scrollRefs[0].scrollIntoView();
+ scrollRefs[0].current.scrollIntoView();
break;
}
case 'dropdownelem': {
// The field that changed is a dropdown element (i.e. dropdownelem)
- const { sections } = state.frontMatter;
+ const { sections } = frontMatter;
// dropdownsIndex is the index of the dropdown.options array
const dropdownsIndex = parseInt(idArray[1], RADIX_PARSE_INT);
@@ -438,16 +425,13 @@ export default class EditHomepage extends Component {
},
});
- this.setState((currState) => ({
- ...currState,
- frontMatter: {
- ...currState.frontMatter,
- sections: newSections,
- },
- errors: newErrors,
- }));
+ setFrontMatter({
+ ...frontMatter,
+ sections: newSections
+ })
+ setErrors(newErrors)
- this.scrollRefs[0].scrollIntoView();
+ scrollRefs[0].current.scrollIntoView();
break;
}
default: {
@@ -461,26 +445,25 @@ export default class EditHomepage extends Component {
},
});
- this.setState((currState) => ({
- ...currState,
- frontMatter: {
- ...currState.frontMatter,
- sections: update(currState.frontMatter.sections, {
- 0: {
- hero: {
- dropdown: {
- title: {
- $set: value,
- },
- },
+ const newSections = update(frontMatter.sections, {
+ 0: {
+ hero: {
+ dropdown: {
+ title: {
+ $set: value,
},
},
- }),
+ },
},
- errors: newErrors,
- }));
+ })
+
+ setFrontMatter({
+ ...frontMatter,
+ sections: newSections
+ })
+ setErrors(newErrors)
- this.scrollRefs[0].scrollIntoView();
+ scrollRefs[0].current.scrollIntoView();
}
}
} catch (err) {
@@ -488,15 +471,12 @@ export default class EditHomepage extends Component {
}
}
- createHandler = async (event) => {
+ const createHandler = async (event) => {
try {
const { id, value } = event.target;
const idArray = id.split('-');
const elemType = idArray[0];
- const {
- frontMatter, errors, displaySections, displayDropdownElems, displayHighlights,
- } = this.state;
let newSections = [];
let newErrors = [];
@@ -505,7 +485,7 @@ export default class EditHomepage extends Component {
// The Isomer site can only have 1 resources section in the homepage
// Set hasResources to prevent the creation of more resources sections
if (value === 'resources') {
- this.setState({ hasResources: true });
+ setHasResources(true)
}
newSections = update(frontMatter.sections, {
@@ -516,15 +496,16 @@ export default class EditHomepage extends Component {
$push: [enumSection(value, true)],
},
});
+ const newScrollRefs = update(scrollRefs, {$push: [createRef()]})
const resetDisplaySections = _.fill(Array(displaySections.length), false)
const newDisplaySections = update(resetDisplaySections, {
$push: [true],
});
- this.setState({
- displaySections: newDisplaySections,
- });
+ setScrollRefs(newScrollRefs)
+ setDisplaySections(newDisplaySections)
+
break;
}
case 'dropdownelem': {
@@ -553,9 +534,8 @@ export default class EditHomepage extends Component {
$splice: [[dropdownsIndex, 0, true]],
});
- this.setState({
- displayDropdownElems: newDisplayDropdownElems,
- });
+ setDisplayDropdownElems(newDisplayDropdownElems)
+
break;
}
case 'highlight': {
@@ -584,11 +564,9 @@ export default class EditHomepage extends Component {
$splice: [[highlightIndex, 0, true]],
});
- this.setState({
- displayHighlights: newDisplayHighlights,
- });
- // If neither key highlights nor dropdown section exists, create new key highlights array
+ setDisplayHighlights(newDisplayHighlights)
} else {
+ // If neither key highlights nor dropdown section exists, create new key highlights array
newSections = _.cloneDeep(frontMatter.sections)
newSections[0].hero.key_highlights = [KeyHighlightConstructor(false)];
@@ -597,36 +575,28 @@ export default class EditHomepage extends Component {
const newDisplayHighlights = [true]
- this.setState({
- displayHighlights: newDisplayHighlights,
- });
+ setDisplayHighlights(newDisplayHighlights)
}
break;
}
default:
return;
}
- this.setState((currState) => ({
- ...currState,
- frontMatter: {
- ...currState.frontMatter,
- sections: newSections,
- },
- errors: newErrors,
- }));
+ setFrontMatter({
+ ...frontMatter,
+ sections: newSections
+ })
+ setErrors(newErrors)
} catch (err) {
console.log(err);
}
}
- deleteHandler = async (id) => {
+ const deleteHandler = async (id) => {
try {
const idArray = id.split('-');
const elemType = idArray[0];
- const {
- frontMatter, errors, displaySections, displayDropdownElems, displayHighlights,
- } = this.state;
let newSections = [];
let newErrors = {};
@@ -636,7 +606,7 @@ export default class EditHomepage extends Component {
// Set hasResources to false to allow users to create a resources section
if (frontMatter.sections[sectionIndex].resources) {
- this.setState({ hasResources: false });
+ setHasResources(false)
}
newSections = update(frontMatter.sections, {
@@ -649,13 +619,14 @@ export default class EditHomepage extends Component {
},
});
+ const newScrollRefs = update(scrollRefs, {$splice: [[sectionIndex, 1]]})
+
const newDisplaySections = update(displaySections, {
$splice: [[sectionIndex, 1]],
});
- this.setState({
- displaySections: newDisplaySections,
- });
+ setDisplaySections(newDisplaySections)
+ setScrollRefs(newScrollRefs)
break;
}
case 'dropdownelem': {
@@ -682,9 +653,7 @@ export default class EditHomepage extends Component {
$splice: [[dropdownsIndex, 1]],
});
- this.setState({
- displayDropdownElems: newDisplayDropdownElems,
- });
+ setDisplayDropdownElems(newDisplayDropdownElems)
break;
}
case 'highlight': {
@@ -709,44 +678,36 @@ export default class EditHomepage extends Component {
$splice: [[highlightIndex, 1]],
});
- this.setState({
- displayHighlights: newDisplayHighlights,
- });
+ setDisplayHighlights(newDisplayHighlights)
break;
}
default:
return;
}
- this.setState((currState) => ({
- ...currState,
- frontMatter: {
- ...currState.frontMatter,
- sections: newSections,
- },
- errors: newErrors,
- }));
+ setFrontMatter({
+ ...frontMatter,
+ sections: newSections
+ })
+ setErrors(newErrors)
} catch (err) {
console.log(err);
}
}
- handleHighlightDropdownToggle = (event) => {
- const {
- frontMatter, errors,
- } = this.state;
+ const handleHighlightDropdownToggle = (event) => {
let newSections = [];
let newErrors = {};
const { target: { value } } = event;
if (value === 'highlights') {
if (!frontMatter.sections[0].hero.dropdown) return
let highlightObj, highlightErrors, buttonObj, buttonErrors, urlObj, urlErrors
- if (this.state.savedHeroElems) {
- highlightObj = this.state.savedHeroElems.key_highlights || []
- highlightErrors = this.state.savedHeroErrors.highlights || []
- buttonObj = this.state.savedHeroElems.button || ''
- buttonErrors = this.state.savedHeroErrors.button || ''
- urlObj = this.state.savedHeroElems.url || ''
- urlErrors = this.state.savedHeroErrors.url || ''
+ if (savedHeroElems) {
+ highlightObj = savedHeroElems.key_highlights || []
+ highlightErrors = savedHeroErrors.highlights || []
+ buttonObj = savedHeroElems.button || ''
+ buttonErrors = savedHeroErrors.button || ''
+ urlObj = savedHeroElems.url || ''
+ urlErrors = savedHeroErrors.url || ''
} else {
highlightObj = [];
highlightErrors = [];
@@ -755,13 +716,11 @@ export default class EditHomepage extends Component {
urlObj = ''
urlErrors = ''
}
- this.setState({
- savedHeroElems: this.state.frontMatter.sections[0].hero,
- savedHeroErrors: {
- dropdown: this.state.errors.sections[0].hero.dropdown,
- dropdownElems: this.state.errors.dropdownElems
- },
- });
+ setSavedHeroElems(frontMatter.sections[0].hero)
+ setSavedHeroErrors({
+ dropdown: errors.sections[0].hero.dropdown,
+ dropdownElems: errors.dropdownElems
+ })
newSections = update(frontMatter.sections, {
0: {
@@ -808,24 +767,22 @@ export default class EditHomepage extends Component {
} else {
if (frontMatter.sections[0].hero.dropdown) return
let dropdownObj, dropdownErrors, dropdownElemErrors
- if (this.state.savedHeroElems) {
- dropdownObj = this.state.savedHeroElems.dropdown || DropdownConstructor();
- dropdownErrors = this.state.savedHeroErrors.dropdown || ''
- dropdownElemErrors = this.state.savedHeroErrors.dropdownElems || ''
+ if (savedHeroElems) {
+ dropdownObj = savedHeroElems.dropdown || DropdownConstructor();
+ dropdownErrors = savedHeroErrors.dropdown || ''
+ dropdownElemErrors = savedHeroErrors.dropdownElems || ''
} else {
dropdownObj = DropdownConstructor();
dropdownErrors = ''
dropdownElemErrors = []
}
- this.setState({
- savedHeroElems: this.state.frontMatter.sections[0].hero,
- savedHeroErrors: {
- highlights: this.state.errors.highlights,
- button: this.state.errors.sections[0].hero.button,
- url: this.state.errors.sections[0].hero.url,
- },
- });
+ setSavedHeroElems(frontMatter.sections[0].hero)
+ setSavedHeroErrors({
+ highlights: errors.highlights,
+ button: errors.sections[0].hero.button,
+ url: errors.sections[0].hero.url,
+ })
newSections = update(frontMatter.sections, {
0: {
@@ -870,34 +827,28 @@ export default class EditHomepage extends Component {
}
});
}
- this.setState((currState) => ({
- ...currState,
- frontMatter: {
- ...currState.frontMatter,
- sections: newSections,
- },
- errors: newErrors,
- }));
+ setFrontMatter({
+ ...frontMatter,
+ sections: newSections
+ })
+ setErrors(newErrors)
}
- toggleDropdown = async () => {
+ const toggleDropdown = async () => {
try {
- this.setState((currState) => ({
- dropdownIsActive: !currState.dropdownIsActive,
- }));
+ setDropdownIsActive((prevState) => !prevState)
} catch (err) {
console.log(err);
}
}
- displayHandler = async (event) => {
+ const displayHandler = async (event) => {
try {
const { id } = event.target;
const idArray = id.split('-');
const elemType = idArray[0];
switch (elemType) {
case 'section': {
- const { displaySections } = this.state;
const sectionId = idArray[1];
let resetDisplaySections = _.fill(Array(displaySections.length), false)
resetDisplaySections[sectionId] = !displaySections[sectionId]
@@ -905,14 +856,12 @@ export default class EditHomepage extends Component {
$set: resetDisplaySections,
});
- this.setState({
- displaySections: newDisplaySections,
- });
- this.scrollRefs[sectionId].scrollIntoView();
+ setDisplaySections(newDisplaySections)
+
+ scrollRefs[sectionId].current.scrollIntoView();
break;
}
case 'highlight': {
- const { displayHighlights } = this.state;
const highlightIndex = idArray[1];
let resetHighlightSections = _.fill(Array(displayHighlights.length), false)
resetHighlightSections[highlightIndex] = !displayHighlights[highlightIndex]
@@ -920,13 +869,10 @@ export default class EditHomepage extends Component {
$set: resetHighlightSections,
});
- this.setState({
- displayHighlights: newDisplayHighlights,
- });
+ setDisplayHighlights(newDisplayHighlights)
break;
}
case 'dropdownelem': {
- const { displayDropdownElems } = this.state;
const dropdownsIndex = idArray[1];
let resetDropdownSections = _.fill(Array(displayDropdownElems.length), false)
resetDropdownSections[dropdownsIndex] = !displayDropdownElems[dropdownsIndex]
@@ -934,9 +880,7 @@ export default class EditHomepage extends Component {
$set: resetDropdownSections,
});
- this.setState({
- displayDropdownElems: newDisplayDropdownElems,
- });
+ setDisplayDropdownElems(newDisplayDropdownElems)
break;
}
default:
@@ -947,14 +891,12 @@ export default class EditHomepage extends Component {
}
}
- savePage = async () => {
+ const savePage = async () => {
try {
- const { state } = this;
- const { match } = this.props;
const { siteName } = match.params;
- let filteredFrontMatter = _.cloneDeep(state.frontMatter)
+ let filteredFrontMatter = _.cloneDeep(frontMatter)
// Filter out components which have no input
- filteredFrontMatter.sections = state.frontMatter.sections.map((section) => {
+ filteredFrontMatter.sections = frontMatter.sections.map((section) => {
let newSection = {}
for (const sectionName in section) {
newSection[sectionName] = _.cloneDeep(_.omitBy(section[sectionName], _.isEmpty))
@@ -962,11 +904,10 @@ export default class EditHomepage extends Component {
return newSection
})
const content = concatFrontMatterMdBody(filteredFrontMatter, '');
- const base64EncodedContent = Base64.encode(content);
const params = {
- content: base64EncodedContent,
- sha: state.sha,
+ content,
+ sha: sha,
};
await axios.post(`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/homepage`, params, {
@@ -975,19 +916,13 @@ export default class EditHomepage extends Component {
window.location.reload();
} catch (err) {
- toast(
- ,
- {className: `${elementStyles.toastError} ${elementStyles.toastLong}`}
- );
+ errorToast(`There was a problem trying to save your homepage. ${DEFAULT_RETRY_MSG}`)
console.log(err);
}
}
- onDragEnd = (result) => {
+ const onDragEnd = (result) => {
const { source, destination, type } = result;
- const {
- frontMatter, errors, displaySections, displayDropdownElems, displayHighlights,
- } = this.state;
// If the user dropped the draggable to no known droppable
if (!destination) return;
@@ -1029,9 +964,7 @@ export default class EditHomepage extends Component {
],
});
- this.setState({
- displaySections: newDisplaySections,
- });
+ setDisplaySections(newDisplaySections)
break;
}
case 'dropdownelem': {
@@ -1069,9 +1002,7 @@ export default class EditHomepage extends Component {
],
});
- this.setState({
- displayDropdownElems: newDisplayDropdownElems,
- });
+ setDisplayDropdownElems(newDisplayDropdownElems)
break;
}
case 'highlight': {
@@ -1107,86 +1038,49 @@ export default class EditHomepage extends Component {
],
});
- this.setState({
- displayHighlights: newDisplayHighlights,
- });
+ setDisplayHighlights(newDisplayHighlights)
break;
}
default:
return;
}
- this.setState((currState) => ({
- ...currState,
- frontMatter: {
- ...currState.frontMatter,
- sections: newSections,
- },
- errors: newErrors,
- }));
+ setFrontMatter({
+ ...frontMatter,
+ sections: newSections
+ })
+ setErrors(newErrors)
}
- render() {
- const {
- frontMatter,
- originalFrontMatter,
- hasResources,
- dropdownIsActive,
- displaySections,
- displayHighlights,
- displayDropdownElems,
- errors,
- itemPendingForDelete,
- sha,
- } = this.state;
- const { match } = this.props;
- const { siteName } = match.params;
+ const isLeftInfoPic = (sectionIndex) => {
+ // If the previous section in the list was not an infopic section
+ // or if the previous section was a right infopic section, return true
+ if (!frontMatter.sections[sectionIndex - 1].infopic
+ || !isLeftInfoPic(sectionIndex - 1)) return true;
- const hasSectionErrors = _.some(errors.sections, (section) => {
- // Section is an object, e.g. { hero: {} }
- // _.keys(section) produces an array with length 1
- // The 0th element of the array contains the sectionType
- const sectionType = _.keys(section)[0];
- return _.some(section[sectionType], (errorMessage) => errorMessage.length > 0) === true;
- });
+ return false;
+ };
- const hasHighlightErrors = _.some(errors.highlights,
- (highlight) => _.some(highlight,
- (errorMessage) => errorMessage.length > 0) === true);
-
- const hasDropdownElemErrors = _.some(errors.dropdownElems,
- (dropdownElem) => _.some(dropdownElem,
- (errorMessage) => errorMessage.length > 0) === true);
-
- const hasErrors = hasSectionErrors || hasHighlightErrors || hasDropdownElemErrors;
-
- const isLeftInfoPic = (sectionIndex) => {
- // If the previous section in the list was not an infopic section
- // or if the previous section was a right infopic section, return true
- if (!frontMatter.sections[sectionIndex - 1].infopic
- || !isLeftInfoPic(sectionIndex - 1)) return true;
-
- return false;
- };
-
- return (
- <>
+ return (
+ <>
{
itemPendingForDelete.id
&& (
this.setState({ itemPendingForDelete: { id: null, type: '' } })}
- onDelete={() => { this.deleteHandler(itemPendingForDelete.id); this.setState({ itemPendingForDelete: { id: null, type: '' } }); }}
+ onCancel={() => setItemPendingForDelete({ id: null, type: '' })}
+ onDelete={() => { deleteHandler(itemPendingForDelete.id); setItemPendingForDelete({ id: null, type: '' })}}
type={itemPendingForDelete.type}
/>
)
}
+ {hasLoaded &&
@@ -1196,14 +1090,14 @@ export default class EditHomepage extends Component {
placeholder="Notification"
value={frontMatter.notification}
id="site-notification"
- onChange={this.onFieldChange} />
+ onChange={onFieldChange} />
Note: Leave text field empty if you don’t need this notification bar
{/* Homepage section configurations */}
-
+
{(droppableProvided) => (
this.setState({ itemPendingForDelete: { id: event.target.id, type } })}
+ onFieldChange={onFieldChange}
+ createHandler={createHandler}
+ deleteHandler={(event, type) => setItemPendingForDelete({ id: event.target.id, type })}
shouldDisplay={displaySections[sectionIndex]}
displayHighlights={displayHighlights}
displayDropdownElems={displayDropdownElems}
- displayHandler={this.displayHandler}
- onDragEnd={this.onDragEnd}
+ displayHandler={displayHandler}
+ onDragEnd={onDragEnd}
errors={errors}
siteName={siteName}
- handleHighlightDropdownToggle={this.handleHighlightDropdownToggle}
+ handleHighlightDropdownToggle={handleHighlightDropdownToggle}
/>
>
) : (
@@ -1260,10 +1154,10 @@ export default class EditHomepage extends Component {
subtitle={section.resources.subtitle}
button={section.resources.button}
sectionIndex={sectionIndex}
- deleteHandler={(event) => this.setState({ itemPendingForDelete: { id: event.target.id, type: 'Resources Section' } })}
- onFieldChange={this.onFieldChange}
+ deleteHandler={(event) => setItemPendingForDelete({ id: event.target.id, type: 'Resources Section' })}
+ onFieldChange={onFieldChange}
shouldDisplay={displaySections[sectionIndex]}
- displayHandler={this.displayHandler}
+ displayHandler={displayHandler}
errors={errors.sections[sectionIndex].resources}
/>
@@ -1293,10 +1187,10 @@ export default class EditHomepage extends Component {
button={section.infobar.button}
url={section.infobar.url}
sectionIndex={sectionIndex}
- deleteHandler={(event) => this.setState({ itemPendingForDelete: { id: event.target.id, type: 'Infobar Section' } })}
- onFieldChange={this.onFieldChange}
+ deleteHandler={(event) => setItemPendingForDelete({ id: event.target.id, type: 'Infobar Section' })}
+ onFieldChange={onFieldChange}
shouldDisplay={displaySections[sectionIndex]}
- displayHandler={this.displayHandler}
+ displayHandler={displayHandler}
errors={errors.sections[sectionIndex].infobar}
/>
@@ -1328,10 +1222,10 @@ export default class EditHomepage extends Component {
imageUrl={section.infopic.image}
imageAlt={section.infopic.alt}
sectionIndex={sectionIndex}
- deleteHandler={(event) => this.setState({ itemPendingForDelete: { id: event.target.id, type: 'Infopic Section' } })}
- onFieldChange={this.onFieldChange}
+ deleteHandler={(event) => setItemPendingForDelete({ id: event.target.id, type: 'Infopic Section' })}
+ onFieldChange={onFieldChange}
shouldDisplay={displaySections[sectionIndex]}
- displayHandler={this.displayHandler}
+ displayHandler={displayHandler}
errors={errors.sections[sectionIndex].infopic}
siteName={siteName}
/>
@@ -1356,7 +1250,7 @@ export default class EditHomepage extends Component {
{/* Section creator */}
@@ -1385,35 +1279,37 @@ export default class EditHomepage extends Component {
{/* Hero section */}
{section.hero
? (
-
{ this.scrollRefs[sectionIndex] = ref; }}>
+ <>
-
+ >
)
: null}
{/* Resources section */}
{section.resources
? (
-
{ this.scrollRefs[sectionIndex] = ref; }}>
+ <>
-
+ >
)
: null}
{/* Infobar section */}
{section.infobar
? (
-
{ this.scrollRefs[sectionIndex] = ref; }}>
+ <>
-
+ >
)
: null}
{/* Infopic section */}
{section.infopic
? (
-
{ this.scrollRefs[sectionIndex] = ref; }}>
+ <>
{ isLeftInfoPic(sectionIndex)
? (
)
: (
@@ -1454,9 +1352,10 @@ export default class EditHomepage extends Component {
button={section.infopic.button}
sectionIndex={sectionIndex}
siteName={siteName}
+ ref={scrollRefs[sectionIndex]}
/>
)}
-
+ >
)
: null}
>
@@ -1468,15 +1367,16 @@ export default class EditHomepage extends Component {
disabled={hasErrors}
disabledStyle={elementStyles.disabled}
className={(hasErrors || !sha) ? elementStyles.disabled : elementStyles.blue}
- callback={this.savePage}
+ callback={savePage}
/>
-
+
}
>
- );
- }
+ )
}
+export default EditHomepage
+
EditHomepage.propTypes = {
match: PropTypes.shape({
params: PropTypes.shape({
diff --git a/src/layouts/EditNav.jsx b/src/layouts/EditNav.jsx
deleted file mode 100644
index 7fef7b984..000000000
--- a/src/layouts/EditNav.jsx
+++ /dev/null
@@ -1,222 +0,0 @@
-import React, { Component } from 'react';
-import Tree, { mutateTree, moveItemOnTree } from '@atlaskit/tree';
-import axios from 'axios';
-import PropTypes from 'prop-types';
-import TreeBuilder from '../utils/tree-builder';
-import {
- dataIterator, ListItem, draggableWrapper, flattenTree, readTree,
-} from '../utils/tree-utils';
-import Header from '../components/Header';
-import styles from '../styles/isomer-cms/pages/MenuEditor.module.scss';
-import NavPreview from '../components/NavigationPreview';
-
-const rootNode = new TreeBuilder('root', 'root', '');
-
-export default class EditNav extends Component {
- constructor(props) {
- super(props);
- this.state = {
- tree: null,
- };
- }
-
- async componentDidMount() {
- const { match } = this.props;
- const { siteName } = match.params;
- try {
- const resp = await axios.get(`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/tree`, {
- withCredentials: true,
- });
- const { directory, unlinked } = resp.data;
-
- // add a root node for the navigation bar
- const tree = rootNode.withSubTree(
- directory.reduce(
- dataIterator,
- new TreeBuilder('Main Menu', 'section'),
- ),
- // add a root node for the unlinked pages
- ).withSubTree(
- unlinked.reduce(
- dataIterator,
- new TreeBuilder('Unlinked', 'section'),
- ),
- );
-
- const flattenedTree = flattenTree(tree);
- const navItems = readTree(flattenedTree);
-
- this.setState({ tree, navItems });
- } catch (err) {
- console.log(err);
- }
- }
-
- onExpand = (itemId) => {
- const { tree } = this.state;
- this.setState({
- tree: mutateTree(tree, itemId, { isExpanded: true }),
- });
- };
-
- onCollapse = (itemId) => {
- const { tree } = this.state;
-
- this.setState({
- tree: mutateTree(tree, itemId, { isExpanded: false }),
- });
- };
-
- getItemFromTreePosition = ({ tree, parentId, index }) => {
- const parent = this.getParentFromTreePosition({ tree, parentId });
-
- const childId = parent.children[index];
-
- return tree.items[childId];
- }
-
- getParentFromTreePosition = ({ tree, parentId }) => tree.items[parentId]
-
- onDragEnd = (
- source,
- destination,
- ) => {
- const { tree } = this.state;
-
- /**
- * `WIP`
- * In our drag'n'drop rules we need to specify the following:
- * 1) You can't drop items outside of the tree
- * 2) pages can be dropped anywhere but can't be merged into one another
- * 3) `collection`, `thirdnav` and `resource-room` can only be reordered at its current depth
- */
- // Rule 1)
- if (!destination) {
- return;
- }
-
- const sourceItemType = this.getItemFromTreePosition({
- tree, parentId: source.parentId, index: source.index,
- }).data.type;
-
- const sourceParentType = this.getParentFromTreePosition({
- tree, parentId: source.parentId,
- }).data.type;
-
- const destinationParent = this.getParentFromTreePosition({
- tree, parentId: destination.parentId,
- });
-
- const destinationParentType = destinationParent.data.type;
-
- switch (sourceItemType) {
- case 'page':
- case 'collection-page':
- case 'thirdnav-page':
- // Rule 2)
- if (!('index' in destination) && ['page', 'collection-page', 'thirdnav-page'].includes(destinationParentType)) return;
- break;
- case 'collection':
- case 'thirdnav':
- case 'resource room':
- // Rule 3)
- if (sourceParentType !== destinationParentType) return;
- break;
- default:
- }
-
- const newTree = moveItemOnTree(tree, source, destination);
- const flattenedNewTree = flattenTree(newTree);
- const newNavItems = readTree(flattenedNewTree);
- this.setState({
- tree: newTree,
- navItems: newNavItems,
- });
- };
-
- renderItem = ({
- item,
- onExpand,
- onCollapse,
- provided,
- snapshot,
- }) => {
- // Nodes of type `section` and the `Unlinked Pages` folder can't be dragged
- if (item.data.type !== 'section' && item.data.title !== 'Unlinked Pages') {
- return draggableWrapper(ListItem, item, onExpand, onCollapse, provided, snapshot);
- }
- return (
- // eslint-disable-next-line react/jsx-props-no-spreading
- <>
-