Skip to content

Commit

Permalink
NEW Add save action to element inline edit forms
Browse files Browse the repository at this point in the history
  • Loading branch information
robbieaverill authored and raissanorth committed Sep 25, 2018
1 parent ff1c82a commit b639f68
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 14 deletions.
2 changes: 1 addition & 1 deletion client/dist/js/bundle.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions client/src/boot/registerTransforms.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import revertToBlockVersionMutation from 'state/history/revertToBlockVersionMuta
import readBlocksForPageQuery from 'state/editor/readBlocksForPageQuery';
import ArchiveAction from 'components/ElementActions/ArchiveAction';
import PublishAction from 'components/ElementActions/PublishAction';
import SaveAction from 'components/ElementActions/SaveAction';
import UnpublishAction from 'components/ElementActions/UnpublishAction';

export default () => {
Expand Down Expand Up @@ -60,6 +61,7 @@ export default () => {

// Add elemental editor actions
Injector.transform('element-actions', (updater) => {
updater.component('ElementActions', SaveAction, 'ElementActionsWithSave');
updater.component('ElementActions', ArchiveAction, 'ElementActionsWithArchive');
updater.component('ElementActions', PublishAction, 'ElementActionsWithPublish');
updater.component('ElementActions', UnpublishAction, 'ElementActionsWithUnpublish');
Expand Down
50 changes: 50 additions & 0 deletions client/src/components/ElementActions/SaveAction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/* global document */
import React from 'react';
import AbstractAction from 'components/ElementActions/AbstractAction';
import backend from 'lib/Backend';
import i18n from 'i18n';
import { loadElementSchemaValue } from 'state/editor/loadElementSchemaValue';
import { getSerializedFormData } from 'state/editor/getSerializedFormData';

/**
* Using a REST backend, serialize the current form data and post it to the backend endpoint to save
* the inline edit form's data for the current block.
*/
const SaveAction = (MenuComponent) => (props) => {
const handleClick = (event) => {
event.stopPropagation();

const { id } = this.props;

const formData = getSerializedFormData(`Form_ElementForm_${id}`);

const endpointSpec = {
url: loadElementSchemaValue('saveUrl', id),
method: loadElementSchemaValue('saveMethod'),
payloadFormat: loadElementSchemaValue('payloadFormat'),
};

// @todo add CSRF SecurityID token
const endpoint = backend.createEndpointFetcher(endpointSpec);
endpoint(formData).then((result) => {
console.log(result);
// @todo update apollo cache?
});
};

const newProps = {
title: i18n._t('SaveAction.SAVE', 'Save'),
extraClass: 'element-editor__actions-save',
onClick: handleClick,
};

return (
<MenuComponent {...props}>
{props.children}

<AbstractAction {...newProps} />
</MenuComponent>
);
};

export default SaveAction;
1 change: 0 additions & 1 deletion client/src/components/ElementEditor/Header.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ class Header extends Component {
});
}


/**
* Renders a message indicating the current versioned state of the element
*
Expand Down
15 changes: 5 additions & 10 deletions client/src/components/ElementEditor/InlineEditForm.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import React, { Component, PropTypes } from 'react';
import React, { PureComponent, PropTypes } from 'react';
import classnames from 'classnames';
import FormBuilderLoader from 'containers/FormBuilderLoader/FormBuilderLoader';
import Config from 'lib/Config';

class InlineEditForm extends Component {
getConfig() {
const sectionKey = 'DNADesign\\Elemental\\Controllers\\ElementalAreaController';
return Config.getSection(sectionKey);
}
import { loadElementSchemaValue } from 'state/editor/loadElementSchemaValue';

class InlineEditForm extends PureComponent {
render() {
const { extraClass, elementId, onClick } = this.props;
const { elementId, extraClass, onClick } = this.props;

const classNames = classnames('element-editor-editform', extraClass);
const schemaUrl = `${this.getConfig().form.elementForm.schemaUrl}/${elementId}`;
const schemaUrl = loadElementSchemaValue('schemaUrl', elementId);

const formProps = {
formTag: 'div',
Expand Down
23 changes: 23 additions & 0 deletions client/src/state/editor/getSerializedFormData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/* global document */
/**
* Get all form inputs (etc) inside the given formName div and return as a serialised object
*
* @param {string} formName
* @returns {object}
*/
export const getSerializedFormData = (formName) => {
const form = document.getElementById(formName);
const fields = form.querySelectorAll('input, select, textarea, checkbox');

const output = {};
for (let i = 0; i <= fields.length; i++) {
if (fields[i]) {
const formField = fields[i];
const fieldName = formField.name;

output[fieldName] = formField.value;
}
}

return output;
};
19 changes: 19 additions & 0 deletions client/src/state/editor/loadElementSchemaValue.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Config from 'lib/Config';

/**
* Returns the named component from the elementForm's schema data
*
* @param {string} key
* @param {number|null} elementId If provided, will be concatenated on as value is treated as a URL
* @returns {string}
*/
export const loadElementSchemaValue = (key, elementId = null) => {
const sectionKey = 'DNADesign\\Elemental\\Controllers\\ElementalAreaController';
const section = Config.getSection(sectionKey);
const schemaValue = section.form.elementForm[key] || '';

if (elementId) {
return `${schemaValue}/${elementId}`;
}
return schemaValue;
};
70 changes: 68 additions & 2 deletions src/Controllers/ElementalAreaController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@

use DNADesign\Elemental\Forms\EditFormFactory;
use DNADesign\Elemental\Models\BaseElement;
use Exception;
use Psr\Log\LoggerInterface;
use SilverStripe\Admin\LeftAndMain;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Control\HTTPResponse;
use SilverStripe\Control\HTTPResponse_Exception;
use SilverStripe\Core\Convert;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Forms\Form;

Expand All @@ -19,16 +23,25 @@ class ElementalAreaController extends LeftAndMain

private static $ignore_menuitem = true;

private static $allowed_actions = array(
private static $url_handlers = [
// API access points with structured data
'POST api/saveForm/$ID' => 'apiSaveForm',
];

private static $allowed_actions = [
'elementForm',
'schema',
);
'apiSaveForm',
];

public function getClientConfig()
{
$clientConfig = parent::getClientConfig();
$clientConfig['form']['elementForm'] = [
'schemaUrl' => $this->Link('schema/elementForm'),
'saveUrl' => $this->Link('api/saveForm'),
'saveMethod' => 'post',
'payloadFormat' => 'json',
];
return $clientConfig;
}
Expand Down Expand Up @@ -75,4 +88,57 @@ public function getElementForm($elementID)

return $form;
}

/**
* Save an inline edit form for a block
*
* @todo CSRF protection!
* @param HTTPRequest $request
* @return HTTPResponse|null JSON encoded string or null if an exception is thrown
* @throws HTTPResponse_Exception
*/
public function apiSaveForm(HTTPRequest $request)
{
// Validate required input data
if (!isset($this->urlParams['ID'])) {
$this->jsonError(400);
return null;
}

$data = Convert::json2array($request->getBody());
if (empty($data)) {
$this->jsonError(400);
return null;
}

/** @var BaseElement $element */
$element = BaseElement::get()->byID($this->urlParams['ID']);
// Ensure the element can be edited by the current user
if (!$element || !$element->canEdit()) {
$this->jsonError(403);
return null;
}

try {
$updated = false;
$element->update($data);
// Check if anything will actually be changed before writing
if ($element->isChanged()) {
$element->write();
// Track changes so we can return to the client
$updated = true;
}
} catch (Exception $ex) {
Injector::inst()->get(LoggerInterface::class)->debug($ex->getMessage());

$this->jsonError(500);
return null;
}

$body = Convert::raw2json([
'status' => 'success',
'updated' => $updated,
]);
return HTTPResponse::create($body)->addHeader('Content-Type', 'application/json');
}
}

0 comments on commit b639f68

Please # to comment.