Skip to content

Commit 939dadd

Browse files
committed
FIX Make change tracking work again for inline editable blocks
1 parent 4c38f21 commit 939dadd

7 files changed

Lines changed: 166 additions & 72 deletions

File tree

client/dist/js/bundle.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/src/components/ElementEditor/Element.js

Lines changed: 11 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { inject } from 'lib/Injector';
1010
import i18n from 'i18n';
1111
import classNames from 'classnames';
1212
import { connect } from 'react-redux';
13-
import { submit } from 'redux-form';
13+
import { submit, isDirty } from 'redux-form';
1414
import { loadElementFormStateName } from 'state/editor/loadElementFormStateName';
1515
import { loadElementSchemaValue } from 'state/editor/loadElementSchemaValue';
1616
import { publishBlockMutation } from 'state/editor/publishBlockMutation';
@@ -20,7 +20,7 @@ import { DragSource, DropTarget } from 'react-dnd';
2020
import { getEmptyImage } from 'react-dnd-html5-backend';
2121
import { elementDragSource, isOverTop } from 'lib/dragHelpers';
2222
import * as toastsActions from 'state/toasts/ToastsActions';
23-
import { addFormChanged, removeFormChanged } from 'state/unsavedForms/UnsavedFormsActions';
23+
import getFormState from 'lib/getFormState';
2424

2525
export const ElementContext = createContext(null);
2626

@@ -39,32 +39,19 @@ const Element = (props) => {
3939
const [doPublishElementAfterSave, setDoPublishElementAfterSave] = useState(false);
4040
const [ensureFormRendered, setEnsureFormRendered] = useState(false);
4141
const [formHasRendered, setFormHasRendered] = useState(false);
42-
const [doDispatchAddFormChanged, setDoDispatchAddFormChanged] = useState(false);
43-
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
4442
const [publishBlock] = useMutation(publishBlockMutation);
4543

4644
const formRenderedIfNeeded = formHasRendered || !props.type.inlineEditable;
4745

4846
useEffect(() => {
49-
// Note that formDirty from redux can be set to undefined after failed validation
50-
// which is confusing as the block still has unsaved changes, hence why we create
51-
// this state variable to track this instead
52-
// props.formDirty is either undefined (when pristine) or an object (when dirty)
53-
const formDirty = typeof props.formDirty !== 'undefined';
54-
if (formDirty && !hasUnsavedChanges) {
55-
setHasUnsavedChanges(true);
56-
}
47+
props.onChangeHasUnsavedChanges(props.formDirty);
5748
}, [props.formDirty]);
5849

5950
useEffect(() => {
60-
props.onChangeHasUnsavedChanges(hasUnsavedChanges);
61-
}, [hasUnsavedChanges]);
62-
63-
useEffect(() => {
64-
if (props.saveElement && hasUnsavedChanges && !doSaveElement) {
51+
if (props.saveElement && props.formDirty && !doSaveElement) {
6552
setDoSaveElement(true);
6653
}
67-
}, [props.saveElement, hasUnsavedChanges, props.increment]);
54+
}, [props.saveElement, props.formDirty, props.increment]);
6855

6956
useEffect(() => {
7057
if (props.connectDragPreview) {
@@ -81,7 +68,7 @@ const Element = (props) => {
8168
useEffect(() => {
8269
if (justClickedPublishButton && formRenderedIfNeeded) {
8370
setJustClickedPublishButton(false);
84-
if (hasUnsavedChanges) {
71+
if (props.formDirty) {
8572
// Save the element first before publishing, which may trigger validation errors
8673
props.submitForm();
8774
setDoPublishElementAfterSave(true);
@@ -92,13 +79,6 @@ const Element = (props) => {
9279
}
9380
}, [justClickedPublishButton, formHasRendered]);
9481

95-
useEffect(() => {
96-
if (doDispatchAddFormChanged) {
97-
setDoDispatchAddFormChanged(false);
98-
props.dispatchAddFormChanged();
99-
}
100-
}, [doDispatchAddFormChanged]);
101-
10282
const getNoTitle = () => i18n.inject(
10383
i18n._t('ElementHeader.NOTITLE', 'Untitled {type} block'),
10484
{ type: props.type.title }
@@ -143,18 +123,7 @@ const Element = (props) => {
143123
showPublishedElementToast(wasError);
144124
setDoPublishElement(false);
145125
setDoPublishElementAfterSave(false);
146-
// Ensure that formDirty becomes falsey after publishing
147-
// We need to call at a later render rather than straight away or redux-form may override this
148-
// and set the form state to dirty under certain conditions
149-
// setTimeout is a hackish way to do this, though I'm not sure how else we can do this
150-
// The core issue is that redux-form will detect changes when a form is hydrated for the first
151-
// time under certain conditions, specifically during a behat test when trying to publish a closed
152-
// block when presumably the apollo cache is empty (or something like that). This happens late and
153-
// there are no hooks/callbacks available after this happens the input onchange handlers are fired
154-
Promise.all(refetchElementalArea())
155-
.then(() => {
156-
setTimeout(() => props.dispatchRemoveFormChanged(), 250);
157-
});
126+
refetchElementalArea();
158127
};
159128

160129
// Save action
@@ -337,10 +306,6 @@ const Element = (props) => {
337306
if (props.type.inlineEditable) {
338307
setPreviewExpanded(true);
339308
}
340-
// Ensure that formDirty remains truthy
341-
// Note we need to call props.dispatchAddFormChanged() on the next render rather than straight away
342-
// or it will get unset by code somewhere else, probably redux-form
343-
setDoDispatchAddFormChanged(true);
344309
// Don't accidentally auto publish the element once validation errors are fixed
345310
if (doPublishElementAfterSave) {
346311
setDoPublishElementAfterSave(false);
@@ -349,7 +314,6 @@ const Element = (props) => {
349314
return;
350315
}
351316
// Form is valid
352-
setHasUnsavedChanges(false);
353317
setNewTitle(title);
354318
if (doPublishElementAfterSave) {
355319
setDoPublishElementAfterSave(false);
@@ -435,6 +399,7 @@ const Element = (props) => {
435399
broken={type.broken}
436400
onFormSchemaSubmitResponse={handleFormSchemaSubmitResponse}
437401
onFormInit={() => handleFormInit(activeTab)}
402+
formDirty={formDirty}
438403
/>
439404
</ElementContext.Provider>
440405
</div>);
@@ -462,7 +427,9 @@ function mapStateToProps(state, ownProps) {
462427

463428
const tabSetName = tabSet && tabSet.id;
464429
const uniqueFieldId = `element.${elementName}__${tabSetName}`;
465-
const formDirty = state.unsavedForms.find((unsaved) => unsaved.name === `element.${elementName}`);
430+
431+
const formName = loadElementFormStateName(ownProps.element.id);
432+
const formDirty = isDirty(`element.${formName}`, getFormState)(state);
466433

467434
// Find name of the active tab in the tab set
468435
// Only defined once an element form is expanded for the first time
@@ -490,16 +457,6 @@ function mapDispatchToProps(dispatch, ownProps) {
490457
// Perform a redux-form remote-submit
491458
dispatch(submit(`element.${elementName}`));
492459
},
493-
dispatchAddFormChanged() {
494-
// Ensures the form identifier is in unsavedForms in the global redux state
495-
// This is used to derive the formDirty prop in mapStateToProps
496-
dispatch(addFormChanged(`element.${elementName}`));
497-
},
498-
dispatchRemoveFormChanged() {
499-
// Removes the form identifier from unsavedForms in the global redux store
500-
// Opposite of beheaviour of dispatchAddFormChanged()
501-
dispatch(removeFormChanged(`element.${elementName}`));
502-
},
503460
actions: {
504461
toasts: bindActionCreators(toastsActions, dispatch),
505462
},

client/src/components/ElementEditor/ElementList.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ class ElementList extends Component {
3939
this.resetState(prevState, false);
4040
return;
4141
}
42+
// Scenario we've just clicked "save" or "submit" on the form
43+
if (this.state.saveAllElements && !prevState.saveAllElements) {
44+
// Reset the validation state of all blocks.
45+
// This mirrors handleBeforeSubmitForm() for individual blocks.
46+
this.resetState(prevState, false);
47+
return;
48+
}
4249
// Scenario Saving all elements and state has just updated because of a formSchema response from
4350
// an inline save - see Element.js handleFormSchemaSubmitResponse()
4451
if (this.state.saveAllElements) {
@@ -154,9 +161,7 @@ class ElementList extends Component {
154161
}
155162

156163
let output = blocks.map(element => {
157-
const saveElement = this.state.saveAllElements
158-
&& this.state.hasUnsavedChangesBlockIDs[element.id]
159-
&& this.state.validBlockIDs[element.id] === null;
164+
const saveElement = this.state.saveAllElements && this.state.hasUnsavedChangesBlockIDs[element.id];
160165
return <div key={element.id}>
161166
<ElementComponent
162167
element={element}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
@retry @job5
2+
Feature: Unsaved changes are identified by changetracker
3+
As a CMS user
4+
I want to know when I have unsaved changes
5+
6+
Background:
7+
Given I add an extension "DNADesign\Elemental\Extensions\ElementalPageExtension" to the "Page" class
8+
And a "page" "Blocks Page" with a "My title" content element with "<p>My content</p>" content
9+
And the "group" "EDITOR" has permissions "Access to 'Pages' section"
10+
And I am logged in as a member of "EDITOR" group
11+
And I go to "/admin/pages"
12+
And I follow "Blocks Page"
13+
14+
Scenario: Change tracking for text and HTML fields
15+
# Note we can't just check for "Save" vs "Saved" because one is a subset of the other,
16+
# so `I should not see a "Save" button` will always fail when the "Saved" button is present.
17+
# Instead, font-icon-tick is a CSS class on the "Saved" button, and font-icon-save is on the "Save" button
18+
Then I should see the "button.font-icon-tick" element
19+
Then I should not see the "button.font-icon-save" element
20+
# Just opening the block doesn't get seen as a "change"
21+
When I click on the caret button for block 1
22+
Then I should see the "button.font-icon-tick" element
23+
Then I should not see the "button.font-icon-save" element
24+
# Update the title
25+
When I fill in "changed" for "Title" for block 1
26+
And I wait for 1 second
27+
Then I should not see the "button.font-icon-tick" element
28+
Then I should see the "button.font-icon-save" element
29+
# Change it back
30+
When I fill in "My title" for "Title" for block 1
31+
Then I should see the "button.font-icon-tick" element
32+
Then I should not see the "button.font-icon-save" element
33+
# Update the HTML content
34+
When I fill in "<p>New sample content</p>" for "Content" for block 1
35+
And I wait for 1 second
36+
Then I should not see the "button.font-icon-tick" element
37+
Then I should see the "button.font-icon-save" element
38+
# Change it back
39+
When I fill in "<p>My content</p>" for "Content" for block 1
40+
Then I should see the "button.font-icon-tick" element
41+
Then I should not see the "button.font-icon-save" element
42+
# Don't click save at the end - there should be no alert because there were no changes
43+

tests/Behat/features/file-upload.feature

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,54 @@ Feature: Files can be saved in and removed from elemental blocks
88
And I add an extension "SilverStripe\FrameworkTest\Elemental\Extension\FileElementalExtension" to the "DNADesign\Elemental\Models\ElementContent" class
99
And I go to "/dev/build?flush"
1010
And a "image" "file1.jpg"
11+
And a "image" "file2.jpg"
1112
And a "page" "Blocks Page" with a "My title" content element with "My content" content
1213
And the "group" "EDITOR" has permissions "Access to 'Pages' section"
1314
And I am logged in as a member of "EDITOR" group
1415
And I go to "/admin/pages"
1516
And I follow "Blocks Page"
1617

18+
Scenario: Add a file and save the block, then remove it and add a different one
19+
# Add a file to the block
20+
Given I click on the caret button for block 1
21+
Then I should not see "file1"
22+
When I click "Choose existing" in the "#Form_ElementForm_1 .uploadfield" element
23+
And I press the "Back" HTML field button
24+
And I click on the file named "file1" in the gallery
25+
And I press the "Insert" button
26+
And I press the "View actions" button
27+
And I click on the ".element-editor__actions-save" element
28+
Then I should see a "Saved 'My title' successfully" success toast
29+
# Check we see the file both in the current page load (react state is correct) and after reloading the form
30+
Then I should see "file1"
31+
When I go to "/admin/pages"
32+
And I follow "Blocks Page"
33+
And I click on the caret button for block 1
34+
Then I should see "file1"
35+
# Then remove the file from the block
36+
And I click on the "#Form_ElementForm_1 .uploadfield-item__remove-btn" element
37+
Then I should not see "file1"
38+
# Try adding the same file back
39+
When I click "Choose existing" in the "#Form_ElementForm_1 .uploadfield" element
40+
And I press the "Back" HTML field button
41+
And I click on the file named "file1" in the gallery
42+
And I press the "Insert" button
43+
And I press the "View actions" button
44+
# same file, so we shouldn't see the button
45+
Then I should not see the save button for block 1
46+
# Add a different file
47+
And I click on the "#Form_ElementForm_1 .uploadfield-item__remove-btn" element
48+
When I click "Choose existing" in the "#Form_ElementForm_1 .uploadfield" element
49+
# Note we don't have to press "Back" here because react knows what folder we were just in before
50+
And I click on the file named "file2" in the gallery
51+
And I press the "Insert" button
52+
And I press the "View actions" button
53+
Then I should see the save button for block 1
54+
And I click on the ".element-editor__actions-save" element
55+
Then I should see a "Saved 'My title' successfully" success toast
56+
And I should see "file2"
57+
And I should not see "file1"
58+
1759
Scenario: Add a file and save the block, then remove the file and save the block
1860
# Add a file to the block
1961
Given I click on the caret button for block 1
@@ -33,6 +75,7 @@ Feature: Files can be saved in and removed from elemental blocks
3375
Then I should see "file1"
3476
# Then remove the file from the block
3577
And I click on the "#Form_ElementForm_1 .uploadfield-item__remove-btn" element
78+
Then I should not see "file1"
3679
And I press the "View actions" button
3780
And I click on the ".element-editor__actions-save" element
3881
Then I should see a "Saved 'My title' successfully" success toast

0 commit comments

Comments
 (0)