Commit 2195bde5 authored by becw's avatar becw

Issue #2645490 by Crell: Add form to certain view modes to manage moderation

parents 27c8d764 daf1a650
......@@ -48,7 +48,7 @@ class ModerationHandler implements ModerationHandlerInterface, EntityHandlerInte
/**
* {@inheritdoc}
*/
public function onEntityModerationFormSubmit(ConfigEntityInterface $bundle) {
public function onBundleModerationConfigurationFormSubmit(ConfigEntityInterface $bundle) {
// The Revisions portion of Entity API is not uniformly applied or consistent.
// Until that's fixed in core, we'll make a best-attempt to apply it to
// the common entity patterns so as to avoid every entity type needing to
......
......@@ -44,7 +44,7 @@ interface ModerationHandlerInterface {
* The bundle definition that is being saved.
* @return mixed
*/
public function onEntityModerationFormSubmit(ConfigEntityInterface $bundle);
public function onBundleModerationConfigurationFormSubmit(ConfigEntityInterface $bundle);
/**
* Alters entity forms to enforce revision handling.
......
......@@ -8,8 +8,11 @@
namespace Drupal\workbench_moderation;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\workbench_moderation\Form\EntityModerationForm;
/**
* Defines a class for reacting to entity events.
......@@ -26,6 +29,11 @@ class EntityOperations {
*/
protected $entityTypeManager;
/**
* @var \Drupal\Core\Form\FormBuilderInterface
*/
protected $formBuilder;
/**
* Constructs a new EntityOperations object.
*
......@@ -34,9 +42,10 @@ class EntityOperations {
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* Entity type manager service.
*/
public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager) {
public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder) {
$this->moderationInfo = $moderation_info;
$this->entityTypeManager = $entity_type_manager;
$this->formBuilder = $form_builder;
}
/**
......@@ -55,4 +64,32 @@ class EntityOperations {
}
}
}
/**
* Act on entities being assembled before rendering.
*
* This is a hook bridge.
*
* @see hook_entity_view()
* @see EntityFieldManagerInterface::getExtraFields()
*/
public function entityView(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
if (!$this->moderationInfo->isModeratableEntity($entity)) {
return;
}
if (!$this->moderationInfo->isLatestRevision($entity)) {
return;
}
/** @var ContentEntityInterface $entity */
if ($entity->isDefaultRevision()) {
return;
}
$component = $display->getComponent('workbench_moderation_control');
if ($component) {
$build['workbench_moderation_control'] = $this->formBuilder->getForm(EntityModerationForm::class, $entity);
$build['workbench_moderation_control']['#weight'] = $component['weight'];
}
}
}
......@@ -7,6 +7,7 @@
namespace Drupal\workbench_moderation;
use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
......@@ -15,13 +16,12 @@ use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Url;
use Drupal\workbench_moderation\Form\EntityModerationForm;
use Drupal\workbench_moderation\Routing\EntityModerationRouteProvider;
use Drupal\workbench_moderation\Routing\EntityTypeModerationRouteProvider;
use Drupal\workbench_moderation\Entity\Handler\NodeModerationHandler;
use Drupal\workbench_moderation\Entity\Handler\BlockContentModerationHandler;
use Drupal\workbench_moderation\Entity\Handler\ModerationHandler;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\workbench_moderation\Entity\Handler\NodeModerationHandler;
use Drupal\workbench_moderation\Form\BundleModerationConfigurationForm;
use Drupal\workbench_moderation\Routing\EntityModerationRouteProvider;
use Drupal\workbench_moderation\Routing\EntityTypeModerationRouteProvider;
/**
* Service class for manipulating entity type information.
......@@ -55,6 +55,7 @@ class EntityTypeInfo {
/**
* EntityTypeInfo constructor.
*
* @param \Drupal\Core\StringTranslation\TranslationInterface $translation
* The translation service. for form alters.
* @param \Drupal\workbench_moderation\ModerationInformationInterface $moderation_information
......@@ -137,7 +138,7 @@ class EntityTypeInfo {
}
if (!$type->getFormClass('moderation')) {
$type->setFormClass('moderation', EntityModerationForm::class);
$type->setFormClass('moderation', BundleModerationConfigurationForm::class);
}
// @todo Core forgot to add a direct way to manipulate route_provider, so
......@@ -177,6 +178,73 @@ class EntityTypeInfo {
return $operations;
}
/**
* Gets the "extra fields" for a bundle.
*
* This is a hook bridge.
*
* @see hook_entity_extra_field_info()
*
* @return array
* A nested array of 'pseudo-field' elements. Each list is nested within the
* following keys: entity type, bundle name, context (either 'form' or
* 'display'). The keys are the name of the elements as appearing in the
* renderable array (either the entity form or the displayed entity). The
* value is an associative array:
* - label: The human readable name of the element. Make sure you sanitize
* this appropriately.
* - description: A short description of the element contents.
* - weight: The default weight of the element.
* - visible: (optional) The default visibility of the element. Defaults to
* TRUE.
* - edit: (optional) String containing markup (normally a link) used as the
* element's 'edit' operation in the administration interface. Only for
* 'form' context.
* - delete: (optional) String containing markup (normally a link) used as the
* element's 'delete' operation in the administration interface. Only for
* 'form' context.
*/
public function entityExtraFieldInfo() {
$return = [];
foreach ($this->getModeratedBundles() as $bundle) {
$return[$bundle['entity']][$bundle['bundle']]['display']['workbench_moderation_control'] = [
'label' => $this->t('Moderation control'),
'description' => $this->t('Status listing and form for the entitiy\'s moderation state.'),
'weight' => -20,
'visible' => TRUE,
];
}
return $return;
}
/**
* Returns an iterable list of entity names and bundle names under moderation.
*
* That is, this method returns a list of bundles that have Workbench
* Moderation enabled on them.
*
* @return \Generator
* A generator, yielding a 2 element associative array:
* - entity: The machine name of an entity, such as "node" or "block_content".
* - bundle: The machine name of a bundle, such as "page" or "article".
*/
protected function getModeratedBundles() {
$revisionable_types = $this->moderationInfo->selectRevisionableEntityTypes($this->entityTypeManager->getDefinitions());
/** @var ConfigEntityTypeInterface $type */
foreach ($revisionable_types as $type_name => $type) {
$result = $this->entityTypeManager
->getStorage($type_name)
->getQuery()
->condition('third_party_settings.workbench_moderation.enabled', TRUE)
->execute();
foreach ($result as $bundle_name) {
yield ['entity' => $type->getBundleOf(), 'bundle' => $bundle_name];
}
}
}
/**
* Force moderatable bundles to have a moderation_state field.
*
......
<?php
namespace Drupal\workbench_moderation\Form;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\workbench_moderation\Entity\ModerationState;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form for configuring moderation usage on a given entity bundle.
*/
class BundleModerationConfigurationForm extends EntityForm {
/**
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* @inheritDoc
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* @inheritDoc
*/
public static function create(ContainerInterface $container) {
return new static($container->get('entity_type.manager'));
}
/**
* {@inheritdoc}
*
* We need to blank out the base form ID so that poorly written form alters
* that use the base form ID to target both add and edit forms don't pick
* up our form. This should be fixed in core.
*/
public function getBaseFormId() {
return NULL;
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
/* @var ConfigEntityTypeInterface $bundle */
$bundle = $form_state->getFormObject()->getEntity();
$form['enable_moderation_state'] = [
'#type' => 'checkbox',
'#title' => t('Enable moderation states.'),
'#description' => t('Content of this type must transition through moderation states in order to be published.'),
'#default_value' => $bundle->getThirdPartySetting('workbench_moderation', 'enabled', FALSE),
];
// Add a special message when moderation is being disabled.
if ($bundle->getThirdPartySetting('workbench_moderation', 'enabled', FALSE)) {
$form['enable_moderation_state_note'] = [
'#type' => 'item',
'#description' => t('After disabling moderation, any existing forward drafts will be accessible via the "Revisions" tab.'),
'#states' => [
'visible' => [
':input[name=enable_moderation_state]' => ['checked' => FALSE],
],
],
];
}
$states = \Drupal::entityTypeManager()->getStorage('moderation_state')->loadMultiple();
$options = [];
/** @var ModerationState $state */
foreach ($states as $key => $state) {
$options[$key] = $state->label() . ' ' . ($state->isPublishedState() ? t('(published)') : t('(non-published)'));
}
$form['allowed_moderation_states'] = [
'#type' => 'checkboxes',
'#title' => t('Allowed moderation states.'),
'#description' => t('The allowed moderation states this content-type can be assigned. You must select at least one published and one non-published state.'),
'#default_value' => $bundle->getThirdPartySetting('workbench_moderation', 'allowed_moderation_states', []),
'#options' => $options,
'#states' => [
'visible' => [
':input[name=enable_moderation_state]' => ['checked' => TRUE],
],
],
];
$form['default_moderation_state'] = [
'#type' => 'select',
'#title' => t('Default moderation state'),
'#empty_option' => t('-- Select --'),
'#options' => $options,
'#description' => t('Select the moderation state for new content'),
'#default_value' => $bundle->getThirdPartySetting('workbench_moderation', 'default_moderation_state', ''),
'#states' => [
'visible' => [
':input[name=enable_moderation_state]' => ['checked' => TRUE],
],
],
];
$form['#entity_builders'][] = [$this, 'formBuilderCallback'];
return parent::form($form, $form_state);
}
/**
* Form builder callback.
*
* @todo I don't know why this needs to be separate from the form() method.
* It was in the form_alter version but we should see if we can just fold
* it into the method above.
*
* @param $entity_type
* @param \Drupal\Core\Config\Entity\ConfigEntityInterface $bundle
* @param array $form
* @param \Drupal\Core\Form\FormStateInterface $form_state
*/
public function formBuilderCallback($entity_type, ConfigEntityInterface $bundle, &$form, FormStateInterface $form_state) {
// @todo write a test for this.
$bundle->setThirdPartySetting('workbench_moderation', 'enabled', $form_state->getValue('enable_moderation_state'));
$bundle->setThirdPartySetting('workbench_moderation', 'allowed_moderation_states', array_keys(array_filter($form_state->getValue('allowed_moderation_states'))));
$bundle->setThirdPartySetting('workbench_moderation', 'default_moderation_state', $form_state->getValue('default_moderation_state'));
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
// @todo write a test for this.
if ($form_state->getValue('enable_moderation_state')) {
$states = $this->entityTypeManager->getStorage('moderation_state')->loadMultiple();
$published = FALSE;
$non_published = TRUE;
$allowed = array_keys(array_filter($form_state->getValue('allowed_moderation_states')));
foreach ($allowed as $state_id) {
/** @var ModerationState $state */
$state = $states[$state_id];
if ($state->isPublishedState()) {
$published = TRUE;
}
else {
$non_published = TRUE;
}
}
if (!$published || !$non_published) {
$form_state->setErrorByName('allowed_moderation_states', t('You must select at least one published moderation and one non-published state.'));
}
if (($default = $form_state->getValue('default_moderation_state')) && !empty($default)) {
if (!in_array($default, $allowed, TRUE)) {
$form_state->setErrorByName('default_moderation_state', t('The default moderation state must be one of the allowed states.'));
}
}
else {
$form_state->setErrorByName('default_moderation_state', t('You must select a default moderation state.'));
}
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// If moderation is enabled, revisions MUST be enabled as well.
// Otherwise we can't have forward revisions.
if($form_state->getValue('enable_moderation_state')) {
/* @var ConfigEntityTypeInterface $bundle */
$bundle = $form_state->getFormObject()->getEntity();
$this->entityTypeManager->getHandler($bundle->getEntityType()->getBundleOf(), 'moderation')->onBundleModerationConfigurationFormSubmit($bundle);
}
parent::submitForm( $form, $form_state);
drupal_set_message($this->t('Your settings have been saved.'));
}
}
<?php
/**
* @file
* Contains \Drupal\workbench_moderation\Form\EntityModerationForm.
*/
namespace Drupal\workbench_moderation\Form;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Config\Entity\ConfigEntityTypeInterface;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\workbench_moderation\Entity\ModerationState;
use Drupal\workbench_moderation\ModerationInformationInterface;
use Drupal\workbench_moderation\StateTransitionValidation;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form for configuring moderation usage on a given entity bundle.
*/
class EntityModerationForm extends EntityForm {
class EntityModerationForm extends FormBase {
/**
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
* @var \Drupal\workbench_moderation\ModerationInformationInterface
*/
protected $entityTypeManager;
protected $moderationInfo;
/**
* @inheritDoc
* @var \Drupal\workbench_moderation\StateTransitionValidation
*/
protected $validation;
/**
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
protected $entityTypeManager;
public function __construct(ModerationInformationInterface $moderation_info, StateTransitionValidation $validation, EntityTypeManagerInterface $entity_type_manager) {
$this->moderationInfo = $moderation_info;
$this->validation = $validation;
$this->entityTypeManager = $entity_type_manager;
}
......@@ -32,151 +44,80 @@ class EntityModerationForm extends EntityForm {
* @inheritDoc
*/
public static function create(ContainerInterface $container) {
return new static($container->get('entity_type.manager'));
return new static(
$container->get('workbench_moderation.moderation_information'),
$container->get('workbench_moderation.state_transition_validation'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*
* We need to blank out the base form ID so that poorly written form alters
* that use the base form ID to target both add and edit forms don't pick
* up our form. This should be fixed in core.
* @inheritDoc
*/
public function getBaseFormId() {
return NULL;
public function getFormId() {
return 'workbench_moderation_entity_moderation_form';
}
/**
* {@inheritdoc}
* @inheritDoc
*/
public function form(array $form, FormStateInterface $form_state) {
/* @var ConfigEntityTypeInterface $bundle */
$bundle = $form_state->getFormObject()->getEntity();
$form['enable_moderation_state'] = [
'#type' => 'checkbox',
'#title' => t('Enable moderation states.'),
'#description' => t('Content of this type must transition through moderation states in order to be published.'),
'#default_value' => $bundle->getThirdPartySetting('workbench_moderation', 'enabled', FALSE),
];
public function buildForm(array $form, FormStateInterface $form_state, ContentEntityInterface $entity = NULL) {
$target_states = $this->validation->getValidTransitionTargets($entity, $this->currentUser());
$target_states = array_map(function(ModerationState $state) {
return $state->label();
}, $target_states);
// Add a special message when moderation is being disabled.
if ($bundle->getThirdPartySetting('workbench_moderation', 'enabled', FALSE)) {
$form['enable_moderation_state_note'] = [
'#type' => 'item',
'#description' => t('After disabling moderation, any existing forward drafts will be accessible via the "Revisions" tab.'),
'#states' => [
'visible' => [
':input[name=enable_moderation_state]' => ['checked' => FALSE],
],
],
/** @var ModerationState $current_state */
$current_state = $entity->moderation_state->entity;
if ($current_state) {
$form['current'] = [
'#type' => 'markup',
'#markup' => $this->t('Current status: %state', ['%state' => $current_state->label()]),
];
}
$states = \Drupal::entityTypeManager()->getStorage('moderation_state')->loadMultiple();
$options = [];
/** @var ModerationState $state */
foreach ($states as $key => $state) {
$options[$key] = $state->label() . ' ' . ($state->isPublishedState() ? t('(published)') : t('(non-published)'));
}
$form['allowed_moderation_states'] = [
'#type' => 'checkboxes',
'#title' => t('Allowed moderation states.'),
'#description' => t('The allowed moderation states this content-type can be assigned. You must select at least one published and one non-published state.'),
'#default_value' => $bundle->getThirdPartySetting('workbench_moderation', 'allowed_moderation_states', []),
'#options' => $options,
'#states' => [
'visible' => [
':input[name=enable_moderation_state]' => ['checked' => TRUE],
],
],
];
$form['default_moderation_state'] = [
// Persist the entity so we can access it in the submit handler.
$form_state->set('entity', $entity);
$form['new_state'] = [
'#type' => 'select',
'#title' => t('Default moderation state'),
'#empty_option' => t('-- Select --'),
'#options' => $options,
'#description' => t('Select the moderation state for new content'),
'#default_value' => $bundle->getThirdPartySetting('workbench_moderation', 'default_moderation_state', ''),
'#states' => [
'visible' => [
':input[name=enable_moderation_state]' => ['checked' => TRUE],
],
],
'#title' => $this->t('New state'),
'#options' => $target_states,
];
$form['#entity_builders'][] = [$this, 'formBuilderCallback'];
return parent::form($form, $form_state);
}
/**
* Form builder callback.
*
* @todo I don't know why this needs to be separate from the form() method.
* It was in the form_alter version but we should see if we can just fold
* it into the method above.
*
* @param $entity_type
* @param \Drupal\Core\Config\Entity\ConfigEntityInterface $bundle
* @param array $form
* @param \Drupal\Core\Form\FormStateInterface $form_state
*/
public function formBuilderCallback($entity_type, ConfigEntityInterface $bundle, &$form, FormStateInterface $form_state) {
// @todo write a test for this.
$bundle->setThirdPartySetting('workbench_moderation', 'enabled', $form_state->getValue('enable_moderation_state'));
$bundle->setThirdPartySetting('workbench_moderation', 'allowed_moderation_states', array_keys(array_filter($form_state->getValue('allowed_moderation_states'))));
$bundle->setThirdPartySetting('workbench_moderation', 'default_moderation_state', $form_state->getValue('default_moderation_state'));
}
$form['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Update'),
];
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
// @todo write a test for this.
if ($form_state->getValue('enable_moderation_state')) {
$states = $this->entityTypeManager->getStorage('moderation_state')->loadMultiple();
$published = FALSE;
$non_published = TRUE;
$allowed = array_keys(array_filter($form_state->getValue('allowed_moderation_states')));
foreach ($allowed as $state_id) {
/** @var ModerationState $state */
$state = $states[$state_id];
if ($state->isPublishedState()) {
$published = TRUE;
}
else {
$non_published = TRUE;
}
}
if (!$published || !$non_published) {
$form_state->setErrorByName('allowed_moderation_states', t('You must select at least one published moderation and one non-published state.'));
}
if (($default = $form_state->getValue('default_moderation_state')) && !empty($default)) {
if (!in_array($default, $allowed, TRUE)) {
$form_state->setErrorByName('default_moderation_state', t('The default moderation state must be one of the allowed states.'));
}
}
else {
$form_state->setErrorByName('default_moderation_state', t('You must select a default moderation state.'));
}
}
return $form;
}
/**
* {@inheritdoc}
* @inheritDoc
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
/** @var ContentEntityInterface $entity */
$entity = $form_state->get('entity');
// If moderation is enabled, revisions MUST be enabled as well.
// Otherwise we can't have forward revisions.
if($form_state->getValue('enable_moderation_state')) {
/* @var ConfigEntityTypeInterface $bundle */
$bundle = $form_state->getFormObject()->getEntity();
$new_state = $form_state->getValue('new_state');
$this->entityTypeManager->getHandler($bundle->getEntityType()->getBundleOf(), 'moderation')->onEntityModerationFormSubmit($bundle);
}
$entity->moderation_state->target_id = $new_state;
$entity->save();
parent::submitForm( $form, $form_state);
drupal_set_message($this->t('The moderation state has been updated.'));
drupal_set_message($this->t('Your settings have been saved.'));
/** @var ModerationState $state */
$state = $this->entityTypeManager->getStorage('moderation_state')->load($new_state);
// The page we're on likely won't be visible if we just set the entity to
// the default state, as we hide that latest-revision tab if there is no
// forward revision. Redirect to the canonical URL instead, since that will
// still exist.
if ($state->isDefaultRevisionState()) {