Skip to content

Commit

Permalink
Move action for elements
Browse files Browse the repository at this point in the history
  • Loading branch information
Jianbinzhu committed Aug 11, 2023
1 parent 00840f6 commit 906d6d5
Show file tree
Hide file tree
Showing 8 changed files with 5,147 additions and 7 deletions.
4,814 changes: 4,813 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 @@ -80,5 +81,6 @@ export default () => {
updater.component('ElementActions', UnpublishAction, 'ElementActionsWithUnpublish');
updater.component('ElementActions', DuplicateAction, 'ElementActionsWithDuplicate');
updater.component('ElementActions', ArchiveAction, 'ElementActionsWithArchive');
updater.component('ElementActions', MoveAction, 'ElementActionsWithMove');
});
};
68 changes: 68 additions & 0 deletions client/src/components/ElementActions/MoveAction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* 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 }, isPublished, actions: { handleMoveBlock } } = props;
const { element: { id } } = props;

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

setModalOpen(true);
};

const closeModal = () => {
// TODO: refetch the elemental list when the modal is closed
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?
};

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

// Todo: Render modal once per area rather than once per block
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);
44 changes: 44 additions & 0 deletions client/src/state/editor/moveBlockMutation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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, fromPageId, toPageId) => mutate({
variables: {
blockId,
fromPageId,
toPageId,
},
});

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
137 changes: 137 additions & 0 deletions src/Forms/MoveElementHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?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\TreeDropdownField;
use SilverStripe\ORM\ValidationException;
use SilverStripe\ORM\ValidationResult;

class MoveElementHandler
{
use Injectable;

/**
* Parent controller for this form
*
* @var Controller
*/
protected $controller;

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

public function Form($elementID)
{
$fields = FieldList::create([
HiddenField::create(
'ElementID',
null,
$elementID
),
$pageField = TreeDropdownField::create(
'PageID',
'Select a page',
SiteTree::class
)
]);
$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

0 comments on commit 906d6d5

Please sign in to comment.