Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move action for elements #1

Open
wants to merge 4 commits into
base: 4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions _graphql/mutations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@
'sortBlock(id: ID!, afterBlockID: ID!)':
model: DNADesign\Elemental\Models\BaseElement
description: Changes the sort position of an element
'moveBlock(id: ID!)':
model: DNADesign\Elemental\Models\BaseElement
description: Move an element to another page
4,820 changes: 4,819 additions & 1 deletion client/dist/js/bundle.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion client/dist/styles/bundle.css

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 @@ -9,6 +9,7 @@ import DuplicateAction from 'components/ElementActions/DuplicateAction';
import PublishAction from 'components/ElementActions/PublishAction';
import SaveAction from 'components/ElementActions/SaveAction';
import UnpublishAction from 'components/ElementActions/UnpublishAction';
import MoveAction from 'components/ElementActions/MoveAction';

export default () => {
Injector.transform(
Expand Down Expand Up @@ -79,6 +80,7 @@ export default () => {
updater.component('ElementActions', PublishAction, 'ElementActionsWithPublish');
updater.component('ElementActions', UnpublishAction, 'ElementActionsWithUnpublish');
updater.component('ElementActions', DuplicateAction, 'ElementActionsWithDuplicate');
updater.component('ElementActions', MoveAction, 'ElementActionsWithMove');
updater.component('ElementActions', ArchiveAction, 'ElementActionsWithArchive');
});
};
73 changes: 73 additions & 0 deletions client/src/components/ElementActions/MoveAction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* global window */
import React, { useState } from 'react';
import { loadComponent } from 'lib/Injector';
import { compose } from 'redux';
import AbstractAction from 'components/ElementActions/AbstractAction';
import moveBlockMutation from 'state/editor/moveBlockMutation';
import { getConfig } from 'state/editor/elementConfig';
import i18n from 'i18n';

/**
* Adds the elemental menu action to move a block of any state
*/
const MoveAction = (MenuComponent) => (props) => {
const FormBuilderModal = loadComponent('FormBuilderModal');
const [modalOpen, setModalOpen] = useState(false);
const { element: { id }, actions: { handleMoveBlock } } = props;

const handleClick = (event) => {
event.stopPropagation();

setModalOpen(true);
};

const closeModal = () => {
if (handleMoveBlock) {
handleMoveBlock(id).then(() => {
const preview = window.jQuery('.cms-preview');
preview.entwine('ss.preview')._loadUrl(preview.find('iframe').attr('src'));
});
}

setModalOpen(false);
};

// Allow user to move to another page if they have create permissions
const disabled = props.element.canCreate !== undefined && !props.element.canCreate;
const label = i18n._t('ElementMoveAction.MOVE', 'Move');
const title = disabled
? i18n._t('ElementMoveAction.MOVE_PERMISSION_DENY', 'Move, insufficient permissions')
: label;
const newProps = {
label,
title,
disabled,
className: 'element-editor__actions-move',
onClick: handleClick,
toggle: props.toggle, // todo: what is this?
Jianbinzhu marked this conversation as resolved.
Show resolved Hide resolved
};

const modalSchemaUrl = `${getConfig().form.elementForm.moveModalSchemaUrl}/${id}`;

// Todo: Render modal once per area rather than once per block

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this from module code or your TODO?

return (
<MenuComponent {...props}>
{props.children}
<AbstractAction {...newProps} />
<FormBuilderModal
title="Move block to"
identifier="Elemental.MoveBlockTo"
isOpen={modalOpen}
onClosed={closeModal}
schemaUrl={modalSchemaUrl}
bodyClassName="modal__dialog"
responseClassBad="modal__response modal__response--error"
responseClassGood="modal__response modal__response--good"
/>
</MenuComponent>
);
};

export { MoveAction as Component };

export default compose(moveBlockMutation, MoveAction);
42 changes: 42 additions & 0 deletions client/src/state/editor/moveBlockMutation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { graphql } from 'react-apollo';
import gql from 'graphql-tag';
import { config as readBlocksConfig, query as readBlocksQuery } from './readBlocksForAreaQuery';

// GraphQL query for moving a specific block
const mutation = gql`
mutation MoveBlock($blockId:ID!) {
moveBlock(
id: $blockId
) {
id
}
}
`;

const config = {
props: ({ mutate, ownProps: { actions } }) => {
const handleMoveBlock = (blockId) => mutate({
variables: {
blockId,
},
});

return {
actions: {
...actions,
handleMoveBlock,
},
};
},
options: ({ areaId }) => ({
// Refetch versions after mutation is completed
refetchQueries: [{
query: readBlocksQuery,
variables: readBlocksConfig.options({ areaId }).variables
}]
}),
};

export { mutation, config };

export default graphql(mutation, config);
53 changes: 50 additions & 3 deletions src/Controllers/ElementalAreaController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@
namespace DNADesign\Elemental\Controllers;

use DNADesign\Elemental\Forms\EditFormFactory;
use DNADesign\Elemental\Forms\MoveElementHandler;
use DNADesign\Elemental\Models\BaseElement;
use DNADesign\Elemental\Services\ElementTypeRegistry;
use Exception;
use Psr\Log\LoggerInterface;
use SilverStripe\CMS\Controllers\CMSMain;
use SilverStripe\Control\Controller;
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;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\SecurityToken;

/**
Expand All @@ -38,13 +41,15 @@ class ElementalAreaController extends CMSMain
'schema',
'apiSaveForm',
'formAction',
'moveElementForm',
];

public function getClientConfig()
{
$clientConfig = parent::getClientConfig();
$clientConfig['form']['elementForm'] = [
'schemaUrl' => $this->Link('schema/elementForm'),
'moveModalSchemaUrl' => $this->Link('schema/moveElementForm'),
'saveUrl' => $this->Link('api/saveForm'),
'saveMethod' => 'post',
'payloadFormat' => 'json',
Expand Down Expand Up @@ -185,15 +190,57 @@ public function formAction(HTTPRequest $request)
{
$formName = $request->param('FormName');

// Get the element ID from the form name
$id = substr($formName ?? '', strlen(sprintf(self::FORM_NAME_TEMPLATE ?? '', '')));
$form = $this->getElementForm($id);
if (substr($formName, 0, 15) === 'MoveElementForm') {
// Get the element ID from the form name
$id = substr($formName, strlen(sprintf('MoveElementForm_%s', '')));
$form = $this->getMoveElementForm($id);
} else {
// Get the element ID from the form name
$id = substr($formName, strlen(sprintf(self::FORM_NAME_TEMPLATE, '')));
$form = $this->getElementForm($id);
}

$field = $form->getRequestHandler()->handleField($request);

return $field->handleRequest($request);
}

public function moveElementForm(HTTPRequest $request)
{
$elementID = $request->param('ElementID');
return $this->getMoveElementForm($elementID);
}

public function getMoveElementForm($elementID)
{
$handler = MoveElementHandler::create($this);
$form = $handler->Form($elementID);

$form->setValidationResponseCallback(function (ValidationResult $errors) use ($form, $elementID) {
$schemaId = Controller::join_links($this->Link('schema/moveElementForm'), $elementID);
return $this->getSchemaResponse($schemaId, $form, $errors);
});

return $form;
}

public function moveelement($data, $form)
{
$id = $data['ElementID'];
$record = BaseElement::get()->byID($id);
$handler = MoveElementHandler::create($this);
$results = $handler->moveElement($record, $data);

if (!isset($results)) {
return null;
}

// Send extra "message" data with schema response
$extraData = ['message' => $results];
$schemaId = Controller::join_links($this->Link('schema/moveElementForm'), $id);
return $this->getSchemaResponse($schemaId, $form, null, $extraData);
}

/**
* Remove the pseudo namespaces that were added to form fields by the form factory
*
Expand Down
143 changes: 143 additions & 0 deletions src/Forms/MoveElementHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
<?php

namespace DNADesign\Elemental\Forms;

use DNADesign\Elemental\Extensions\ElementalPageExtension;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\Form;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\TreeDropdownField;
use SilverStripe\ORM\ValidationException;
use SilverStripe\ORM\ValidationResult;

class MoveElementHandler
{
use Injectable;

/**
* Parent controller for this form
*
* @var Controller
*/
protected $controller;
Jianbinzhu marked this conversation as resolved.
Show resolved Hide resolved

public function __construct($controller = null)
{
$this->controller = $controller;
}

public function Form($elementID)
{
$fields = FieldList::create([
LiteralField::create(
'MoveWarning',
'<p class="alert alert-info"><strong>Note</strong>: All published blocks will changed draft state once moved.</p>'
),
HiddenField::create(
'ElementID',
null,
$elementID
),
$pageField = TreeDropdownField::create(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Limit to pages with elemental active

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, this is done on L62

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was able to move a block to a page without elemental

'PageID',
'Select a page',
SiteTree::class
)
// TODO add check if there's multiple elemental areas on the selected page
]);
$actions = FieldList::create([
FormAction::create('moveelement', 'Move')
->addExtraClass('btn btn-primary')
]);

$pageField->setDisableFunction(function ($page) {
return !$page->hasExtension(ElementalPageExtension::class);
});

$form = Form::create(
$this->controller,
sprintf('MoveElementForm_%s', $elementID),
$fields,
$actions
);

// Todo: make this dynamic
$form->setFormAction('admin/elemental-area/MoveElementForm/');
$form->addExtraClass('form--no-dividers');

return $form;
}

public function moveElement($element, $formData)
{
$page = SiteTree::get()->byId($formData['PageID']);

// if ElementalAreaNotFound
if (!$page->ElementalArea()->exists()) {
throw $this->validationResult(_t(
__CLASS__ . '.ElementalAreaNotFound',
'Could not find an elemental area on <strong>{PageName}</strong> to move <strong>{BlockName}</strong> to',
[
'PageName' => $page->Title,
'BlockName' => $element->Title
]
));
}

if (!$page->canEdit() || !$element->canEdit()) {
throw $this->validationResult(_t(
__CLASS__ . '.InsufficientPermissions',
'Can not move <strong>{PageName}</strong> to <strong>{BlockName}</strong> due to insufficient permissions',
[
'PageName' => $page->Title,
'BlockName' => $element->Title
]
));
}

// TODO: Error handling
// TODO: pages with multiple element areas
// TODO: How does this work with sort?
$page->ElementalArea()->Elements()->add($element->ID);

$request = $this->controller->getRequest();
$message = _t(
__CLASS__ . '.Success',
'Successfully moved <a href="{BlockEditLink}">{BlockName}</a> to <a href="{PageEditLink}">{PageName}</a>.',
[
'BlockName' => $element->Title,
'BlockEditLink' => $element->MovedElementCMSLink(true, $element->ID),
'PageName' => $page->Title,
'PageEditLink' => $page->CMSEditLink(),
]
);
if ($request->getHeader('X-Formschema-Request')) {
return $message;
} elseif (Director::is_ajax()) {
$response = new HTTPResponse($message, 200);

$response->addHeader('Content-Type', 'text/html; charset=utf-8');
return $response;
} else {
return $this->controller->redirectBack();
}
}

/**
* Raise validation error
*
* @param string $message
* @param string $field
* @return ValidationException
*/
protected function validationResult($message, $field = null)
{
$error = ValidationResult::create()
->addFieldError($field, $message);
return new ValidationException($error);
}
}
Loading