Skip to content

Commit

Permalink
ENH Refactor Element and graphql hoc
Browse files Browse the repository at this point in the history
  • Loading branch information
emteknetnz committed Apr 23, 2024
1 parent e5cc486 commit baeebb4
Show file tree
Hide file tree
Showing 9 changed files with 85 additions and 161 deletions.
4 changes: 2 additions & 2 deletions client/dist/js/bundle.js

Large diffs are not rendered by default.

22 changes: 3 additions & 19 deletions client/src/components/ElementActions/PublishAction.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,21 @@
/* global window */
import React, { useContext, useEffect } from 'react';
import React, { useContext } from 'react';
import { compose } from 'redux';
import AbstractAction from 'components/ElementActions/AbstractAction';
import publishBlockMutation from 'state/editor/publishBlockMutation';
import i18n from 'i18n';
import { ElementContext } from 'components/ElementEditor/Element';
import { ElementContext } from 'components/ElementEditor/ElementContext';

/**
* Adds the elemental menu action to publish a draft/modified block
*/
const PublishAction = (MenuComponent) => (props) => {
const {
doPublishElement,
formDirty,
formHasRendered,
onAfterPublish,
onPublishButtonClick,
} = useContext(ElementContext);

const { element, actions } = props;

const publishElement = () => {
// handlePublishBlock is a graphql mutation defined in publishBlockMutation.js
actions.handlePublishBlock(element.id)
.then(() => onAfterPublish(false))
.catch(() => onAfterPublish(true));
};
const { element } = props;

const handleClick = (event) => {
event.stopPropagation();
Expand All @@ -46,12 +36,6 @@ const PublishAction = (MenuComponent) => (props) => {
toggle: props.toggle,
};

useEffect(() => {
if (formHasRendered && doPublishElement) {
publishElement();
}
}, [formHasRendered, doPublishElement]);

if (props.type.broken) {
// Don't allow this action for a broken element.
return (
Expand Down
17 changes: 1 addition & 16 deletions client/src/components/ElementActions/SaveAction.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import React, { useContext, useEffect } from 'react';
import AbstractAction from 'components/ElementActions/AbstractAction';
import i18n from 'i18n';
import { ElementContext } from 'components/ElementEditor/Element';
import { ElementContext } from 'components/ElementEditor/ElementContext';

const SaveAction = (MenuComponent) => (props) => {
const {
doSaveElement,
onSaveButtonClick,
onAfterSave,
submitForm,
formHasRendered,
formDirty,
} = useContext(ElementContext);

Expand All @@ -18,24 +14,13 @@ const SaveAction = (MenuComponent) => (props) => {
onSaveButtonClick();
};

const saveElement = () => {
submitForm();
onAfterSave();
};

const newProps = {
title: i18n._t('ElementSaveAction.SAVE', 'Save'),
className: 'element-editor__actions-save',
onClick: handleClick,
toggle: props.toggle,
};

useEffect(() => {
if (formHasRendered && doSaveElement) {
saveElement();
}
}, [formHasRendered, doSaveElement]);

if (!props.expandable || props.type.broken) {
// Some elemental blocks can not be edited inline (e.g. User form blocks)
// We don't want to add a "Save" action for those blocks.
Expand Down
71 changes: 1 addition & 70 deletions client/src/components/ElementActions/tests/PublishAction-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import { Component as PublishAction } from '../PublishAction';
import { ElementContext } from '../../ElementEditor/Element';
import { ElementContext } from '../../ElementEditor/ElementContext';

window.jQuery = {
noticeAdd: () => null
Expand Down Expand Up @@ -100,72 +100,3 @@ test('Clicking button calls onPublishButtonClick', () => {
fireEvent.click(container.querySelector('button.element-editor__actions-publish'));
expect(onPublishButtonClick).toHaveBeenCalled();
});

test('Do trigger graphql mutation if doPublishElement is true and formHasRendered is true', () => {
const handlePublishBlock = jest.fn(() => Promise.resolve());
render(
<ElementContext.Provider value={makeProviderValue({
doPublishElement: true,
formHasRendered: true,
})}
>
<ActionComponent {...makeProps({ actions: { handlePublishBlock } })}/>
</ElementContext.Provider>
);
expect(handlePublishBlock).toHaveBeenCalledWith(123);
});

test('Do not trigger graphql mutation if doPublishElement is true and formHasRendered is false', () => {
// handlePublishBlock is a graphql mutation defined in publishBlockMutation.js
const handlePublishBlock = jest.fn(() => Promise.resolve());
render(
<ElementContext.Provider value={makeProviderValue({
doPublishElement: true,
formHasRendered: false,
})}
>
<ActionComponent {...makeProps({ actions: { handlePublishBlock } })}/>
</ElementContext.Provider>
);
expect(handlePublishBlock).not.toHaveBeenCalled();
});

test('Do not trigger graphql mutation if doPublishElement is false and formHasRendered is true', () => {
const handlePublishBlock = jest.fn(() => Promise.resolve());
render(
<ElementContext.Provider value={makeProviderValue({
doPublishElement: false,
formHasRendered: true,
})}
>
<ActionComponent {...makeProps({ actions: { handlePublishBlock } })}/>
</ElementContext.Provider>
);
expect(handlePublishBlock).not.toHaveBeenCalled();
});

test('onAfterPublish is called after graphql mutation', async () => {
let value = 1;
const handlePublishBlock = jest.fn(() => {
value = 2;
return Promise.resolve();
});
const onAfterPublish = jest.fn(() => {
value = 3;
});
render(
<ElementContext.Provider value={makeProviderValue({
doPublishElement: true,
formHasRendered: true,
onAfterPublish,
})}
>
<ActionComponent {...makeProps({ actions: { handlePublishBlock } })}/>
</ElementContext.Provider>
);
// This is required to ensure the resolved promised returned by handlePublishBlock is handled
await new Promise(resolve => setTimeout(resolve, 0));
expect(handlePublishBlock).toHaveBeenCalled();
expect(onAfterPublish).toHaveBeenCalled();
expect(value).toBe(3);
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import { Component as SaveAction } from '../SaveAction';
import { ElementContext } from '../../ElementEditor/Element';
import { ElementContext } from '../../ElementEditor/ElementContext';

function makeProps(obj = {}) {
return {
Expand Down
16 changes: 2 additions & 14 deletions client/src/components/ElementEditor/Content.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { inject } from 'lib/Injector';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { loadElementFormStateName } from 'state/editor/loadElementFormStateName';
import { isDirty } from 'redux-form';
import getFormState from 'lib/getFormState';

class Content extends PureComponent {
render() {
Expand Down Expand Up @@ -87,18 +83,11 @@ Content.propTypes = {
onFormInit: PropTypes.func,
ensureFormRendered: PropTypes.bool,
formHasRendered: PropTypes.bool,
formDirty: PropTypes.object,
};

Content.defaultProps = {};

function mapStateToProps(state, ownProps) {
const formName = loadElementFormStateName(ownProps.id);

return {
formDirty: isDirty(`element.${formName}`, getFormState)(state),
};
}

export { Content as Component };

export default compose(
Expand All @@ -108,6 +97,5 @@ export default compose(
SummaryComponent, InlineEditFormComponent,
}),
() => 'ElementEditor.ElementList.Element'
),
connect(mapStateToProps)
)
)(Content);
82 changes: 54 additions & 28 deletions client/src/components/ElementEditor/Element.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* global window */

import React, { createContext, useState, useEffect } from 'react';
import React, { useState, useEffect } from 'react';
import { useMutation } from '@apollo/client';
import PropTypes from 'prop-types';
import { elementType } from 'types/elementType';
import { elementTypeType } from 'types/elementTypeType';
Expand All @@ -12,14 +13,15 @@ import { connect } from 'react-redux';
import { submit } from 'redux-form';
import { loadElementFormStateName } from 'state/editor/loadElementFormStateName';
import { loadElementSchemaValue } from 'state/editor/loadElementSchemaValue';
import { mutation as publishBlockMutation } from 'state/editor/publishBlockMutation';
import { query as readBlocksForAreaQuery } from 'state/editor/readBlocksForAreaQuery';
import * as TabsActions from 'state/tabs/TabsActions';
import { DragSource, DropTarget } from 'react-dnd';
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';

export const ElementContext = createContext(null);
import { ElementContext } from 'components/ElementEditor/ElementContext';

/**
* The Element component used in the context of an ElementEditor shows the summary
Expand All @@ -37,6 +39,7 @@ const Element = (props) => {
const [ensureFormRendered, setEnsureFormRendered] = useState(false);
const [formHasRendered, setFormHasRendered] = useState(false);
const [doDispatchAddFormChanged, setDoDispatchAddFormChanged] = useState(false);
const [publishBlock] = useMutation(publishBlockMutation);

useEffect(() => {
if (props.connectDragPreview) {
Expand Down Expand Up @@ -67,11 +70,14 @@ const Element = (props) => {
setDoPublishElement(true);
}
}
}, [justClickedPublishButton, formHasRendered]);

useEffect(() => {
if (doDispatchAddFormChanged) {
setDoDispatchAddFormChanged(false);
props.dispatchAddFormChanged();
}
}, [justClickedPublishButton, formHasRendered]);
}, [doDispatchAddFormChanged]);

const getNoTitle = () => i18n.inject(
i18n._t('ElementHeader.NOTITLE', 'Untitled {type} block'),
Expand Down Expand Up @@ -104,6 +110,50 @@ const Element = (props) => {
}
};

// This will trigger a graphql request that will cause this
// element to re-render including any updated title and versioned badge
const refetchElementalArea = () => window.ss.apolloClient.queryManager.refetchQueries({
include: [{
query: readBlocksForAreaQuery,
variables: { id: props.areaId }
}]
});

const handleAfterPublish = (wasError) => {
showPublishedElementToast(wasError);
setDoPublishElement(false);
setDoPublishElementAfterSave(false);
// Ensure that formDirty becomes falsey after publishing
// We need to call at a later render rather than straight away or redux-form may override this
// and set the form state to dirty under certain conditions
// setTimeout is a hackish way to do this, though I'm not sure how else we can do this
// The core issue is that redux-form will detect changes when a form is hydrated for the first
// time under certain conditions, specifically during a behat test when trying to publish a closed
// block when presumably the apollo cache is empty (or something like that). This happens late and
// there are no hooks/callbacks available after this happens the input onchange handlers are fired
Promise.all(refetchElementalArea())
.then(() => {
setTimeout(() => props.dispatchRemoveFormChanged(), 250);
});
};

// Save action
useEffect(() => {
if (formHasRendered && doSaveElement) {
props.submitForm();
setDoSaveElement(false);
}
}, [formHasRendered, doSaveElement]);

// Publish action
useEffect(() => {
if (formHasRendered && doPublishElement) {
publishBlock({ variables: { blockId: props.element.id } })
.then(() => handleAfterPublish(false))
.catch(() => handleAfterPublish(true));
}
}, [formHasRendered, doPublishElement]);

/**
* Returns the applicable versioned state class names for the element
*
Expand Down Expand Up @@ -188,12 +238,6 @@ const Element = (props) => {
}
};

const refetchElementalArea = () => {
// This will trigger a graphql readOneElementalArea request that will cause this
// element to re-render including any updated title and versioned badge
window.ss.apolloClient.queryManager.reFetchObservableQueries();
};

/**
* Update the active tab on tab actions menu button click event. Is passed down to InlineEditForm.
*
Expand Down Expand Up @@ -259,24 +303,6 @@ const Element = (props) => {
setDoSaveElement(false);
};

const handleAfterPublish = (wasError) => {
showPublishedElementToast(wasError);
setDoPublishElement(false);
setDoPublishElementAfterSave(false);
// Ensure that formDirty becomes falsey after publishing
// We need to call at a later render rather than straight away or redux-form may override this
// and set the form state to dirty under certain conditions
// setTimeout is a hackish way to do this, though I'm not sure how else we can do this
// The core issue is that redux-form will detect changes when a form is hydrated for the first
// time under certain conditions, specifically during a behat test when trying to publish a closed
// block when presumably the apollo cache is empty (or something like that). This happens late and
// there are no hooks/callbacks available after this happens the input onchange handlers are fired
setTimeout(() => {
props.dispatchRemoveFormChanged();
}, 500);
refetchElementalArea();
};

const handleFormInit = (activeTab) => {
updateFormTab(activeTab);
setFormHasRendered(true);
Expand Down
3 changes: 3 additions & 0 deletions client/src/components/ElementEditor/ElementContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createContext } from 'react';

export const ElementContext = createContext(null);
Loading

0 comments on commit baeebb4

Please sign in to comment.