Skip to content

Commit

Permalink
NEW Inline save all rendered element forms on parent form submit
Browse files Browse the repository at this point in the history
  • Loading branch information
emteknetnz committed Jun 24, 2024
1 parent 9ee52c5 commit 006d14c
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 71 deletions.
2 changes: 1 addition & 1 deletion client/dist/js/bundle.js

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion client/src/components/ElementEditor/Element.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* global window */

import React, { useState, useEffect, createContext } from 'react';
import React, { useState, useEffect, useContext, createContext } from 'react';
import { useMutation } from '@apollo/client';
import PropTypes from 'prop-types';
import { elementType } from 'types/elementType';
Expand All @@ -21,6 +21,7 @@ import { getEmptyImage } from 'react-dnd-html5-backend';
import { elementDragSource, isOverTop } from 'lib/dragHelpers';
import * as toastsActions from 'state/toasts/ToastsActions';
import { addFormChanged, removeFormChanged } from 'state/unsavedForms/UnsavedFormsActions';
import { ElementEditorContext } from 'components/ElementEditor/ElementEditor';

export const ElementContext = createContext(null);

Expand All @@ -41,6 +42,13 @@ const Element = (props) => {
const [formHasRendered, setFormHasRendered] = useState(false);
const [doDispatchAddFormChanged, setDoDispatchAddFormChanged] = useState(false);
const [publishBlock] = useMutation(publishBlockMutation);
const { saveAllElements, parentResolve } = useContext(ElementEditorContext);

useEffect(() => {
if (saveAllElements && !doSaveElement) {
setDoSaveElement(true);
}
}, [saveAllElements, doSaveElement]);

useEffect(() => {
if (props.connectDragPreview) {
Expand Down Expand Up @@ -324,6 +332,7 @@ const Element = (props) => {
if (doPublishElementAfterSave) {
setDoPublishElementAfterSave(false);
}
parentResolve(false);
return;
}
// Form is valid
Expand All @@ -336,6 +345,7 @@ const Element = (props) => {
showSavedElementToast(title);
}
refetchElementalArea();
parentResolve(true);
};

const {
Expand Down
76 changes: 48 additions & 28 deletions client/src/components/ElementEditor/ElementEditor.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* global window */
import React, { PureComponent } from 'react';
import React, { PureComponent, createContext } from 'react';
import PropTypes from 'prop-types';
import { inject } from 'lib/Injector';
import { compose } from 'redux';
Expand All @@ -12,6 +12,8 @@ import ElementDragPreview from 'components/ElementEditor/ElementDragPreview';
import withDragDropContext from 'lib/withDragDropContext';
import { createSelector } from 'reselect';

export const ElementEditorContext = createContext(null);

/**
* The ElementEditor is used in the CMS to manage a list or nested lists of
* elements for a page or other DataObject.
Expand Down Expand Up @@ -80,6 +82,10 @@ class ElementEditor extends PureComponent {
isDraggingOver,
connectDropTarget,
allowedElements,
//
saveAllElements,
parentResolve,
//
} = this.props;
const { dragTargetElementId, dragSpot } = this.state;

Expand All @@ -88,33 +94,38 @@ class ElementEditor extends PureComponent {
elementTypes.find(type => type.class === className)
);

return connectDropTarget(
<div className="element-editor">
<ToolbarComponent
elementTypes={allowedElementTypes}
areaId={areaId}
onDragOver={this.handleDragOver}
/>
<ListComponent
allowedElementTypes={allowedElementTypes}
elementTypes={elementTypes}
areaId={areaId}
onDragOver={this.handleDragOver}
onDragStart={this.handleDragStart}
onDragEnd={this.handleDragEnd}
dragSpot={dragSpot}
isDraggingOver={isDraggingOver}
dragTargetElementId={dragTargetElementId}
/>
<ElementDragPreview elementTypes={elementTypes} />
<input
name={fieldName}
type="hidden"
value={JSON.stringify(formState) || ''}
className="no-change-track"
/>
</div>
);
// TODO
// warning The object passed as the value prop to the Context provider (at line 97) changes
// every render. To fix this consider wrapping it in a useMemo hook react/jsx-no-constructed-context-values
return <ElementEditorContext.Provider value={{ saveAllElements, parentResolve }}>
{ connectDropTarget(
<div className="element-editor">
<ToolbarComponent
elementTypes={allowedElementTypes}
areaId={areaId}
onDragOver={this.handleDragOver}
/>
<ListComponent
allowedElementTypes={allowedElementTypes}
elementTypes={elementTypes}
areaId={areaId}
onDragOver={this.handleDragOver}
onDragStart={this.handleDragStart}
onDragEnd={this.handleDragEnd}
dragSpot={dragSpot}
isDraggingOver={isDraggingOver}
dragTargetElementId={dragTargetElementId}
/>
<ElementDragPreview elementTypes={elementTypes} />
<input
name={fieldName}
type="hidden"
value={JSON.stringify(formState) || ''}
className="no-change-track"
/>
</div>
) }
</ElementEditorContext.Provider>;
}
}

Expand All @@ -126,6 +137,15 @@ ElementEditor.propTypes = {
actions: PropTypes.shape({
handleSortBlock: PropTypes.func,
}),
//
saveAllElements: PropTypes.bool.isRequired,
parentResolve: PropTypes.func.isRequired,
//
};

ElementEditor.defaultProps = {
saveAllElements: false,
parentResolve: () => {},
};

const defaultElementFormState = {};
Expand Down
109 changes: 72 additions & 37 deletions client/src/components/ElementEditor/tests/Element-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React from 'react';
import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';
import { render } from '@testing-library/react';
import { Component as Element } from '../Element';
import { ElementEditorContext } from '../ElementEditor';

function makeProps(obj = {}) {
return {
Expand Down Expand Up @@ -40,36 +41,58 @@ function makeProps(obj = {}) {
};
}

function makeContextProps() {
return {
value: {
saveAllElements: false,
parentResolve: () => {},
}
};
}

test('Element should render the HeaderComponent and the ContentComponent', () => {
const client = new ApolloClient({ cache: new InMemoryCache() });
const { container } = render(<ApolloProvider client={client}><Element {...makeProps()}/></ApolloProvider>);
const { container } = render(
<ApolloProvider client={client}>
<ElementEditorContext.Provider {...makeContextProps()}>
<Element {...makeProps()} />
</ElementEditorContext.Provider>
</ApolloProvider>);
expect(container.querySelectorAll('.element-editor__element .test-header')).toHaveLength(1);
expect(container.querySelectorAll('.element-editor__element .test-content')).toHaveLength(1);
});

test('Element should not render at all if no ID is given', () => {
const client = new ApolloClient({ cache: new InMemoryCache() });
const { container } = render(
<ApolloProvider client={client}><Element {...makeProps({
element: {
...makeProps().element,
id: ''
}
})}
/></ApolloProvider>
<ApolloProvider client={client}>
<ElementEditorContext.Provider {...makeContextProps()}>
<Element {...makeProps({
element: {
...makeProps().element,
id: ''
}
})}
/>
</ElementEditorContext.Provider>
</ApolloProvider>
);
expect(container.querySelectorAll('.element-editor__element')).toHaveLength(0);
});

test('Element should render even if the element is broken', () => {
const client = new ApolloClient({ cache: new InMemoryCache() });
const { container } = render(
<ApolloProvider client={client}><Element {...makeProps({
type: {
broken: true
}
})}
/></ApolloProvider>
<ApolloProvider client={client}>
<ElementEditorContext.Provider {...makeContextProps()}>
<Element {...makeProps({
type: {
broken: true
}
})}
/>
</ElementEditorContext.Provider>
</ApolloProvider>
);
expect(container.querySelectorAll('.element-editor__element .test-header')).toHaveLength(1);
expect(container.querySelectorAll('.element-editor__element .test-content')).toHaveLength(1);
Expand All @@ -78,43 +101,55 @@ test('Element should render even if the element is broken', () => {
test('Element getVersionedStateClassName() should identify draft elements', () => {
const client = new ApolloClient({ cache: new InMemoryCache() });
const { container } = render(
<ApolloProvider client={client}><Element {...makeProps({
element: {
...makeProps().element,
isPublished: false
}
})}
/></ApolloProvider>
<ApolloProvider client={client}>
<ElementEditorContext.Provider {...makeContextProps()}>
<Element {...makeProps({
element: {
...makeProps().element,
isPublished: false
}
})}
/>
</ElementEditorContext.Provider>
</ApolloProvider>
);
expect(container.querySelector('.element-editor__element').classList.contains('element-editor__element--draft')).toBe(true);
});

test('Element getVersionedStateClassName() should identify modified elements', () => {
const client = new ApolloClient({ cache: new InMemoryCache() });
const { container } = render(
<ApolloProvider client={client}><Element {...makeProps({
element: {
...makeProps().element,
isPublished: true,
isLiveVersion: false
}
})}
/></ApolloProvider>
<ApolloProvider client={client}>
<ElementEditorContext.Provider {...makeContextProps()}>
<Element {...makeProps({
element: {
...makeProps().element,
isPublished: true,
isLiveVersion: false
}
})}
/>
</ElementEditorContext.Provider>
</ApolloProvider>
);
expect(container.querySelectorAll('.element-editor__element--modified')).toHaveLength(1);
});

test('Element getVersionedStateClassName() should identify published elements', () => {
const client = new ApolloClient({ cache: new InMemoryCache() });
const { container } = render(
<ApolloProvider client={client}><Element {...makeProps({
element: {
...makeProps().element,
isPublished: true,
isLiveVersion: true
}
})}
/></ApolloProvider>
<ApolloProvider client={client}>
<ElementEditorContext.Provider {...makeContextProps()}>
<Element {...makeProps({
element: {
...makeProps().element,
isPublished: true,
isLiveVersion: true
}
})}
/>
</ElementEditorContext.Provider>
</ApolloProvider>
);
expect(container.querySelectorAll('.element-editor__element--published')).toHaveLength(1);
});
19 changes: 18 additions & 1 deletion client/src/legacy/ElementEditor/entwine.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jQuery.entwine('ss', ($) => {
$('.js-injector-boot .element-editor__container').entwine({
ReactRoot: null,

onmatch() {
render(extraProps = {}) {
const context = {};
const ElementEditorComponent = loadComponent('ElementEditor', context);
const schemaData = this.data('schema');
Expand All @@ -54,6 +54,7 @@ jQuery.entwine('ss', ($) => {
areaId: schemaData['elemental-area-id'],
allowedElements: schemaData['allowed-elements'],
elementTypes,
...extraProps
};

let root = this.getReactRoot();
Expand All @@ -64,6 +65,10 @@ jQuery.entwine('ss', ($) => {
root.render(<ElementEditorComponent {...props} />);
},

onmatch() {
this.render();
},

onunmatch() {
// Reset the store if the user navigates to a different part of the CMS
// or after submission if there are no validation errors
Expand All @@ -78,6 +83,18 @@ jQuery.entwine('ss', ($) => {
},

'from .cms-edit-form': {
onbeforesubmitform(event, data) {
let parentResolve;
const entwinePromise = new Promise((resolve) => {
parentResolve = resolve;
});
this.render({
saveAllElements: true,
parentResolve,
});
data.promises.push(entwinePromise);
},

onaftersubmitform(event, data) {
const validationResultPjax = JSON.parse(data.xhr.responseText).ValidationResult;
const validationResult = JSON.parse(validationResultPjax.replace(/<\/?script[^>]*?>/g, ''));
Expand Down
Loading

0 comments on commit 006d14c

Please sign in to comment.