diff --git a/core/modules/content_moderation/config/install/content_moderation.state.archived.yml b/core/modules/content_moderation/config/install/content_moderation.state.archived.yml deleted file mode 100644 index 0279481..0000000 --- a/core/modules/content_moderation/config/install/content_moderation.state.archived.yml +++ /dev/null @@ -1,8 +0,0 @@ -langcode: en -status: true -dependencies: { } -id: archived -label: Archived -published: false -default_revision: true -weight: -8 diff --git a/core/modules/content_moderation/config/install/content_moderation.state.draft.yml b/core/modules/content_moderation/config/install/content_moderation.state.draft.yml deleted file mode 100644 index c7eb64c..0000000 --- a/core/modules/content_moderation/config/install/content_moderation.state.draft.yml +++ /dev/null @@ -1,8 +0,0 @@ -langcode: en -status: true -dependencies: { } -id: draft -label: Draft -published: false -default_revision: false -weight: -10 diff --git a/core/modules/content_moderation/config/install/content_moderation.state.published.yml b/core/modules/content_moderation/config/install/content_moderation.state.published.yml deleted file mode 100644 index 8467e86..0000000 --- a/core/modules/content_moderation/config/install/content_moderation.state.published.yml +++ /dev/null @@ -1,8 +0,0 @@ -langcode: en -status: true -dependencies: { } -id: published -label: Published -published: true -default_revision: true -weight: -9 diff --git a/core/modules/content_moderation/config/install/content_moderation.state_transition.archived_draft.yml b/core/modules/content_moderation/config/install/content_moderation.state_transition.archived_draft.yml deleted file mode 100644 index 8fbf9c3..0000000 --- a/core/modules/content_moderation/config/install/content_moderation.state_transition.archived_draft.yml +++ /dev/null @@ -1,11 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - content_moderation.state.archived - - content_moderation.state.draft -id: archived_draft -label: 'Un-archive to Draft' -stateFrom: archived -stateTo: draft -weight: -5 diff --git a/core/modules/content_moderation/config/install/content_moderation.state_transition.archived_published.yml b/core/modules/content_moderation/config/install/content_moderation.state_transition.archived_published.yml deleted file mode 100644 index 4be7600..0000000 --- a/core/modules/content_moderation/config/install/content_moderation.state_transition.archived_published.yml +++ /dev/null @@ -1,11 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - content_moderation.state.archived - - content_moderation.state.published -id: archived_published -label: 'Un-archive' -stateFrom: archived -stateTo: published -weight: -4 diff --git a/core/modules/content_moderation/config/install/content_moderation.state_transition.draft_draft.yml b/core/modules/content_moderation/config/install/content_moderation.state_transition.draft_draft.yml deleted file mode 100644 index 0ba0f34..0000000 --- a/core/modules/content_moderation/config/install/content_moderation.state_transition.draft_draft.yml +++ /dev/null @@ -1,10 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - content_moderation.state.draft -id: draft_draft -label: 'Create New Draft' -stateFrom: draft -stateTo: draft -weight: -10 diff --git a/core/modules/content_moderation/config/install/content_moderation.state_transition.draft_published.yml b/core/modules/content_moderation/config/install/content_moderation.state_transition.draft_published.yml deleted file mode 100644 index cf95d3d..0000000 --- a/core/modules/content_moderation/config/install/content_moderation.state_transition.draft_published.yml +++ /dev/null @@ -1,11 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - content_moderation.state.draft - - content_moderation.state.published -id: draft_published -label: 'Publish' -stateFrom: draft -stateTo: published -weight: -9 diff --git a/core/modules/content_moderation/config/install/content_moderation.state_transition.published_archived.yml b/core/modules/content_moderation/config/install/content_moderation.state_transition.published_archived.yml deleted file mode 100644 index f3a866a..0000000 --- a/core/modules/content_moderation/config/install/content_moderation.state_transition.published_archived.yml +++ /dev/null @@ -1,11 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - content_moderation.state.archived - - content_moderation.state.published -id: published_archived -label: 'Archive' -stateFrom: published -stateTo: archived -weight: -6 diff --git a/core/modules/content_moderation/config/install/content_moderation.state_transition.published_draft.yml b/core/modules/content_moderation/config/install/content_moderation.state_transition.published_draft.yml deleted file mode 100644 index bd25a31..0000000 --- a/core/modules/content_moderation/config/install/content_moderation.state_transition.published_draft.yml +++ /dev/null @@ -1,11 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - content_moderation.state.draft - - content_moderation.state.published -id: published_draft -label: 'Create New Draft' -stateFrom: published -stateTo: draft -weight: -8 diff --git a/core/modules/content_moderation/config/install/content_moderation.state_transition.published_published.yml b/core/modules/content_moderation/config/install/content_moderation.state_transition.published_published.yml deleted file mode 100644 index 3c09a85..0000000 --- a/core/modules/content_moderation/config/install/content_moderation.state_transition.published_published.yml +++ /dev/null @@ -1,10 +0,0 @@ -langcode: en -status: true -dependencies: - config: - - content_moderation.state.published -id: published_published -label: 'Publish' -stateFrom: published -stateTo: published -weight: -7 diff --git a/core/modules/content_moderation/config/install/workflow.workflow.typical.yml b/core/modules/content_moderation/config/install/workflow.workflow.typical.yml new file mode 100644 index 0000000..eb6deb5 --- /dev/null +++ b/core/modules/content_moderation/config/install/workflow.workflow.typical.yml @@ -0,0 +1,33 @@ +langcode: en +status: true +dependencies: { } +id: typical +label: 'Typical workflow' +states: + archived: + label: Archived + published: false + default_revision: true + weight: 5 + draft: + label: Draft + published: false + default_revision: false + weight: -5 + published: + label: Published + published: true + default_revision: true + weight: 0 +transitions: + archived: + draft: 'Un-archive to Draft' + published: 'Un-archive' + draft: + draft: 'Create New Draft' + published: 'Publish' + published: + archived: 'Archive' + draft: 'Create New Draft' + published: 'Publish' +applies: { } diff --git a/core/modules/content_moderation/config/schema/content_moderation.schema.yml b/core/modules/content_moderation/config/schema/content_moderation.schema.yml index 7f9e8fd..0eaa863 100644 --- a/core/modules/content_moderation/config/schema/content_moderation.schema.yml +++ b/core/modules/content_moderation/config/schema/content_moderation.schema.yml @@ -1,75 +1,3 @@ -content_moderation.state.*: - type: config_entity - label: 'Moderation state config' - mapping: - id: - type: string - label: 'ID' - label: - type: label - label: 'Label' - published: - type: boolean - label: 'Is published' - default_revision: - type: boolean - label: 'Is default revision' - weight: - type: integer - label: 'Weight' - -content_moderation.state_transition.*: - type: config_entity - label: 'Moderation state transition config' - mapping: - id: - type: string - label: 'ID' - label: - type: label - label: 'Label' - stateFrom: - type: string - label: 'From state' - stateTo: - type: string - label: 'To state' - weight: - type: integer - label: 'Weight' - -node.type.*.third_party.content_moderation: - type: mapping - label: 'Enable moderation states for this node type' - mapping: - enabled: - type: boolean - label: 'Moderation states enabled' - allowed_moderation_states: - type: sequence - sequence: - type: string - label: 'Moderation state' - default_moderation_state: - type: string - label: 'Moderation state for new content' - -block_content.type.*.third_party.content_moderation: - type: mapping - label: 'Enable moderation states for this block content type' - mapping: - enabled: - type: boolean - label: 'Moderation states enabled' - allowed_moderation_states: - type: sequence - sequence: - type: string - label: 'Moderation state' - default_moderation_state: - type: string - label: 'Moderation state for new block content' - views.filter.latest_revision: type: views_filter label: 'Latest revision' diff --git a/core/modules/content_moderation/content_moderation.info.yml b/core/modules/content_moderation/content_moderation.info.yml index 6d92b64..ad20744 100644 --- a/core/modules/content_moderation/content_moderation.info.yml +++ b/core/modules/content_moderation/content_moderation.info.yml @@ -5,3 +5,5 @@ version: VERSION core: 8.x package: Core (Experimental) configure: content_moderation.overview +dependencies: + - workflow \ No newline at end of file diff --git a/core/modules/content_moderation/content_moderation.links.action.yml b/core/modules/content_moderation/content_moderation.links.action.yml deleted file mode 100644 index cbb2d3f..0000000 --- a/core/modules/content_moderation/content_moderation.links.action.yml +++ /dev/null @@ -1,11 +0,0 @@ -entity.moderation_state.add_form: - route_name: 'entity.moderation_state.add_form' - title: 'Add moderation state' - appears_on: - - entity.moderation_state.collection - -entity.moderation_state_transition.add_form: - route_name: 'entity.moderation_state_transition.add_form' - title: 'Add moderation state transition' - appears_on: - - entity.moderation_state_transition.collection diff --git a/core/modules/content_moderation/content_moderation.links.menu.yml b/core/modules/content_moderation/content_moderation.links.menu.yml deleted file mode 100644 index 0fcb3eb..0000000 --- a/core/modules/content_moderation/content_moderation.links.menu.yml +++ /dev/null @@ -1,21 +0,0 @@ -# Moderation state menu items definition -content_moderation.overview: - title: 'Content moderation' - route_name: content_moderation.overview - description: 'Configure states and transitions for entities.' - parent: system.admin_config_workflow - -entity.moderation_state.collection: - title: 'Moderation states' - route_name: entity.moderation_state.collection - description: 'Administer moderation states.' - parent: content_moderation.overview - weight: 10 - -# Moderation state transition menu items definition -entity.moderation_state_transition.collection: - title: 'Moderation state transitions' - route_name: entity.moderation_state_transition.collection - description: 'Administer moderation states transitions.' - parent: content_moderation.overview - weight: 20 diff --git a/core/modules/content_moderation/content_moderation.links.task.yml b/core/modules/content_moderation/content_moderation.links.task.yml index d715219..f92e92e 100644 --- a/core/modules/content_moderation/content_moderation.links.task.yml +++ b/core/modules/content_moderation/content_moderation.links.task.yml @@ -1,3 +1,3 @@ -moderation_state.entities: +content_moderation.workflows: deriver: 'Drupal\content_moderation\Plugin\Derivative\DynamicLocalTasks' weight: 100 diff --git a/core/modules/content_moderation/content_moderation.module b/core/modules/content_moderation/content_moderation.module index 582242b..b004375 100644 --- a/core/modules/content_moderation/content_moderation.module +++ b/core/modules/content_moderation/content_moderation.module @@ -21,6 +21,7 @@ use Drupal\node\NodeInterface; use Drupal\node\Plugin\Action\PublishNode; use Drupal\node\Plugin\Action\UnpublishNode; +use Drupal\workflow\Entity\Workflow; /** * Implements hook_help(). @@ -31,13 +32,9 @@ function content_moderation_help($route_name, RouteMatchInterface $route_match) case 'help.page.content_moderation': $output = ''; $output .= '

' . t('About') . '

'; - $output .= '

' . t('The Content Moderation module provides basic moderation for content. This lets site admins define states for content, and then define transitions between those states. For more information, see the online documentation for the Content Moderation module.', [':content_moderation' => 'https://www.drupal.org/documentation/modules/content_moderation']) . '

'; + $output .= '

' . t('The Content Moderation module provides moderation for content by applying workflows to content. For more information, see the online documentation for the Content Moderation module.', [':content_moderation' => 'https://www.drupal.org/documentation/modules/content_moderation']) . '

'; $output .= '

' . t('Uses') . '

'; $output .= '
'; - $output .= '
' . t('Moderation states') . '
'; - $output .= '
' . t('Moderation states provide the Draft and Archived states as additions to the basic Published option. You can click the blue Add Moderation state button and create new states.') . '
'; - $output .= '
' . t('Moderation state transitions') . '
'; - $output .= '
' . t('Using the "Moderation state transitions" screen, you can create the actual workflow. You decide the direction in which content moves from state to state, and which user roles are allowed to make that move.') . '
'; $output .= '
' . t('Configure Content Moderation permissions') . '
'; $output .= '
' . t('Each state is exposed as a permission. If a user has the permission for a transition, then they can move that node from the start state to the end state') . '

'; $output .= '
'; @@ -182,15 +179,17 @@ function content_moderation_node_access(NodeInterface $node, $operation, Account $access_result->addCacheableDependency($node); } - elseif ($operation === 'update' && $moderation_info->isModeratedEntity($node) && $node->moderation_state && $node->moderation_state->target_id) { + elseif ($operation === 'update' && $moderation_info->isModeratedEntity($node) && $node->moderation_state) { /** @var \Drupal\content_moderation\StateTransitionValidation $transition_validation */ $transition_validation = \Drupal::service('content_moderation.state_transition_validation'); - $valid_transition_targets = $transition_validation->getValidTransitionTargets($node, $account); + $valid_transition_targets = $transition_validation->getValidTransitions($node, $account); $access_result = $valid_transition_targets ? AccessResult::neutral() : AccessResult::forbidden(); $access_result->addCacheableDependency($node); $access_result->addCacheableDependency($account); + $workflow = \Drupal::service('content_moderation.moderation_information')->getWorkflowForEntity($node); + $access_result->addCacheableDependency($workflow); foreach ($valid_transition_targets as $valid_transition_target) { $access_result->addCacheableDependency($valid_transition_target); } @@ -222,3 +221,18 @@ function content_moderation_action_info_alter(&$definitions) { $definitions['node_unpublish_action']['class'] = ModerationOptOutUnpublishNode::class; } } + +/** + * Implements hook_entity_bundle_info_alter(). + */ +function content_moderation_entity_bundle_info_alter(&$bundles) { + foreach (Workflow::loadMultiple() as $workflow_id => $workflow) { + foreach ($workflow->getApplies() as $entity_type => $workflow_bundles) { + foreach ($workflow_bundles as $bundle => $default_state) { + if (isset($bundles[$entity_type][$bundle])) { + $bundles[$entity_type][$bundle]['workflow'] = $workflow_id; + } + } + } + } +} diff --git a/core/modules/content_moderation/content_moderation.permissions.yml b/core/modules/content_moderation/content_moderation.permissions.yml index 293a77d..af28bbf 100644 --- a/core/modules/content_moderation/content_moderation.permissions.yml +++ b/core/modules/content_moderation/content_moderation.permissions.yml @@ -2,18 +2,13 @@ view any unpublished content: title: 'View any unpublished content' description: 'This permission is necessary for any users that may moderate content.' -'view moderation states': - title: 'View moderation states' - description: 'View moderation states.' +'view content moderation': + title: 'View content moderation' + description: 'View content moderation.' -'administer moderation states': - title: 'Administer moderation states' - description: 'Create and edit moderation states.' - 'restrict access': TRUE - -'administer moderation state transitions': - title: 'Administer content moderation state transitions' - description: 'Create and edit content moderation state transitions.' +'administer content moderation': + title: 'Administer content moderation' + description: 'Administer workflows on content entities.' 'restrict access': TRUE view latest version: diff --git a/core/modules/content_moderation/content_moderation.routing.yml b/core/modules/content_moderation/content_moderation.routing.yml deleted file mode 100644 index 912eed8..0000000 --- a/core/modules/content_moderation/content_moderation.routing.yml +++ /dev/null @@ -1,7 +0,0 @@ -content_moderation.overview: - path: '/admin/config/workflow/moderation' - defaults: - _controller: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage' - _title: 'Content moderation' - requirements: - _permission: 'access administration pages' diff --git a/core/modules/content_moderation/content_moderation.services.yml b/core/modules/content_moderation/content_moderation.services.yml index 75f0c64..904bc0d 100644 --- a/core/modules/content_moderation/content_moderation.services.yml +++ b/core/modules/content_moderation/content_moderation.services.yml @@ -6,10 +6,10 @@ services: - { name: paramconverter, priority: 5 } content_moderation.state_transition_validation: class: \Drupal\content_moderation\StateTransitionValidation - arguments: ['@entity_type.manager', '@entity.query'] + arguments: ['@content_moderation.moderation_information'] content_moderation.moderation_information: class: Drupal\content_moderation\ModerationInformation - arguments: ['@entity_type.manager'] + arguments: ['@entity_type.manager', '@entity_type.bundle.info'] access_check.latest_revision: class: Drupal\content_moderation\Access\LatestRevisionCheck arguments: ['@content_moderation.moderation_information'] diff --git a/core/modules/content_moderation/src/Entity/ContentModerationState.php b/core/modules/content_moderation/src/Entity/ContentModerationState.php index 978408f..9fc766a 100644 --- a/core/modules/content_moderation/src/Entity/ContentModerationState.php +++ b/core/modules/content_moderation/src/Entity/ContentModerationState.php @@ -55,10 +55,9 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setTranslatable(TRUE) ->setRevisionable(TRUE); - $fields['moderation_state'] = BaseFieldDefinition::create('entity_reference') + $fields['moderation_state'] = BaseFieldDefinition::create('string') ->setLabel(t('Moderation state')) ->setDescription(t('The moderation state of the referenced content.')) - ->setSetting('target_type', 'moderation_state') ->setRequired(TRUE) ->setTranslatable(TRUE) ->setRevisionable(TRUE) @@ -155,7 +154,7 @@ public function save() { if ($related_entity instanceof TranslatableInterface) { $related_entity = $related_entity->getTranslation($this->activeLangcode); } - $related_entity->moderation_state->target_id = $this->moderation_state->target_id; + $related_entity->moderation_state = $this->moderation_state; return $related_entity->save(); } diff --git a/core/modules/content_moderation/src/Entity/Handler/NodeModerationHandler.php b/core/modules/content_moderation/src/Entity/Handler/NodeModerationHandler.php index 83de187..247d352 100644 --- a/core/modules/content_moderation/src/Entity/Handler/NodeModerationHandler.php +++ b/core/modules/content_moderation/src/Entity/Handler/NodeModerationHandler.php @@ -2,8 +2,11 @@ namespace Drupal\content_moderation\Entity\Handler; +use Drupal\content_moderation\ModerationInformationInterface; use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Form\FormStateInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Customizations for node entities. @@ -11,6 +14,32 @@ class NodeModerationHandler extends ModerationHandler { /** + * The moderation information service. + * + * @var \Drupal\content_moderation\ModerationInformationInterface + */ + protected $moderationInfo; + + /** + * NodeModerationHandler constructor. + * + * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info + * The moderation information service. + */ + public function __construct(ModerationInformationInterface $moderation_info) { + $this->moderationInfo = $moderation_info; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $container->get('content_moderation.moderation_information') + ); + } + + /** * {@inheritdoc} */ public function onPresave(ContentEntityInterface $entity, $default_revision, $published_state) { @@ -38,7 +67,7 @@ public function enforceRevisionsBundleFormAlter(array &$form, FormStateInterface /* @var \Drupal\node\Entity\NodeType $entity */ $entity = $form_state->getFormObject()->getEntity(); - if ($entity->getThirdPartySetting('content_moderation', 'enabled', FALSE)) { + if ($this->moderationInfo->getWorkFlowForEntity($entity)) { // Force the revision checkbox on. $form['workflow']['options']['#default_value']['revision'] = 'revision'; $form['workflow']['options']['revision']['#disabled'] = TRUE; diff --git a/core/modules/content_moderation/src/Entity/ModerationState.php b/core/modules/content_moderation/src/Entity/ModerationState.php deleted file mode 100644 index 379ff60..0000000 --- a/core/modules/content_moderation/src/Entity/ModerationState.php +++ /dev/null @@ -1,102 +0,0 @@ -published; - } - - /** - * {@inheritdoc} - */ - public function isDefaultRevisionState() { - return $this->published || $this->default_revision; - } - -} diff --git a/core/modules/content_moderation/src/Entity/ModerationStateTransition.php b/core/modules/content_moderation/src/Entity/ModerationStateTransition.php deleted file mode 100644 index 95a115b..0000000 --- a/core/modules/content_moderation/src/Entity/ModerationStateTransition.php +++ /dev/null @@ -1,114 +0,0 @@ -stateFrom) { - $this->addDependency('config', ModerationState::load($this->stateFrom)->getConfigDependencyName()); - } - if ($this->stateTo) { - $this->addDependency('config', ModerationState::load($this->stateTo)->getConfigDependencyName()); - } - return $this; - } - - /** - * {@inheritdoc} - */ - public function getFromState() { - return $this->stateFrom; - } - - /** - * {@inheritdoc} - */ - public function getToState() { - return $this->stateTo; - } - - /** - * {@inheritdoc} - */ - public function getWeight() { - return $this->weight; - } - -} diff --git a/core/modules/content_moderation/src/EntityOperations.php b/core/modules/content_moderation/src/EntityOperations.php index 51ad7d6..907ebe1 100644 --- a/core/modules/content_moderation/src/EntityOperations.php +++ b/core/modules/content_moderation/src/EntityOperations.php @@ -6,10 +6,12 @@ use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\TypedData\TranslatableInterface; use Drupal\content_moderation\Form\EntityModerationForm; +use Drupal\workflow\WorkflowInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -46,6 +48,11 @@ class EntityOperations implements ContainerInjectionInterface { protected $tracker; /** + * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface + */ + protected $bundleInfo; + + /** * Constructs a new EntityOperations object. * * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info @@ -57,11 +64,12 @@ class EntityOperations implements ContainerInjectionInterface { * @param \Drupal\content_moderation\RevisionTrackerInterface $tracker * The revision tracker. */ - public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, RevisionTrackerInterface $tracker) { + public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, RevisionTrackerInterface $tracker, EntityTypeBundleInfoInterface $bundle_indo) { $this->moderationInfo = $moderation_info; $this->entityTypeManager = $entity_type_manager; $this->formBuilder = $form_builder; $this->tracker = $tracker; + $this->bundleInfo = $bundle_indo; } /** @@ -72,7 +80,8 @@ public static function create(ContainerInterface $container) { $container->get('content_moderation.moderation_information'), $container->get('entity_type.manager'), $container->get('form_builder'), - $container->get('content_moderation.revision_tracker') + $container->get('content_moderation.revision_tracker'), + $container->get('entity_type.bundle.info') ); } @@ -86,17 +95,16 @@ public function entityPresave(EntityInterface $entity) { if (!$this->moderationInfo->isModeratedEntity($entity)) { return; } - if ($entity->moderation_state->target_id) { - $moderation_state = $this->entityTypeManager - ->getStorage('moderation_state') - ->load($entity->moderation_state->target_id); - $published_state = $moderation_state->isPublishedState(); + + if ($entity->moderation_state->value) { + $workflow = $this->moderationInfo->getWorkFlowForEntity($entity); + $published_state = $workflow->isPublishedState($entity->moderation_state->value); // This entity is default if it is new, the default revision, or the // default revision is not published. $update_default_revision = $entity->isNew() - || $moderation_state->isDefaultRevisionState() - || !$this->isDefaultRevisionPublished($entity); + || $workflow->isDefaultRevisionState($entity->moderation_state->value) + || !$this->isDefaultRevisionPublished($entity, $workflow); // Fire per-entity-type logic for handling the save process. $this->entityTypeManager->getHandler($entity->getEntityTypeId(), 'moderation')->onPresave($entity, $update_default_revision, $published_state); @@ -140,15 +148,13 @@ public function entityUpdate(EntityInterface $entity) { * The entity to update or create a moderation state for. */ protected function updateOrCreateFromEntity(EntityInterface $entity) { - $moderation_state = $entity->moderation_state->target_id; + $moderation_state = $entity->moderation_state->value; /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ if (!$moderation_state) { - $moderation_state = $this->entityTypeManager - ->getStorage($entity->getEntityType()->getBundleEntityType())->load($entity->bundle()) - ->getThirdPartySetting('content_moderation', 'default_moderation_state'); + $moderation_state = $this->moderationInfo->getWorkFlowForEntity($entity)->getInitialState(); } - // @todo what if $entity->moderation_state->target_id is null at this point? + // @todo what if $entity->moderation_state is null at this point? $entity_type_id = $entity->getEntityTypeId(); $entity_id = $entity->id(); $entity_revision_id = $entity->getRevisionId(); @@ -239,11 +245,13 @@ public function entityView(array &$build, EntityInterface $entity, EntityViewDis * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity being saved. + * @param \Drupal\workflow\WorkflowInterface $workflow + * The workflow being applied to the entity. * * @return bool * TRUE if the default revision is published. FALSE otherwise. */ - protected function isDefaultRevisionPublished(EntityInterface $entity) { + protected function isDefaultRevisionPublished(EntityInterface $entity, WorkflowInterface $workflow) { $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId()); $default_revision = $storage->load($entity->id()); @@ -258,7 +266,7 @@ protected function isDefaultRevisionPublished(EntityInterface $entity) { $default_revision = $default_revision->getTranslation($entity->language()->getId()); } - return $default_revision && $default_revision->moderation_state->entity->isPublishedState(); + return $default_revision && $workflow->isPublishedState($default_revision->moderation_state->value); } } diff --git a/core/modules/content_moderation/src/EntityTypeInfo.php b/core/modules/content_moderation/src/EntityTypeInfo.php index ab7e918..6d6855f 100644 --- a/core/modules/content_moderation/src/EntityTypeInfo.php +++ b/core/modules/content_moderation/src/EntityTypeInfo.php @@ -9,6 +9,7 @@ use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\ContentEntityTypeInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Field\BaseFieldDefinition; @@ -50,6 +51,13 @@ class EntityTypeInfo implements ContainerInjectionInterface { protected $entityTypeManager; /** + * The bundle information service. + * + * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface + */ + protected $bundleInfo; + + /** * The current user. * * @var \Drupal\Core\Session\AccountInterface @@ -77,11 +85,16 @@ class EntityTypeInfo implements ContainerInjectionInterface { * The moderation information service. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * Entity type manager. + * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info + * Bundle information service. + * @param \Drupal\Core\Session\AccountInterface $current_user + * Current user. */ - public function __construct(TranslationInterface $translation, ModerationInformationInterface $moderation_information, EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user) { + public function __construct(TranslationInterface $translation, ModerationInformationInterface $moderation_information, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_info, AccountInterface $current_user) { $this->stringTranslation = $translation; $this->moderationInfo = $moderation_information; $this->entityTypeManager = $entity_type_manager; + $this->bundleInfo = $bundle_info; $this->currentUser = $current_user; } @@ -93,6 +106,7 @@ public static function create(ContainerInterface $container) { $container->get('string_translation'), $container->get('content_moderation.moderation_information'), $container->get('entity_type.manager'), + $container->get('entity_type.bundle.info'), $container->get('current_user') ); } @@ -196,7 +210,7 @@ public function entityOperation(EntityInterface $entity) { $operations = []; $type = $entity->getEntityType(); $bundle_of = $type->getBundleOf(); - if ($this->currentUser->hasPermission('administer moderation states') && $bundle_of && + if ($this->currentUser->hasPermission('administer content moderation') && $bundle_of && $this->moderationInfo->canModerateEntitiesOfEntityType($this->entityTypeManager->getDefinition($bundle_of)) ) { $operations['manage-moderation'] = [ @@ -262,16 +276,12 @@ public function entityExtraFieldInfo() { * - bundle: The machine name of a bundle, such as "page" or "article". */ protected function getModeratedBundles() { - /** @var ConfigEntityTypeInterface $type */ - foreach ($this->filterNonRevisionableEntityTypes($this->entityTypeManager->getDefinitions()) as $type_name => $type) { - $result = $this->entityTypeManager - ->getStorage($type_name) - ->getQuery() - ->condition('third_party_settings.content_moderation.enabled', TRUE) - ->execute(); - - foreach ($result as $bundle_name) { - yield ['entity' => $type->getBundleOf(), 'bundle' => $bundle_name]; + $entity_types = array_filter($this->entityTypeManager->getDefinitions(), [$this->moderationInfo, 'canModerateEntitiesOfEntityType']); + foreach ($entity_types as $type_name => $type) { + foreach ($this->bundleInfo->getBundleInfo($type_name) as $bundle_id => $bundle) { + if ($this->moderationInfo->shouldModerateEntitiesOfBundle($type, $bundle_id)) { + yield ['entity' => $type_name, 'bundle' => $bundle_id]; + } } } } @@ -291,7 +301,7 @@ public function entityBaseFieldInfo(EntityTypeInterface $entity_type) { } $fields = []; - $fields['moderation_state'] = BaseFieldDefinition::create('entity_reference') + $fields['moderation_state'] = BaseFieldDefinition::create('string') ->setLabel(t('Moderation state')) ->setDescription(t('The moderation state of this piece of content.')) ->setComputed(TRUE) diff --git a/core/modules/content_moderation/src/Form/BundleModerationConfigurationForm.php b/core/modules/content_moderation/src/Form/BundleModerationConfigurationForm.php index 58dd334..0951334 100644 --- a/core/modules/content_moderation/src/Form/BundleModerationConfigurationForm.php +++ b/core/modules/content_moderation/src/Form/BundleModerationConfigurationForm.php @@ -2,12 +2,10 @@ namespace Drupal\content_moderation\Form; -use Drupal\Core\Config\Entity\ThirdPartySettingsInterface; +use Drupal\workflow\WorkflowInterface; use Drupal\Core\Entity\EntityForm; -use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; -use Drupal\content_moderation\Entity\ModerationState; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -50,146 +48,96 @@ public function getBaseFormId() { * {@inheritdoc} */ public function form(array $form, FormStateInterface $form_state) { - /* @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $bundle */ - $bundle = $form_state->getFormObject()->getEntity(); - $form['enable_moderation_state'] = [ - '#type' => 'checkbox', - '#title' => $this->t('Enable moderation states.'), - '#description' => $this->t('Content of this type must transition through moderation states in order to be published.'), - '#default_value' => $bundle->getThirdPartySetting('content_moderation', 'enabled', FALSE), - ]; - - // Add a special message when moderation is being disabled. - if ($bundle->getThirdPartySetting('content_moderation', 'enabled', FALSE)) { - $form['enable_moderation_state_note'] = [ - '#type' => 'item', - '#description' => $this->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 = $this->entityTypeManager->getStorage('moderation_state')->loadMultiple(); - $label = function(ModerationState $state) { - return $state->label(); - }; - - $options_published = array_map($label, array_filter($states, function(ModerationState $state) { - return $state->isPublishedState(); - })); - - $options_unpublished = array_map($label, array_filter($states, function(ModerationState $state) { - return !$state->isPublishedState(); + /* @var \Drupal\Core\Config\Entity\ConfigEntityInterface $bundle */ + $bundle = $this->getEntity(); + $bundle_of_entity_type = $this->entityTypeManager->getDefinition($bundle->getEntityType()->getBundleOf()); + /* @var \Drupal\workflow\WorkflowInterface[] $workflows */ + $workflows = $this->entityTypeManager->getStorage('workflow')->loadMultiple(); + + $options = array_map(function (WorkflowInterface $workflow) { + return $workflow->label(); + }, array_filter($workflows, function (WorkflowInterface $workflow) { + return $workflow->status(); })); - $form['allowed_moderation_states_unpublished'] = [ - '#type' => 'checkboxes', - '#title' => $this->t('Allowed moderation states (Unpublished)'), - '#description' => $this->t('The allowed unpublished moderation states this content-type can be assigned.'), - '#default_value' => $bundle->getThirdPartySetting('content_moderation', 'allowed_moderation_states', array_keys($options_unpublished)), - '#options' => $options_unpublished, - '#required' => TRUE, - '#states' => [ - 'visible' => [ - ':input[name=enable_moderation_state]' => ['checked' => TRUE], - ], - ], + $selected_workflow = array_reduce($workflows, function ($carry, WorkflowInterface $workflow) use ($bundle_of_entity_type, $bundle) { + if ($workflow->appliesToEntityTypeAndBundle($bundle_of_entity_type->id(), $bundle->id())) { + return $workflow->id(); + } + return $carry; + }); + $form['workflow'] = [ + '#type' => 'select', + '#title' => $this->t('Select the workflow to apply'), + '#default_value' => $selected_workflow, + '#options' => $options, + '#required' => FALSE, + '#empty_value' => '', ]; - $form['allowed_moderation_states_published'] = [ - '#type' => 'checkboxes', - '#title' => $this->t('Allowed moderation states (Published)'), - '#description' => $this->t('The allowed published moderation states this content-type can be assigned.'), - '#default_value' => $bundle->getThirdPartySetting('content_moderation', 'allowed_moderation_states', array_keys($options_published)), - '#options' => $options_published, - '#required' => TRUE, - '#states' => [ - 'visible' => [ - ':input[name=enable_moderation_state]' => ['checked' => TRUE], - ], - ], + $form['bundle'] = [ + '#type' => 'hidden', + '#value' => $bundle->id(), ]; - // The key of the array needs to be a user-facing string so we have to fully - // render the translatable string to a real string, or else PHP errors on an - // object used as an array key. - $options = [ - $this->t('Unpublished')->render() => $options_unpublished, - $this->t('Published')->render() => $options_published, + $form['entity_type'] = [ + '#type' => 'hidden', + '#value' => $bundle_of_entity_type->id(), ]; - $form['default_moderation_state'] = [ - '#type' => 'select', - '#title' => $this->t('Default moderation state'), - '#options' => $options, - '#description' => $this->t('Select the moderation state for new content'), - '#default_value' => $bundle->getThirdPartySetting('content_moderation', 'default_moderation_state', 'draft'), - '#states' => [ - 'visible' => [ - ':input[name=enable_moderation_state]' => ['checked' => TRUE], - ], - ], - ]; - $form['#entity_builders'][] = [$this, 'formBuilderCallback']; + // Add a special message when moderation is being disabled. + if ($selected_workflow) { + $form['enable_moderation_state_note'] = [ + '#type' => 'item', + '#description' => $this->t('After disabling moderation, any existing forward drafts will be accessible via the "Revisions" tab.'), + // @todo make this work with new select + // '#states' => [ + // 'visible' => [ + // ':input[name=enable_moderation_state]' => ['checked' => FALSE], + // ], + // ], + ]; + } return parent::form($form, $form_state); } /** - * Form builder callback. - * - * @todo This should be folded into the form method. - * - * @param string $entity_type_id - * The entity type identifier. - * @param \Drupal\Core\Entity\EntityInterface $bundle - * The bundle entity updated with the submitted values. - * @param array $form - * The complete form array. - * @param \Drupal\Core\Form\FormStateInterface $form_state - * The current state of the form. - */ - public function formBuilderCallback($entity_type_id, EntityInterface $bundle, &$form, FormStateInterface $form_state) { - // @todo https://www.drupal.org/node/2779933 write a test for this. - if ($bundle instanceof ThirdPartySettingsInterface) { - $bundle->setThirdPartySetting('content_moderation', 'enabled', $form_state->getValue('enable_moderation_state')); - $bundle->setThirdPartySetting('content_moderation', 'allowed_moderation_states', array_keys(array_filter($form_state->getValue('allowed_moderation_states_published') + $form_state->getValue('allowed_moderation_states_unpublished')))); - $bundle->setThirdPartySetting('content_moderation', 'default_moderation_state', $form_state->getValue('default_moderation_state')); - } - } - - /** * {@inheritdoc} */ - public function validateForm(array &$form, FormStateInterface $form_state) { - if ($form_state->getValue('enable_moderation_state')) { - $allowed = array_keys(array_filter($form_state->getValue('allowed_moderation_states_published') + $form_state->getValue('allowed_moderation_states_unpublished'))); - - if (($default = $form_state->getValue('default_moderation_state')) && !in_array($default, $allowed, TRUE)) { - $form_state->setErrorByName('default_moderation_state', $this->t('The default moderation state must be one of the allowed states.')); - } - } + 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. + drupal_set_message($this->t('Your settings have been saved.')); } /** * {@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 \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $bundle */ - $bundle = $form_state->getFormObject()->getEntity(); - - $this->entityTypeManager->getHandler($bundle->getEntityType()->getBundleOf(), 'moderation')->onBundleModerationConfigurationFormSubmit($bundle); + public function save(array $form, FormStateInterface $form_state) { + if ($workflow = $form_state->getValue('workflow')) { + /* @var \Drupal\workflow\WorkflowInterface $workflow */ + $workflow = $this->entityTypeManager->getStorage('workflow')->load($workflow); + $workflow + ->applyToEntityTypeAndBundle($form_state->getValue('entity_type'), $form_state->getValue('bundle')) + ->save(); } - - parent::submitForm($form, $form_state); - - drupal_set_message($this->t('Your settings have been saved.')); + else { + /* @var \Drupal\workflow\WorkflowInterface[] $workflows */ + $workflows = $this->entityTypeManager->getStorage('workflow')->loadMultiple(); + foreach ($workflows as $workflow) { + if ($workflow->appliesToEntityTypeAndBundle($form_state->getValue('entity_type'), $form_state->getValue('bundle'))) { + $workflow + ->removeEntityTypeAndBundle($form_state->getValue('entity_type'), $form_state->getValue('bundle')) + ->save(); + } + } + } + // Clear bundle cache so workflow gets added or removed from the bundle + // information. + \Drupal::service('entity_type.bundle.info')->clearCachedBundles(); + // Clear field cache so extra field is added or removed. + \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); } } diff --git a/core/modules/content_moderation/src/Form/EntityModerationForm.php b/core/modules/content_moderation/src/Form/EntityModerationForm.php index 39baec0..4f79aa9 100644 --- a/core/modules/content_moderation/src/Form/EntityModerationForm.php +++ b/core/modules/content_moderation/src/Form/EntityModerationForm.php @@ -3,10 +3,8 @@ namespace Drupal\content_moderation\Form; use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; -use Drupal\content_moderation\Entity\ModerationStateTransition; use Drupal\content_moderation\ModerationInformationInterface; use Drupal\content_moderation\StateTransitionValidation; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -31,26 +29,16 @@ class EntityModerationForm extends FormBase { protected $validation; /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - - /** * EntityModerationForm constructor. * * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info * The moderation information service. * @param \Drupal\content_moderation\StateTransitionValidation $validation * The moderation state transition validation service. - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. */ - public function __construct(ModerationInformationInterface $moderation_info, StateTransitionValidation $validation, EntityTypeManagerInterface $entity_type_manager) { + public function __construct(ModerationInformationInterface $moderation_info, StateTransitionValidation $validation) { $this->moderationInfo = $moderation_info; $this->validation = $validation; - $this->entityTypeManager = $entity_type_manager; } /** @@ -59,8 +47,7 @@ public function __construct(ModerationInformationInterface $moderation_info, Sta public static function create(ContainerInterface $container) { return new static( $container->get('content_moderation.moderation_information'), - $container->get('content_moderation.state_transition_validation'), - $container->get('entity_type.manager') + $container->get('content_moderation.state_transition_validation') ); } @@ -75,20 +62,20 @@ public function getFormId() { * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state, ContentEntityInterface $entity = NULL) { - /** @var \Drupal\content_moderation\Entity\ModerationState $current_state */ - $current_state = $entity->moderation_state->entity; + $current_state = $entity->moderation_state->value; + $workflow = $this->moderationInfo->getWorkFlowForEntity($entity); $transitions = $this->validation->getValidTransitions($entity, $this->currentUser()); // Exclude self-transitions. - $transitions = array_filter($transitions, function(ModerationStateTransition $transition) use ($current_state) { - return $transition->getToState() != $current_state->id(); + $transitions = array_filter($transitions, function($transition) use ($current_state) { + return $transition != $current_state; }); $target_states = []; - /** @var ModerationStateTransition $transition */ + foreach ($transitions as $transition) { - $target_states[$transition->getToState()] = $transition->label(); + $target_states[$transition] = $workflow->getStateLabel($transition); } if (!count($target_states)) { @@ -99,7 +86,7 @@ public function buildForm(array $form, FormStateInterface $form_state, ContentEn $form['current'] = [ '#type' => 'item', '#title' => $this->t('Status'), - '#markup' => $current_state->label(), + '#markup' => $workflow->getStateLabel($current_state), ]; } @@ -139,21 +126,19 @@ public function submitForm(array &$form, FormStateInterface $form_state) { // @todo should we just just be updating the content moderation state // entity? That would prevent setting the revision log. - $entity->moderation_state->target_id = $new_state; + $entity->set('moderation_state', $new_state); $entity->revision_log = $form_state->getValue('revision_log'); $entity->save(); drupal_set_message($this->t('The moderation state has been updated.')); - /** @var \Drupal\content_moderation\Entity\ModerationState $state */ - $state = $this->entityTypeManager->getStorage('moderation_state')->load($new_state); - + $workflow = $this->moderationInfo->getWorkFlowForEntity($entity); // 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()) { + if ($workflow->isDefaultRevisionState($new_state)) { $form_state->setRedirectUrl($entity->toUrl('canonical')); } } diff --git a/core/modules/content_moderation/src/Form/ModerationStateForm.php b/core/modules/content_moderation/src/Form/ModerationStateForm.php deleted file mode 100644 index 32d7a48..0000000 --- a/core/modules/content_moderation/src/Form/ModerationStateForm.php +++ /dev/null @@ -1,82 +0,0 @@ -entity; - $form['label'] = array( - '#type' => 'textfield', - '#title' => $this->t('Label'), - '#maxlength' => 255, - '#default_value' => $moderation_state->label(), - '#description' => $this->t('Label for the Moderation state.'), - '#required' => TRUE, - ); - - $form['id'] = array( - '#type' => 'machine_name', - '#default_value' => $moderation_state->id(), - '#machine_name' => array( - 'exists' => [ModerationState::class, 'load'], - ), - '#disabled' => !$moderation_state->isNew(), - ); - - $form['published'] = [ - '#type' => 'checkbox', - '#title' => $this->t('Published'), - '#description' => $this->t('When content reaches this state it should be published.'), - '#default_value' => $moderation_state->isPublishedState(), - ]; - - $form['default_revision'] = [ - '#type' => 'checkbox', - '#title' => $this->t('Default revision'), - '#description' => $this->t('When content reaches this state it should be made the default revision; this is implied for published states.'), - '#default_value' => $moderation_state->isDefaultRevisionState(), - // @todo Add form #state to force "make default" on when "published" is - // on for a state. - // @see https://www.drupal.org/node/2645614 - ]; - - return $form; - } - - /** - * {@inheritdoc} - */ - public function save(array $form, FormStateInterface $form_state) { - $moderation_state = $this->entity; - $status = $moderation_state->save(); - - switch ($status) { - case SAVED_NEW: - drupal_set_message($this->t('Created the %label Moderation state.', [ - '%label' => $moderation_state->label(), - ])); - break; - - default: - drupal_set_message($this->t('Saved the %label Moderation state.', [ - '%label' => $moderation_state->label(), - ])); - } - $form_state->setRedirectUrl($moderation_state->toUrl('collection')); - } - -} diff --git a/core/modules/content_moderation/src/Form/ModerationStateTransitionDeleteForm.php b/core/modules/content_moderation/src/Form/ModerationStateTransitionDeleteForm.php deleted file mode 100644 index f153f1f..0000000 --- a/core/modules/content_moderation/src/Form/ModerationStateTransitionDeleteForm.php +++ /dev/null @@ -1,49 +0,0 @@ -t('Are you sure you want to delete %name?', array('%name' => $this->entity->label())); - } - - /** - * {@inheritdoc} - */ - public function getCancelUrl() { - return new Url('entity.moderation_state_transition.collection'); - } - - /** - * {@inheritdoc} - */ - public function getConfirmText() { - return $this->t('Delete'); - } - - /** - * {@inheritdoc} - */ - public function submitForm(array &$form, FormStateInterface $form_state) { - $this->entity->delete(); - - drupal_set_message($this->t( - 'Moderation transition %label deleted.', - ['%label' => $this->entity->label()] - )); - - $form_state->setRedirectUrl($this->getCancelUrl()); - } - -} diff --git a/core/modules/content_moderation/src/Form/ModerationStateTransitionForm.php b/core/modules/content_moderation/src/Form/ModerationStateTransitionForm.php deleted file mode 100644 index 8322c18..0000000 --- a/core/modules/content_moderation/src/Form/ModerationStateTransitionForm.php +++ /dev/null @@ -1,151 +0,0 @@ -entityTypeManager = $entity_type_manager; - $this->queryFactory = $query_factory; - } - - /** - * {@inheritdoc} - */ - public static function create(ContainerInterface $container) { - return new static($container->get('entity_type.manager'), $container->get('entity.query')); - } - - /** - * {@inheritdoc} - */ - public function form(array $form, FormStateInterface $form_state) { - $form = parent::form($form, $form_state); - - /* @var \Drupal\content_moderation\ModerationStateTransitionInterface $moderation_state_transition */ - $moderation_state_transition = $this->entity; - $form['label'] = [ - '#type' => 'textfield', - '#title' => $this->t('Label'), - '#maxlength' => 255, - '#default_value' => $moderation_state_transition->label(), - '#description' => $this->t('Label for the Moderation state transition.'), - '#required' => TRUE, - ]; - - $form['id'] = [ - '#type' => 'machine_name', - '#default_value' => $moderation_state_transition->id(), - '#machine_name' => [ - 'exists' => '\Drupal\content_moderation\Entity\ModerationStateTransition::load', - ], - '#disabled' => !$moderation_state_transition->isNew(), - ]; - - $options = []; - foreach ($this->entityTypeManager->getStorage('moderation_state') - ->loadMultiple() as $moderation_state) { - $options[$moderation_state->id()] = $moderation_state->label(); - } - - $form['container'] = [ - '#type' => 'container', - '#attributes' => [ - 'class' => ['container-inline'], - ], - ]; - - $form['container']['stateFrom'] = [ - '#type' => 'select', - '#title' => $this->t('Transition from'), - '#options' => $options, - '#required' => TRUE, - '#empty_option' => $this->t('-- Select --'), - '#default_value' => $moderation_state_transition->getFromState(), - ]; - - $form['container']['stateTo'] = [ - '#type' => 'select', - '#options' => $options, - '#required' => TRUE, - '#title' => $this->t('Transition to'), - '#empty_option' => $this->t('-- Select --'), - '#default_value' => $moderation_state_transition->getToState(), - ]; - - // Make sure there's always at least a wide enough delta on weight to cover - // the current value or the total number of transitions. That way we - // never end up forcing a transition to change its weight needlessly. - $num_transitions = $this->queryFactory->get('moderation_state_transition') - ->count() - ->execute(); - $delta = max(abs($moderation_state_transition->getWeight()), $num_transitions); - - $form['weight'] = [ - '#type' => 'weight', - '#delta' => $delta, - '#options' => $options, - '#title' => $this->t('Weight'), - '#default_value' => $moderation_state_transition->getWeight(), - '#description' => $this->t('Orders the transitions in moderation forms and the administrative listing. Heavier items will sink and the lighter items will be positioned nearer the top.'), - ]; - - return $form; - } - - /** - * {@inheritdoc} - */ - public function save(array $form, FormStateInterface $form_state) { - $moderation_state_transition = $this->entity; - $status = $moderation_state_transition->save(); - - switch ($status) { - case SAVED_NEW: - drupal_set_message($this->t('Created the %label Moderation state transition.', [ - '%label' => $moderation_state_transition->label(), - ])); - break; - - default: - drupal_set_message($this->t('Saved the %label Moderation state transition.', [ - '%label' => $moderation_state_transition->label(), - ])); - } - $form_state->setRedirectUrl($moderation_state_transition->toUrl('collection')); - } - -} diff --git a/core/modules/content_moderation/src/ModerationInformation.php b/core/modules/content_moderation/src/ModerationInformation.php index ed33902..eac4ab7 100644 --- a/core/modules/content_moderation/src/ModerationInformation.php +++ b/core/modules/content_moderation/src/ModerationInformation.php @@ -4,6 +4,7 @@ use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; @@ -20,15 +21,23 @@ class ModerationInformation implements ModerationInformationInterface { protected $entityTypeManager; /** + * The bundle information service. + * + * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface + */ + protected $bundleInfo; + + /** * Creates a new ModerationInformation instance. * * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. - * @param \Drupal\Core\Session\AccountInterface $current_user - * The current user. + * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info + * The bundle information service. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_info) { $this->entityTypeManager = $entity_type_manager; + $this->bundleInfo = $bundle_info; } /** @@ -54,10 +63,8 @@ public function canModerateEntitiesOfEntityType(EntityTypeInterface $entity_type */ public function shouldModerateEntitiesOfBundle(EntityTypeInterface $entity_type, $bundle) { if ($this->canModerateEntitiesOfEntityType($entity_type)) { - $bundle_entity = $this->entityTypeManager->getStorage($entity_type->getBundleEntityType())->load($bundle); - if ($bundle_entity) { - return $bundle_entity->getThirdPartySetting('content_moderation', 'enabled', FALSE); - } + $bundles = $this->bundleInfo->getBundleInfo($entity_type->id()); + return isset($bundles[$bundle]['workflow']); } return FALSE; } @@ -123,10 +130,26 @@ public function hasForwardRevision(ContentEntityInterface $entity) { * {@inheritdoc} */ public function isLiveRevision(ContentEntityInterface $entity) { + $workflow = $this->getWorkFlowForEntity($entity); return $this->isLatestRevision($entity) && $entity->isDefaultRevision() - && $entity->moderation_state->entity - && $entity->moderation_state->entity->isPublishedState(); + && $entity->moderation_state->value + && $workflow->isPublishedState($entity->moderation_state->value); + } + + /** + * @param $entity_type + * @param $bundle + * + * @return \Drupal\workflow\WorkflowInterface + * The workflow entity. + */ + public function getWorkFlowForEntity(ContentEntityInterface $entity) { + $bundles = $this->bundleInfo->getBundleInfo($entity->getEntityTypeId()); + if (isset($bundles[$entity->bundle()]['workflow'])) { + return $this->entityTypeManager->getStorage('workflow')->load($bundles[$entity->bundle()]['workflow']); + }; + return NULL; } } diff --git a/core/modules/content_moderation/src/ModerationInformationInterface.php b/core/modules/content_moderation/src/ModerationInformationInterface.php index 95a658e..eac7999 100644 --- a/core/modules/content_moderation/src/ModerationInformationInterface.php +++ b/core/modules/content_moderation/src/ModerationInformationInterface.php @@ -126,4 +126,13 @@ public function hasForwardRevision(ContentEntityInterface $entity); */ public function isLiveRevision(ContentEntityInterface $entity); + /** + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The content entity to get the workflow for. + * + * @return \Drupal\workflow\WorkflowInterface|null + * The workflow entity. + */ + public function getWorkFlowForEntity(ContentEntityInterface $entity); + } diff --git a/core/modules/content_moderation/src/ModerationStateInterface.php b/core/modules/content_moderation/src/ModerationStateInterface.php deleted file mode 100644 index 99f664f..0000000 --- a/core/modules/content_moderation/src/ModerationStateInterface.php +++ /dev/null @@ -1,28 +0,0 @@ -t('Moderation state'); - $header['id'] = $this->t('Machine name'); - - return $header + parent::buildHeader(); - } - - /** - * {@inheritdoc} - */ - public function buildRow(EntityInterface $entity) { - $row['label'] = $entity->label(); - $row['id']['#markup'] = $entity->id(); - - return $row + parent::buildRow($entity); - } - -} diff --git a/core/modules/content_moderation/src/ModerationStateTransitionInterface.php b/core/modules/content_moderation/src/ModerationStateTransitionInterface.php deleted file mode 100644 index 91b5b13..0000000 --- a/core/modules/content_moderation/src/ModerationStateTransitionInterface.php +++ /dev/null @@ -1,36 +0,0 @@ -get('entity.manager')->getStorage($entity_type->id()), - $container->get('entity.manager')->getStorage('moderation_state'), - $container->get('entity.manager')->getStorage('user_role') - ); - } - - /** - * Constructs a new ModerationStateTransitionListBuilder. - * - * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type - * Entity Type. - * @param \Drupal\Core\Entity\EntityStorageInterface $transition_storage - * Moderation state transition entity storage. - * @param \Drupal\Core\Entity\EntityStorageInterface $state_storage - * Moderation state entity storage. - * @param \Drupal\user\RoleStorageInterface $role_storage - * The role storage. - */ - public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $transition_storage, EntityStorageInterface $state_storage, RoleStorageInterface $role_storage) { - parent::__construct($entity_type, $transition_storage); - $this->stateStorage = $state_storage; - $this->roleStorage = $role_storage; - } - - /** - * {@inheritdoc} - */ - public function getFormId() { - return 'content_moderation_transition_list'; - } - - /** - * {@inheritdoc} - */ - public function buildHeader() { - $header['to'] = $this->t('To state'); - $header['label'] = $this->t('Button label'); - $header['roles'] = $this->t('Allowed roles'); - - return $header + parent::buildHeader(); - } - - /** - * {@inheritdoc} - */ - public function buildRow(EntityInterface $entity) { - $row['to']['#markup'] = $this->stateStorage->load($entity->getToState())->label(); - $row['label'] = $entity->label(); - $row['roles']['#markup'] = implode(', ', user_role_names(FALSE, 'use ' . $entity->id() . ' transition')); - - return $row + parent::buildRow($entity); - } - - /** - * {@inheritdoc} - */ - public function render() { - $build = parent::render(); - - $build['item'] = [ - '#type' => 'item', - '#markup' => $this->t('On this screen you can define transitions. Every time an entity is saved, it undergoes a transition. It is not possible to save an entity if it tries do a transition not defined here. Transitions do not necessarily mean a state change, it is possible to transition from a state to the same state but that transition needs to be defined here as well.'), - '#weight' => -5, - ]; - - return $build; - } - - /** - * {@inheritdoc} - */ - public function buildForm(array $form, FormStateInterface $form_state) { - $this->entities = $this->load(); - - // Get all the moderation states and sort them by weight. - $states = $this->stateStorage->loadMultiple(); - uasort($states, array($this->entityType->getClass(), 'sort')); - - /** @var \Drupal\content_moderation\ModerationStateTransitionInterface $entity */ - $groups = array_fill_keys(array_keys($states), []); - foreach ($this->entities as $entity) { - $groups[$entity->getFromState()][] = $entity; - } - - foreach ($groups as $group_name => $entities) { - $form[$group_name] = [ - '#type' => 'details', - '#title' => $this->t('From @state to...', ['@state' => $states[$group_name]->label()]), - // Make sure that the first group is always open. - '#open' => $group_name === array_keys($groups)[0], - ]; - - $form[$group_name][$this->entitiesKey] = array( - '#type' => 'table', - '#header' => $this->buildHeader(), - '#empty' => t('There is no @label yet.', array('@label' => $this->entityType->getLabel())), - '#tabledrag' => array( - array( - 'action' => 'order', - 'relationship' => 'sibling', - 'group' => 'weight', - ), - ), - ); - - $delta = 10; - // Change the delta of the weight field if have more than 20 entities. - if (!empty($this->weightKey)) { - $count = count($this->entities); - if ($count > 20) { - $delta = ceil($count / 2); - } - } - foreach ($entities as $entity) { - $row = $this->buildRow($entity); - if (isset($row['label'])) { - $row['label'] = array('#markup' => $row['label']); - } - if (isset($row['weight'])) { - $row['weight']['#delta'] = $delta; - } - $form[$group_name][$this->entitiesKey][$entity->id()] = $row; - } - } - - $form['actions']['#type'] = 'actions'; - $form['actions']['submit'] = array( - '#type' => 'submit', - '#value' => t('Save order'), - '#button_type' => 'primary', - ); - - return $form; - } - -} diff --git a/core/modules/content_moderation/src/Permissions.php b/core/modules/content_moderation/src/Permissions.php index 027684c..e6af161 100644 --- a/core/modules/content_moderation/src/Permissions.php +++ b/core/modules/content_moderation/src/Permissions.php @@ -3,8 +3,7 @@ namespace Drupal\content_moderation; use Drupal\Core\StringTranslation\StringTranslationTrait; -use Drupal\content_moderation\Entity\ModerationState; -use Drupal\content_moderation\Entity\ModerationStateTransition; +use Drupal\workflow\Entity\Workflow; /** * Defines a class for dynamic permissions based on transitions. @@ -22,19 +21,18 @@ class Permissions { public function transitionPermissions() { // @todo https://www.drupal.org/node/2779933 write a test for this. $perms = []; - /* @var \Drupal\content_moderation\ModerationStateInterface[] $states */ - $states = ModerationState::loadMultiple(); - /* @var \Drupal\content_moderation\ModerationStateTransitionInterface $transition */ - foreach (ModerationStateTransition::loadMultiple() as $id => $transition) { - $perms['use ' . $id . ' transition'] = [ - 'title' => $this->t('Use the %transition_name transition', [ - '%transition_name' => $transition->label(), - ]), - 'description' => $this->t('Move content from %from state to %to state.', [ - '%from' => $states[$transition->getFromState()]->label(), - '%to' => $states[$transition->getToState()]->label(), - ]), - ]; + + foreach (Workflow::loadMultiple() as $id => $workflow) { + foreach ($workflow->getStates() as $from_state => $state) { + foreach ($workflow->getPossibleStates($from_state) as $to_state) { + $perms['use ' . $workflow->id() . ' transition from ' . $from_state . ' to ' . $to_state] = [ + 'title' => $this->t('Move content from %from state to %to state.', [ + '%from' => $workflow->getStateLabel($from_state), + '%to' => $workflow->getStateLabel($to_state), + ]), + ]; + } + } } return $perms; diff --git a/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php b/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php index d6dc89d..4c19647 100644 --- a/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php +++ b/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php @@ -3,9 +3,7 @@ namespace Drupal\content_moderation\Plugin\Field\FieldWidget; use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Entity\Query\QueryInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\Plugin\Field\FieldWidget\OptionsSelectWidget; @@ -23,7 +21,7 @@ * id = "moderation_state_default", * label = @Translation("Moderation state"), * field_types = { - * "entity_reference" + * "string" * } * ) */ @@ -37,20 +35,6 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact protected $currentUser; /** - * Moderation state transition entity query. - * - * @var \Drupal\Core\Entity\Query\QueryInterface - */ - protected $moderationStateTransitionEntityQuery; - - /** - * Moderation state storage. - * - * @var \Drupal\Core\Entity\EntityStorageInterface - */ - protected $moderationStateStorage; - - /** * Moderation information service. * * @var \Drupal\content_moderation\ModerationInformation @@ -65,13 +49,6 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact protected $entityTypeManager; /** - * Moderation state transition storage. - * - * @var \Drupal\Core\Entity\EntityStorageInterface - */ - protected $moderationStateTransitionStorage; - - /** * Moderation state transition validation service. * * @var \Drupal\content_moderation\StateTransitionValidation @@ -95,22 +72,13 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact * Current user service. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * Entity type manager. - * @param \Drupal\Core\Entity\EntityStorageInterface $moderation_state_storage - * Moderation state storage. - * @param \Drupal\Core\Entity\EntityStorageInterface $moderation_state_transition_storage - * Moderation state transition storage. - * @param \Drupal\Core\Entity\Query\QueryInterface $entity_query - * Moderation transition entity query service. * @param \Drupal\content_moderation\ModerationInformation $moderation_information * Moderation information service. * @param \Drupal\content_moderation\StateTransitionValidation $validator * Moderation state transition validation service */ - public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, EntityStorageInterface $moderation_state_storage, EntityStorageInterface $moderation_state_transition_storage, QueryInterface $entity_query, ModerationInformation $moderation_information, StateTransitionValidation $validator) { + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, ModerationInformation $moderation_information, StateTransitionValidation $validator) { parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); - $this->moderationStateTransitionEntityQuery = $entity_query; - $this->moderationStateTransitionStorage = $moderation_state_transition_storage; - $this->moderationStateStorage = $moderation_state_storage; $this->entityTypeManager = $entity_type_manager; $this->currentUser = $current_user; $this->moderationInformation = $moderation_information; @@ -129,9 +97,6 @@ public static function create(ContainerInterface $container, array $configuratio $configuration['third_party_settings'], $container->get('current_user'), $container->get('entity_type.manager'), - $container->get('entity_type.manager')->getStorage('moderation_state'), - $container->get('entity_type.manager')->getStorage('moderation_state_transition'), - $container->get('entity.query')->get('moderation_state_transition', 'AND'), $container->get('content_moderation.moderation_information'), $container->get('content_moderation.state_transition_validation') ); @@ -151,19 +116,17 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen return $element + ['#access' => FALSE]; } - $default = $items->get($delta)->value ?: $bundle_entity->getThirdPartySetting('content_moderation', 'default_moderation_state', FALSE); - /** @var \Drupal\content_moderation\ModerationStateInterface $default_state */ - $default_state = $this->entityTypeManager->getStorage('moderation_state')->load($default); - if (!$default || !$default_state) { + $workflow = $this->moderationInformation->getWorkFlowForEntity($entity); + $default = $items->get($delta)->value ?: $workflow->getInitialState(); + if (!$default) { throw new \UnexpectedValueException(sprintf('The %s bundle has an invalid moderation state configuration, moderation states are enabled but no default is set.', $bundle_entity->label())); } $transitions = $this->validator->getValidTransitions($entity, $this->currentUser); $target_states = []; - /** @var \Drupal\content_moderation\Entity\ModerationStateTransition $transition */ foreach ($transitions as $transition) { - $target_states[$transition->getToState()] = $transition->label(); + $target_states[$transition] = $workflow->getTransitionLabel($default, $transition); } // @todo https://www.drupal.org/node/2779933 write a test for this. @@ -172,7 +135,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen '#type' => 'select', '#options' => $target_states, '#default_value' => $default, - '#published' => $default ? $default_state->isPublishedState() : FALSE, + '#published' => $default ? $workflow->isPublishedState($default) : FALSE, '#key_column' => $this->column, ]; $element['#element_validate'][] = array(get_class($this), 'validateElement'); @@ -197,7 +160,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen public static function updateStatus($entity_type_id, ContentEntityInterface $entity, array $form, FormStateInterface $form_state) { $element = $form_state->getTriggeringElement(); if (isset($element['#moderation_state'])) { - $entity->moderation_state->target_id = $element['#moderation_state']; + $entity->moderation_state->value = $element['#moderation_state']; } } diff --git a/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php b/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php index 80820b4..2213259 100644 --- a/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php +++ b/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php @@ -2,8 +2,7 @@ namespace Drupal\content_moderation\Plugin\Field; -use Drupal\content_moderation\Entity\ModerationState; -use Drupal\Core\Field\EntityReferenceFieldItemList; +use Drupal\Core\Field\FieldItemList; /** * A computed field that provides a content entity's moderation state. @@ -11,19 +10,20 @@ * It links content entities to a moderation state configuration entity via a * moderation state content entity. */ -class ModerationStateFieldItemList extends EntityReferenceFieldItemList { +class ModerationStateFieldItemList extends FieldItemList { /** * Gets the moderation state entity linked to a content entity revision. * - * @return \Drupal\content_moderation\ModerationStateInterface|null - * The moderation state configuration entity linked to a content entity - * revision. + * @return string|null + * The moderation state linked to a content entity revision. */ protected function getModerationState() { $entity = $this->getEntity(); - if (!\Drupal::service('content_moderation.moderation_information')->shouldModerateEntitiesOfBundle($entity->getEntityType(), $entity->bundle())) { + /** @var \Drupal\content_moderation\ModerationInformationInterface $moderation_info */ + $moderation_info = \Drupal::service('content_moderation.moderation_information'); + if (!$moderation_info->shouldModerateEntitiesOfBundle($entity->getEntityType(), $entity->bundle())) { return NULL; } @@ -51,17 +51,15 @@ protected function getModerationState() { $content_moderation_state = $content_moderation_state->getTranslation($langcode); } - return $content_moderation_state->get('moderation_state')->entity; + return $content_moderation_state->get('moderation_state')->value; } } // It is possible that the bundle does not exist at this point. For example, // the node type form creates a fake Node entity to get default values. // @see \Drupal\node\NodeTypeForm::form() - $bundle_entity = \Drupal::entityTypeManager() - ->getStorage($entity->getEntityType()->getBundleEntityType()) - ->load($entity->bundle()); - if ($bundle_entity && ($default = $bundle_entity->getThirdPartySetting('content_moderation', 'default_moderation_state'))) { - return ModerationState::load($default); + $workflow = $moderation_info->getWorkFlowForEntity($entity); + if ($workflow) { + return $workflow->getInitialState(); } } @@ -91,10 +89,11 @@ protected function computeModerationFieldItemList() { // Compute the value of the moderation state. $index = 0; if (!isset($this->list[$index]) || $this->list[$index]->isEmpty()) { + $moderation_state = $this->getModerationState(); // Do not store NULL values in the static cache. if ($moderation_state) { - $this->list[$index] = $this->createItem($index, ['entity' => $moderation_state]); + $this->list[$index] = $this->createItem($index, $moderation_state); } } } diff --git a/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraintValidator.php b/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraintValidator.php index ca75604..af4e549 100644 --- a/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraintValidator.php +++ b/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraintValidator.php @@ -2,7 +2,6 @@ namespace Drupal\content_moderation\Plugin\Validation\Constraint; -use Drupal\content_moderation\Entity\ModerationState as ModerationStateEntity; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; @@ -93,21 +92,12 @@ public function validate($value, Constraint $constraint) { $original_entity = $original_entity->getTranslation($entity->language()->getId()); } - if ($entity->moderation_state->target_id) { - $new_state_id = $entity->moderation_state->target_id; - } - else { - $new_state_id = $default = $this->entityTypeManager - ->getStorage($entity->getEntityType()->getBundleEntityType())->load($entity->bundle()) - ->getThirdPartySetting('content_moderation', 'default_moderation_state'); - } - if ($new_state_id) { - $new_state = ModerationStateEntity::load($new_state_id); - } - // @todo - what if $new_state_id references something that does not exist or + $workflow = $this->moderationInformation->getWorkFlowForEntity($entity); + $new_state = $entity->moderation_state->value ?: $workflow->getInitialState(); + // @todo - what if $new_state references something that does not exist or // is null. - if (!$this->validation->isTransitionAllowed($original_entity->moderation_state->entity, $new_state)) { - $this->context->addViolation($constraint->message, ['%from' => $original_entity->moderation_state->entity->label(), '%to' => $new_state->label()]); + if (!$workflow->canTranstion($original_entity->moderation_state->value, $new_state)) { + $this->context->addViolation($constraint->message, ['%from' => $workflow->getStateLabel($original_entity->moderation_state->value), '%to' => $workflow->getStateLabel($new_state)]); } } @@ -126,9 +116,9 @@ public function validate($value, Constraint $constraint) { protected function isFirstTimeModeration(EntityInterface $entity) { $original_entity = $this->moderationInformation->getLatestRevision($entity->getEntityTypeId(), $entity->id()); - $original_id = $original_entity->moderation_state->target_id; + $original_id = $original_entity->moderation_state; - return !($entity->moderation_state->target_id && $original_entity && $original_id); + return !($entity->moderation_state && $original_entity && $original_id); } } diff --git a/core/modules/content_moderation/src/Routing/EntityTypeModerationRouteProvider.php b/core/modules/content_moderation/src/Routing/EntityTypeModerationRouteProvider.php index c722a67..d1dcd2b 100644 --- a/core/modules/content_moderation/src/Routing/EntityTypeModerationRouteProvider.php +++ b/core/modules/content_moderation/src/Routing/EntityTypeModerationRouteProvider.php @@ -47,7 +47,7 @@ protected function getModerationFormRoute(EntityTypeInterface $entity_type) { '_entity_form' => "{$entity_type_id}.moderation", '_title' => 'Moderation', ]) - ->setRequirement('_permission', 'administer moderation states') + ->setRequirement('_permission', 'administer content moderation') ->setOption('parameters', [ $entity_type_id => ['type' => 'entity:' . $entity_type_id], ]); diff --git a/core/modules/content_moderation/src/StateTransitionValidation.php b/core/modules/content_moderation/src/StateTransitionValidation.php index 2e2a4e2..446fec6 100644 --- a/core/modules/content_moderation/src/StateTransitionValidation.php +++ b/core/modules/content_moderation/src/StateTransitionValidation.php @@ -3,10 +3,7 @@ namespace Drupal\content_moderation; use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Entity\Query\QueryFactory; use Drupal\Core\Session\AccountInterface; -use Drupal\content_moderation\Entity\ModerationStateTransition; /** * Validates whether a certain state transition is allowed. @@ -14,18 +11,11 @@ class StateTransitionValidation implements StateTransitionValidationInterface { /** - * Entity type manager. + * The moderation information service. * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface + * @var \Drupal\content_moderation\ModerationInformationInterface */ - protected $entityTypeManager; - - /** - * Entity query factory. - * - * @var \Drupal\Core\Entity\Query\QueryFactory - */ - protected $queryFactory; + protected $moderationInfo; /** * Stores the possible state transitions. @@ -37,211 +27,42 @@ class StateTransitionValidation implements StateTransitionValidationInterface { /** * Constructs a new StateTransitionValidation. * - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager service. - * @param \Drupal\Core\Entity\Query\QueryFactory $query_factory - * The entity query factory. + * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info + * The moderation information service. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, QueryFactory $query_factory) { - $this->entityTypeManager = $entity_type_manager; - $this->queryFactory = $query_factory; - } - - /** - * Computes a mapping of possible transitions. - * - * This method is uncached and will recalculate the list on every request. - * In most cases you want to use getPossibleTransitions() instead. - * - * @see static::getPossibleTransitions() - * - * @return array[] - * An array containing all possible transitions. Each entry is keyed by the - * "from" state, and the value is an array of all legal "to" states based - * on the currently defined transition objects. - */ - protected function calculatePossibleTransitions() { - $transitions = $this->transitionStorage()->loadMultiple(); - - $possible_transitions = []; - /** @var \Drupal\content_moderation\ModerationStateTransitionInterface $transition */ - foreach ($transitions as $transition) { - $possible_transitions[$transition->getFromState()][] = $transition->getToState(); - } - return $possible_transitions; - } - - /** - * Returns a mapping of possible transitions. - * - * @return array[] - * An array containing all possible transitions. Each entry is keyed by the - * "from" state, and the value is an array of all legal "to" states based - * on the currently defined transition objects. - */ - protected function getPossibleTransitions() { - if (empty($this->possibleTransitions)) { - $this->possibleTransitions = $this->calculatePossibleTransitions(); - } - return $this->possibleTransitions; - } - - /** - * {@inheritdoc} - */ - public function getValidTransitionTargets(ContentEntityInterface $entity, AccountInterface $user) { - $bundle = $this->loadBundleEntity($entity->getEntityType()->getBundleEntityType(), $entity->bundle()); - - $states_for_bundle = $bundle->getThirdPartySetting('content_moderation', 'allowed_moderation_states', []); - - /** @var \Drupal\content_moderation\Entity\ModerationState $current_state */ - $current_state = $entity->moderation_state->entity; - - $all_transitions = $this->getPossibleTransitions(); - $destination_ids = $all_transitions[$current_state->id()]; - - $destination_ids = array_intersect($states_for_bundle, $destination_ids); - $destinations = $this->entityTypeManager->getStorage('moderation_state')->loadMultiple($destination_ids); - - return array_filter($destinations, function(ModerationStateInterface $destination_state) use ($current_state, $user) { - return $this->userMayTransition($current_state, $destination_state, $user); - }); + public function __construct(ModerationInformationInterface $moderation_info) { + $this->moderationInfo = $moderation_info; } /** * {@inheritdoc} */ public function getValidTransitions(ContentEntityInterface $entity, AccountInterface $user) { - $bundle = $this->loadBundleEntity($entity->getEntityType()->getBundleEntityType(), $entity->bundle()); + $workflow = $this->moderationInfo->getWorkFlowForEntity($entity); - /** @var \Drupal\content_moderation\Entity\ModerationState $current_state */ - $current_state = $entity->moderation_state->entity; - $current_state_id = $current_state ? $current_state->id() : $bundle->getThirdPartySetting('content_moderation', 'default_moderation_state'); + $current_state = $entity->moderation_state->value; + $current_state = $current_state ?: $workflow->getInitialState(); - // Determine the states that are legal on this bundle. - $legal_bundle_states = $bundle->getThirdPartySetting('content_moderation', 'allowed_moderation_states', []); + $destinations = $workflow->getPossibleStates($current_state); - // Legal transitions include those that are possible from the current state, - // filtered by those whose target is legal on this bundle and that the - // user has access to execute. - $transitions = array_filter($this->getTransitionsFrom($current_state_id), function(ModerationStateTransition $transition) use ($legal_bundle_states, $user) { - return in_array($transition->getToState(), $legal_bundle_states, TRUE) - && $user->hasPermission('use ' . $transition->id() . ' transition'); + return array_filter($destinations, function($destination_state) use ($entity, $current_state, $user) { + return $this->userMayTransition($entity, $destination_state, $user); }); - - return $transitions; - } - - /** - * Returns a list of possible transitions from a given state. - * - * This list is based only on those transitions that exist, not what - * transitions are legal in a given context. - * - * @param string $state_name - * The machine name of the state from which we are transitioning. - * - * @return ModerationStateTransition[] - * A list of possible transitions from a given state. - */ - protected function getTransitionsFrom($state_name) { - $result = $this->transitionStateQuery() - ->condition('stateFrom', $state_name) - ->sort('weight') - ->execute(); - - return $this->transitionStorage()->loadMultiple($result); } /** * {@inheritdoc} */ - public function userMayTransition(ModerationStateInterface $from, ModerationStateInterface $to, AccountInterface $user) { - if ($transition = $this->getTransitionFromStates($from, $to)) { - return $user->hasPermission('use ' . $transition->id() . ' transition'); - } - return FALSE; - } - - /** - * Returns the transition object that transitions from one state to another. - * - * @param \Drupal\content_moderation\ModerationStateInterface $from - * The origin state. - * @param \Drupal\content_moderation\ModerationStateInterface $to - * The destination state. - * - * @return ModerationStateTransition|null - * A transition object, or NULL if there is no such transition. - */ - protected function getTransitionFromStates(ModerationStateInterface $from, ModerationStateInterface $to) { - $from = $this->transitionStateQuery() - ->condition('stateFrom', $from->id()) - ->condition('stateTo', $to->id()) - ->execute(); + public function userMayTransition(ContentEntityInterface $entity, $to, AccountInterface $user) { + $workflow = $this->moderationInfo->getWorkFlowForEntity($entity); - $transitions = $this->transitionStorage()->loadMultiple($from); + $current_state = $entity->moderation_state->value; + $current_state = $current_state ?: $workflow->getInitialState(); - if ($transitions) { - return current($transitions); - } - return NULL; - } - - /** - * {@inheritdoc} - */ - public function isTransitionAllowed(ModerationStateInterface $from, ModerationStateInterface $to) { - $allowed_transitions = $this->calculatePossibleTransitions(); - if (isset($allowed_transitions[$from->id()])) { - return in_array($to->id(), $allowed_transitions[$from->id()], TRUE); + if ($workflow->canTranstion($current_state, $to)) { + return $user->hasPermission('use ' . $workflow->id() . ' transition from ' . $current_state . ' to ' . $to); } return FALSE; } - /** - * Returns a transition state entity query. - * - * @return \Drupal\Core\Entity\Query\QueryInterface - * A transition state entity query. - */ - protected function transitionStateQuery() { - return $this->queryFactory->get('moderation_state_transition', 'AND'); - } - - /** - * Returns the transition entity storage service. - * - * @return \Drupal\Core\Entity\EntityStorageInterface - * The transition state entity storage. - */ - protected function transitionStorage() { - return $this->entityTypeManager->getStorage('moderation_state_transition'); - } - - /** - * Returns the state entity storage service. - * - * @return \Drupal\Core\Entity\EntityStorageInterface - * The moderation state entity storage. - */ - protected function stateStorage() { - return $this->entityTypeManager->getStorage('moderation_state'); - } - - /** - * Loads a specific bundle entity. - * - * @param string $bundle_entity_type_id - * The bundle entity type ID. - * @param string $bundle_id - * The bundle ID. - * - * @return \Drupal\Core\Config\Entity\ConfigEntityInterface|null - * The specific bundle entity. - */ - protected function loadBundleEntity($bundle_entity_type_id, $bundle_id) { - return $this->entityTypeManager->getStorage($bundle_entity_type_id)->load($bundle_id); - } - } diff --git a/core/modules/content_moderation/src/StateTransitionValidationInterface.php b/core/modules/content_moderation/src/StateTransitionValidationInterface.php index 5ef0dd1..e09e994 100644 --- a/core/modules/content_moderation/src/StateTransitionValidationInterface.php +++ b/core/modules/content_moderation/src/StateTransitionValidationInterface.php @@ -11,20 +11,6 @@ interface StateTransitionValidationInterface { /** - * Gets a list of states a user may transition an entity to. - * - * @param \Drupal\Core\Entity\ContentEntityInterface $entity - * The entity to be transitioned. - * @param \Drupal\Core\Session\AccountInterface $user - * The account that wants to perform a transition. - * - * @return \Drupal\content_moderation\Entity\ModerationState[] - * Returns an array of States to which the specified user may transition the - * entity. - */ - public function getValidTransitionTargets(ContentEntityInterface $entity, AccountInterface $user); - - /** * Gets a list of transitions that are legal for this user on this entity. * * @param \Drupal\Core\Entity\ContentEntityInterface $entity @@ -43,9 +29,9 @@ public function getValidTransitions(ContentEntityInterface $entity, AccountInter * This method will also return FALSE if there is no transition between the * specified states at all. * - * @param \Drupal\content_moderation\ModerationStateInterface $from - * The origin state. - * @param \Drupal\content_moderation\ModerationStateInterface $to + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity to check. + * @param string $to * The destination state. * @param \Drupal\Core\Session\AccountInterface $user * The user to validate. @@ -53,19 +39,6 @@ public function getValidTransitions(ContentEntityInterface $entity, AccountInter * @return bool * TRUE if the given user may transition between those two states. */ - public function userMayTransition(ModerationStateInterface $from, ModerationStateInterface $to, AccountInterface $user); - - /** - * Determines a transition allowed. - * - * @param \Drupal\content_moderation\ModerationStateInterface $from - * The origin state. - * @param \Drupal\content_moderation\ModerationStateInterface $to - * The destination state. - * - * @return bool - * Is the transition allowed. - */ - public function isTransitionAllowed(ModerationStateInterface $from, ModerationStateInterface $to); + public function userMayTransition(ContentEntityInterface $entity, $to, AccountInterface $user); } diff --git a/core/modules/content_moderation/src/Tests/ModerationFormTest.php b/core/modules/content_moderation/src/Tests/ModerationFormTest.php index d6c92b9..16da4e4 100644 --- a/core/modules/content_moderation/src/Tests/ModerationFormTest.php +++ b/core/modules/content_moderation/src/Tests/ModerationFormTest.php @@ -15,10 +15,7 @@ class ModerationFormTest extends ModerationStateTestBase { protected function setUp() { parent::setUp(); $this->drupalLogin($this->adminUser); - $this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE, [ - 'draft', - 'published', - ], 'draft'); + $this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE); $this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content'); } diff --git a/core/modules/content_moderation/src/Tests/ModerationLocaleTest.php b/core/modules/content_moderation/src/Tests/ModerationLocaleTest.php index 0d40356..a78c104 100644 --- a/core/modules/content_moderation/src/Tests/ModerationLocaleTest.php +++ b/core/modules/content_moderation/src/Tests/ModerationLocaleTest.php @@ -28,13 +28,7 @@ public function testTranslateModeratedContent() { $this->drupalLogin($this->rootUser); // Enable moderation on Article node type. - $this->createContentTypeFromUi( - 'Article', - 'article', - TRUE, - ['draft', 'published', 'archived'], - 'draft' - ); + $this->createContentTypeFromUi('Article', 'article', TRUE); // Add French language. $edit = [ @@ -103,9 +97,9 @@ public function testTranslateModeratedContent() { $french_node = $english_node->getTranslation('fr'); $this->assertEqual('French node', $french_node->label()); - $this->assertEqual($english_node->moderation_state->target_id, 'published'); + $this->assertEqual($english_node->moderation_state->value, 'published'); $this->assertTrue($english_node->isPublished()); - $this->assertEqual($french_node->moderation_state->target_id, 'draft'); + $this->assertEqual($french_node->moderation_state->value, 'draft'); $this->assertFalse($french_node->isPublished()); // Create another article with its translation. This time we will publish @@ -133,9 +127,9 @@ public function testTranslateModeratedContent() { $this->assertText(t('Article Translated node has been updated.')); $english_node = $this->drupalGetNodeByTitle('Another node', TRUE); $french_node = $english_node->getTranslation('fr'); - $this->assertEqual($french_node->moderation_state->target_id, 'published'); + $this->assertEqual($french_node->moderation_state->value, 'published'); $this->assertTrue($french_node->isPublished()); - $this->assertEqual($english_node->moderation_state->target_id, 'draft'); + $this->assertEqual($english_node->moderation_state->value, 'draft'); $this->assertFalse($english_node->isPublished()); // Now check that we can create a new draft of the translation. @@ -146,7 +140,7 @@ public function testTranslateModeratedContent() { $this->assertText(t('Article New draft of translated node has been updated.')); $english_node = $this->drupalGetNodeByTitle('Another node', TRUE); $french_node = $english_node->getTranslation('fr'); - $this->assertEqual($french_node->moderation_state->target_id, 'published'); + $this->assertEqual($french_node->moderation_state->value, 'published'); $this->assertTrue($french_node->isPublished()); $this->assertEqual($french_node->getTitle(), 'Translated node', 'The default revision of the published translation remains the same.'); @@ -158,7 +152,7 @@ public function testTranslateModeratedContent() { $this->assertText(t('The moderation state has been updated.')); $english_node = $this->drupalGetNodeByTitle('Another node', TRUE); $french_node = $english_node->getTranslation('fr'); - $this->assertEqual($french_node->moderation_state->target_id, 'published'); + $this->assertEqual($french_node->moderation_state->value, 'published'); $this->assertTrue($french_node->isPublished()); $this->assertEqual($french_node->getTitle(), 'New draft of translated node', 'The draft has replaced the published revision.'); @@ -166,7 +160,7 @@ public function testTranslateModeratedContent() { $this->drupalPostForm('node/' . $english_node->id() . '/edit', [], t('Save and Publish (this translation)')); $this->assertText(t('Article Another node has been updated.')); $english_node = $this->drupalGetNodeByTitle('Another node', TRUE); - $this->assertEqual($english_node->moderation_state->target_id, 'published'); + $this->assertEqual($english_node->moderation_state->value, 'published'); // Archive the node and its translation. $this->drupalPostForm('node/' . $english_node->id() . '/edit', [], t('Save and Archive (this translation)')); @@ -175,9 +169,9 @@ public function testTranslateModeratedContent() { $this->assertText(t('Article New draft of translated node has been updated.')); $english_node = $this->drupalGetNodeByTitle('Another node', TRUE); $french_node = $english_node->getTranslation('fr'); - $this->assertEqual($english_node->moderation_state->target_id, 'archived'); + $this->assertEqual($english_node->moderation_state->value, 'archived'); $this->assertFalse($english_node->isPublished()); - $this->assertEqual($french_node->moderation_state->target_id, 'archived'); + $this->assertEqual($french_node->moderation_state->value, 'archived'); $this->assertFalse($french_node->isPublished()); // Create another article with its translation. This time publishing english diff --git a/core/modules/content_moderation/src/Tests/ModerationStateBlockTest.php b/core/modules/content_moderation/src/Tests/ModerationStateBlockTest.php index 03a4b0e..3a7a68c 100644 --- a/core/modules/content_moderation/src/Tests/ModerationStateBlockTest.php +++ b/core/modules/content_moderation/src/Tests/ModerationStateBlockTest.php @@ -59,12 +59,7 @@ public function testCustomBlockModeration() { // Enable moderation for custom blocks at // admin/structure/block/block-content/manage/basic/moderation. - $edit = [ - 'enable_moderation_state' => TRUE, - 'allowed_moderation_states_unpublished[draft]' => TRUE, - 'allowed_moderation_states_published[published]' => TRUE, - 'default_moderation_state' => 'draft', - ]; + $edit = ['workflow' => 'typical']; $this->drupalPostForm(NULL, $edit, t('Save')); $this->assertText(t('Your settings have been saved.')); diff --git a/core/modules/content_moderation/src/Tests/ModerationStateNodeTest.php b/core/modules/content_moderation/src/Tests/ModerationStateNodeTest.php index e2069b4..62adcc9 100644 --- a/core/modules/content_moderation/src/Tests/ModerationStateNodeTest.php +++ b/core/modules/content_moderation/src/Tests/ModerationStateNodeTest.php @@ -18,13 +18,7 @@ class ModerationStateNodeTest extends ModerationStateTestBase { protected function setUp() { parent::setUp(); $this->drupalLogin($this->adminUser); - $this->createContentTypeFromUi( - 'Moderated content', - 'moderated_content', - TRUE, - ['draft', 'needs_review', 'published'], - 'draft' - ); + $this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE); $this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content'); } @@ -35,19 +29,11 @@ public function testCreatingContent() { $this->drupalPostForm('node/add/moderated_content', [ 'title[0][value]' => 'moderated content', ], t('Save and Create New Draft')); - $nodes = \Drupal::entityTypeManager() - ->getStorage('node') - ->loadByProperties([ - 'title' => 'moderated content', - ]); - - if (!$nodes) { + $node = $this->getNodeByTitle('moderated content'); + if (!$node) { $this->fail('Test node was not saved correctly.'); - return; } - - $node = reset($nodes); - $this->assertEqual('draft', $node->moderation_state->target_id); + $this->assertEqual('draft', $node->moderation_state->value); $path = 'node/' . $node->id() . '/edit'; // Set up published revision. @@ -56,7 +42,7 @@ public function testCreatingContent() { /* @var \Drupal\node\NodeInterface $node */ $node = \Drupal::entityTypeManager()->getStorage('node')->load($node->id()); $this->assertTrue($node->isPublished()); - $this->assertEqual('published', $node->moderation_state->target_id); + $this->assertEqual('published', $node->moderation_state->value); // Verify that the state field is not shown. $this->assertNoText('Published'); @@ -65,30 +51,26 @@ public function testCreatingContent() { $this->drupalPostForm('node/' . $node->id() . '/delete', array(), t('Delete')); $this->assertText(t('The Moderated content moderated content has been deleted.')); + // Disable content moderation. + $this->drupalPostForm('admin/structure/types/manage/moderated_content/moderation', ['workflow' => ''], t('Save')); $this->drupalGet('admin/structure/types/manage/moderated_content/moderation'); - $this->assertFieldByName('enable_moderation_state'); - $this->assertFieldChecked('edit-enable-moderation-state'); - $this->drupalPostForm(NULL, ['enable_moderation_state' => FALSE], t('Save')); - $this->drupalGet('admin/structure/types/manage/moderated_content/moderation'); - $this->assertFieldByName('enable_moderation_state'); - $this->assertNoFieldChecked('edit-enable-moderation-state'); + $this->assertOptionSelected('edit-workflow', ''); + // Clear bundle cache so workflow gets added or removed from the bundle + // information. + \Drupal::service('entity_type.bundle.info')->clearCachedBundles(); + // Clear field cache so extra field is added or removed. + \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); + + // Create a new node. $this->drupalPostForm('node/add/moderated_content', [ 'title[0][value]' => 'non-moderated content', ], t('Save and publish')); - $nodes = \Drupal::entityTypeManager() - ->getStorage('node') - ->loadByProperties([ - 'title' => 'non-moderated content', - ]); - - if (!$nodes) { + $node = $this->getNodeByTitle('non-moderated content'); + if (!$node) { $this->fail('Non-moderated test node was not saved correctly.'); - return; } - - $node = reset($nodes); - $this->assertEqual(NULL, $node->moderation_state->target_id); + $this->assertEqual(NULL, $node->moderation_state->value); } /** diff --git a/core/modules/content_moderation/src/Tests/ModerationStateNodeTypeTest.php b/core/modules/content_moderation/src/Tests/ModerationStateNodeTypeTest.php index debb32c..d23bdfe 100644 --- a/core/modules/content_moderation/src/Tests/ModerationStateNodeTypeTest.php +++ b/core/modules/content_moderation/src/Tests/ModerationStateNodeTypeTest.php @@ -42,12 +42,16 @@ public function testEnablingOnExistingContent() { ], t('Save and publish')); $this->assertText('Not moderated Test has been created.'); - // Now enable moderation state. - $this->enableModerationThroughUi( - 'not_moderated', - ['draft', 'needs_review', 'published'], - 'draft' - ); + // Now enable moderation state, ensuring all the expected links and tabs are + // present. + $this->drupalGet('admin/structure/types'); + $this->assertLinkByHref('admin/structure/types/manage/not_moderated/moderation'); + $this->drupalGet('admin/structure/types/manage/not_moderated'); + $this->assertLinkByHref('admin/structure/types/manage/not_moderated/moderation'); + $this->drupalGet('admin/structure/types/manage/not_moderated/moderation'); + $this->assertOptionSelected('edit-workflow', ''); + $edit['workflow'] = 'typical'; + $this->drupalPostForm(NULL, $edit, t('Save')); // And make sure it works. $nodes = \Drupal::entityTypeManager()->getStorage('node') diff --git a/core/modules/content_moderation/src/Tests/ModerationStateStatesTest.php b/core/modules/content_moderation/src/Tests/ModerationStateStatesTest.php deleted file mode 100644 index 3c5fd14..0000000 --- a/core/modules/content_moderation/src/Tests/ModerationStateStatesTest.php +++ /dev/null @@ -1,75 +0,0 @@ -drupalGet($path); - // No access. - $this->assertResponse(403); - } - $this->drupalLogin($this->adminUser); - foreach ($paths as $path) { - $this->drupalGet($path); - // User has access. - $this->assertResponse(200); - } - } - - /** - * Tests administration of moderation state entity. - */ - public function testStateAdministration() { - $this->drupalLogin($this->adminUser); - $this->drupalGet('admin/config/workflow/moderation'); - $this->assertLink('Moderation states'); - $this->assertLink('Moderation state transitions'); - $this->clickLink('Moderation states'); - $this->assertLink('Add moderation state'); - $this->assertText('Draft'); - // Edit the draft. - $this->clickLink('Edit', 0); - $this->assertFieldByName('label', 'Draft'); - $this->assertNoFieldChecked('edit-published'); - $this->drupalPostForm(NULL, [ - 'label' => 'Drafty', - ], t('Save')); - $this->assertText('Saved the Drafty Moderation state.'); - $this->drupalGet('admin/config/workflow/moderation/states/draft'); - $this->assertFieldByName('label', 'Drafty'); - $this->drupalPostForm(NULL, [ - 'label' => 'Draft', - ], t('Save')); - $this->assertText('Saved the Draft Moderation state.'); - $this->clickLink(t('Add moderation state')); - $this->drupalPostForm(NULL, [ - 'label' => 'Expired', - 'id' => 'expired', - ], t('Save')); - $this->assertText('Created the Expired Moderation state.'); - $this->drupalGet('admin/config/workflow/moderation/states/expired'); - $this->clickLink('Delete'); - $this->assertText('Are you sure you want to delete Expired?'); - $this->drupalPostForm(NULL, [], t('Delete')); - $this->assertText('Moderation state Expired deleted'); - } - -} diff --git a/core/modules/content_moderation/src/Tests/ModerationStateTestBase.php b/core/modules/content_moderation/src/Tests/ModerationStateTestBase.php index ddd275e..454dd08 100644 --- a/core/modules/content_moderation/src/Tests/ModerationStateTestBase.php +++ b/core/modules/content_moderation/src/Tests/ModerationStateTestBase.php @@ -5,7 +5,6 @@ use Drupal\Core\Session\AccountInterface; use Drupal\simpletest\WebTestBase; use Drupal\user\Entity\Role; -use Drupal\content_moderation\Entity\ModerationState; /** * Defines a base class for moderation state tests. @@ -30,12 +29,7 @@ * @var array */ protected $permissions = [ - 'administer moderation states', - 'administer moderation state transitions', - 'use draft_draft transition', - 'use draft_published transition', - 'use published_draft transition', - 'use published_archived transition', + 'administer content moderation', 'access administration pages', 'administer content types', 'administer nodes', @@ -61,6 +55,16 @@ */ protected function setUp() { parent::setUp(); + $transitions = [ + 'draft' => ['draft', 'published'], + 'published' => ['draft', 'published'], + ]; + $workflow_id = 'typical'; + foreach ($transitions as $from_state => $transition) { + foreach ($transition as $to_state) { + $this->permissions[] = $this->getWorkflowTransitionPermission($workflow_id, $from_state, $to_state); + } + } $this->adminUser = $this->drupalCreateUser($this->permissions); $this->drupalPlaceBlock('local_tasks_block', ['id' => 'tabs_block']); $this->drupalPlaceBlock('page_title_block'); @@ -68,6 +72,23 @@ protected function setUp() { } /** + * Gets the permission machine name for a transition. + * + * @param string $workflow_id + * The workflow ID. + * @param string $from_state + * The starting state. + * @param string $to_state + * The ending state. + * + * @return string + * The permission machine name for a transition. + */ + protected function getWorkflowTransitionPermission($workflow_id, $from_state, $to_state) { + return 'use ' . $workflow_id . ' transition from ' . $from_state . ' to ' . $to_state; + } + + /** * Creates a content-type from the UI. * * @param string $content_type_name @@ -76,12 +97,10 @@ protected function setUp() { * Machine name. * @param bool $moderated * TRUE if should be moderated. - * @param string[] $allowed_states - * Array of allowed state IDs. - * @param string $default_state - * Default state. + * @param string $workflow_id + * The workflow to attach to the bundle. */ - protected function createContentTypeFromUi($content_type_name, $content_type_id, $moderated = FALSE, array $allowed_states = [], $default_state = NULL) { + protected function createContentTypeFromUi($content_type_name, $content_type_id, $moderated = FALSE, $workflow_id = 'typical') { $this->drupalGet('admin/structure/types'); $this->clickLink('Add content type'); $edit = [ @@ -91,7 +110,7 @@ protected function createContentTypeFromUi($content_type_name, $content_type_id, $this->drupalPostForm(NULL, $edit, t('Save content type')); if ($moderated) { - $this->enableModerationThroughUi($content_type_id, $allowed_states, $default_state); + $this->enableModerationThroughUi($content_type_id, $workflow_id); } } @@ -100,31 +119,17 @@ protected function createContentTypeFromUi($content_type_name, $content_type_id, * * @param string $content_type_id * Machine name. - * @param string[] $allowed_states - * Array of allowed state IDs. - * @param string $default_state - * Default state. + * @param string $workflow_id + * The workflow to attach to the bundle. */ - protected function enableModerationThroughUi($content_type_id, array $allowed_states, $default_state) { - $this->drupalGet('admin/structure/types'); - $this->assertLinkByHref('admin/structure/types/manage/' . $content_type_id . '/moderation'); - $this->drupalGet('admin/structure/types/manage/' . $content_type_id); - $this->assertLinkByHref('admin/structure/types/manage/' . $content_type_id . '/moderation'); - $this->drupalGet('admin/structure/types/manage/' . $content_type_id . '/moderation'); - $this->assertFieldByName('enable_moderation_state'); - $this->assertNoFieldChecked('edit-enable-moderation-state'); - - $edit['enable_moderation_state'] = 1; - - /** @var ModerationState $state */ - foreach (ModerationState::loadMultiple() as $state) { - $key = $state->isPublishedState() ? 'allowed_moderation_states_published[' . $state->id() . ']' : 'allowed_moderation_states_unpublished[' . $state->id() . ']'; - $edit[$key] = in_array($state->id(), $allowed_states, TRUE) ? $state->id() : FALSE; - } - - $edit['default_moderation_state'] = $default_state; - - $this->drupalPostForm(NULL, $edit, t('Save')); + protected function enableModerationThroughUi($content_type_id, $workflow_id = 'typical') { + $edit['workflow'] = $workflow_id; + $this->drupalPostForm('admin/structure/types/manage/' . $content_type_id . '/moderation', $edit, t('Save')); + // Clear bundle cache so workflow gets added or removed from the bundle + // information. + \Drupal::service('entity_type.bundle.info')->clearCachedBundles(); + // Clear field cache so extra field is added or removed. + \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); } /** diff --git a/core/modules/content_moderation/src/Tests/ModerationStateTransitionsTest.php b/core/modules/content_moderation/src/Tests/ModerationStateTransitionsTest.php deleted file mode 100644 index 703561b..0000000 --- a/core/modules/content_moderation/src/Tests/ModerationStateTransitionsTest.php +++ /dev/null @@ -1,91 +0,0 @@ -drupalGet($path); - // No access. - $this->assertResponse(403); - } - $this->drupalLogin($this->adminUser); - foreach ($paths as $path) { - $this->drupalGet($path); - // User has access. - $this->assertResponse(200); - } - } - - /** - * Tests administration of moderation state transition entity. - */ - public function testTransitionAdministration() { - $this->drupalLogin($this->adminUser); - - $this->drupalGet('admin/config/workflow/moderation'); - $this->clickLink('Moderation state transitions'); - $this->assertLink('Add moderation state transition'); - $this->assertText('Create New Draft'); - - // Edit the Draft » Draft review. - $this->drupalGet('admin/config/workflow/moderation/transitions/draft_draft'); - $this->assertFieldByName('label', 'Create New Draft'); - $this->assertFieldByName('stateFrom', 'draft'); - $this->assertFieldByName('stateTo', 'draft'); - $this->drupalPostForm(NULL, [ - 'label' => 'Create Draft', - ], t('Save')); - $this->assertText('Saved the Create Draft Moderation state transition.'); - $this->drupalGet('admin/config/workflow/moderation/transitions/draft_draft'); - $this->assertFieldByName('label', 'Create Draft'); - // Now set it back. - $this->drupalPostForm(NULL, [ - 'label' => 'Create New Draft', - ], t('Save')); - $this->assertText('Saved the Create New Draft Moderation state transition.'); - - // Add a new state. - $this->drupalGet('admin/config/workflow/moderation/states/add'); - $this->drupalPostForm(NULL, [ - 'label' => 'Expired', - 'id' => 'expired', - ], t('Save')); - $this->assertText('Created the Expired Moderation state.'); - - // Add a new transition. - $this->drupalGet('admin/config/workflow/moderation/transitions'); - $this->clickLink(t('Add moderation state transition')); - $this->drupalPostForm(NULL, [ - 'label' => 'Published » Expired', - 'id' => 'published_expired', - 'stateFrom' => 'published', - 'stateTo' => 'expired', - ], t('Save')); - $this->assertText('Created the Published » Expired Moderation state transition.'); - - // Delete the new transition. - $this->drupalGet('admin/config/workflow/moderation/transitions/published_expired'); - $this->clickLink('Delete'); - $this->assertText('Are you sure you want to delete Published » Expired?'); - $this->drupalPostForm(NULL, [], t('Delete')); - $this->assertText('Moderation transition Published » Expired deleted'); - } - -} diff --git a/core/modules/content_moderation/src/Tests/NodeAccessTest.php b/core/modules/content_moderation/src/Tests/NodeAccessTest.php index 1b05406..b2f26d1 100644 --- a/core/modules/content_moderation/src/Tests/NodeAccessTest.php +++ b/core/modules/content_moderation/src/Tests/NodeAccessTest.php @@ -15,13 +15,7 @@ class NodeAccessTest extends ModerationStateTestBase { protected function setUp() { parent::setUp(); $this->drupalLogin($this->adminUser); - $this->createContentTypeFromUi( - 'Moderated content', - 'moderated_content', - TRUE, - ['draft', 'published'], - 'draft' - ); + $this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE); $this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content'); } @@ -35,20 +29,11 @@ public function testPageAccess() { $this->drupalPostForm('node/add/moderated_content', [ 'title[0][value]' => 'moderated content', ], t('Save and Create New Draft')); - $nodes = \Drupal::entityTypeManager() - ->getStorage('node') - ->loadByProperties([ - 'title' => 'moderated content', - ]); - - if (!$nodes) { + $node = $this->getNodeByTitle('moderated content'); + if (!$node) { $this->fail('Test node was not saved correctly.'); - return; } - /** @var \Drupal\node\NodeInterface $node */ - $node = reset($nodes); - $view_path = 'node/' . $node->id(); $edit_path = 'node/' . $node->id() . '/edit'; $latest_path = 'node/' . $node->id() . '/latest'; @@ -75,8 +60,8 @@ public function testPageAccess() { // Now make a new user and verify that the new user's access is correct. $user = $this->createUser([ - 'use draft_draft transition', - 'use published_draft transition', + $this->getWorkflowTransitionPermission('typical', 'draft', 'draft'), + $this->getWorkflowTransitionPermission('typical', 'published', 'draft'), 'view latest version', 'view any unpublished content', ]); @@ -92,7 +77,7 @@ public function testPageAccess() { // Now make another user, who should not be able to see forward revisions. $user = $this->createUser([ - 'use published_draft transition', + $this->getWorkflowTransitionPermission('typical', 'published', 'draft'), ]); $this->drupalLogin($user); diff --git a/core/modules/content_moderation/tests/src/Functional/LatestRevisionViewsFilterTest.php b/core/modules/content_moderation/tests/src/Functional/LatestRevisionViewsFilterTest.php index 77ae046..a61e518 100644 --- a/core/modules/content_moderation/tests/src/Functional/LatestRevisionViewsFilterTest.php +++ b/core/modules/content_moderation/tests/src/Functional/LatestRevisionViewsFilterTest.php @@ -5,6 +5,7 @@ use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; use Drupal\Tests\BrowserTestBase; +use Drupal\workflow\Entity\Workflow; /** * Tests the "Latest Revision" views filter. @@ -25,7 +26,7 @@ class LatestRevisionViewsFilterTest extends BrowserTestBase { * Tests view shows the correct node IDs. */ public function testViewShowsCorrectNids() { - $node_type = $this->createNodeType('Test', 'test'); + $this->createNodeType('Test', 'test'); $permissions = [ 'access content', @@ -45,8 +46,15 @@ public function testViewShowsCorrectNids() { $node_0->save(); // Now enable moderation for subsequent nodes. - $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE); - $node_type->save(); + $workflow = Workflow::load('typical'); + $workflow + ->applyToEntityTypeAndBundle('node', 'test') + ->save(); + // Clear bundle cache so workflow gets added or removed from the bundle + // information. + \Drupal::service('entity_type.bundle.info')->clearCachedBundles(); + // Clear field cache so extra field is added or removed. + \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); // Make a node that is only ever in Draft. /** @var Node $node_1 */ @@ -55,7 +63,7 @@ public function testViewShowsCorrectNids() { 'title' => 'Node 1 - Rev 1', 'uid' => $editor1->id(), ]); - $node_1->moderation_state->target_id = 'draft'; + $node_1->moderation_state->value = 'draft'; $node_1->save(); // Make a node that is in Draft, then Published. @@ -65,11 +73,11 @@ public function testViewShowsCorrectNids() { 'title' => 'Node 2 - Rev 1', 'uid' => $editor1->id(), ]); - $node_2->moderation_state->target_id = 'draft'; + $node_2->moderation_state->value = 'draft'; $node_2->save(); $node_2->setTitle('Node 2 - Rev 2'); - $node_2->moderation_state->target_id = 'published'; + $node_2->moderation_state->value = 'published'; $node_2->save(); // Make a node that is in Draft, then Published, then Draft. @@ -79,15 +87,15 @@ public function testViewShowsCorrectNids() { 'title' => 'Node 3 - Rev 1', 'uid' => $editor1->id(), ]); - $node_3->moderation_state->target_id = 'draft'; + $node_3->moderation_state->value = 'draft'; $node_3->save(); $node_3->setTitle('Node 3 - Rev 2'); - $node_3->moderation_state->target_id = 'published'; + $node_3->moderation_state->value = 'published'; $node_3->save(); $node_3->setTitle('Node 3 - Rev 3'); - $node_3->moderation_state->target_id = 'draft'; + $node_3->moderation_state->value = 'draft'; $node_3->save(); // Now show the View, and confirm that only the correct titles are showing. diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationStateAccessTest.php b/core/modules/content_moderation/tests/src/Functional/ModerationStateAccessTest.php index 799d89a..73501be 100644 --- a/core/modules/content_moderation/tests/src/Functional/ModerationStateAccessTest.php +++ b/core/modules/content_moderation/tests/src/Functional/ModerationStateAccessTest.php @@ -5,6 +5,7 @@ use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; use Drupal\Tests\BrowserTestBase; +use Drupal\workflow\Entity\Workflow; /** * Tests the view access control handler for moderation state entities. @@ -31,7 +32,7 @@ public function testViewShowsCorrectStates() { $permissions = [ 'access content', 'view all revisions', - 'view moderation states', + 'view content moderation', ]; $editor1 = $this->drupalCreateUser($permissions); $this->drupalLogin($editor1); @@ -41,7 +42,7 @@ public function testViewShowsCorrectStates() { 'title' => 'Draft node', 'uid' => $editor1->id(), ]); - $node_1->moderation_state->target_id = 'draft'; + $node_1->moderation_state->value = 'draft'; $node_1->save(); $node_2 = Node::create([ @@ -49,12 +50,12 @@ public function testViewShowsCorrectStates() { 'title' => 'Published node', 'uid' => $editor1->id(), ]); - $node_2->moderation_state->target_id = 'published'; + $node_2->moderation_state->value = 'published'; $node_2->save(); // Resave the node with a new state. $node_2->setTitle('Archived node'); - $node_2->moderation_state->target_id = 'archived'; + $node_2->moderation_state->value = 'archived'; $node_2->save(); // Now show the View, and confirm that the state labels are showing. @@ -68,7 +69,7 @@ public function testViewShowsCorrectStates() { $permissions = [ 'access content', 'view all revisions', - 'administer moderation states', + 'administer content moderation', ]; $admin1 = $this->drupalCreateUser($permissions); $this->drupalLogin($admin1); @@ -98,9 +99,17 @@ protected function createNodeType($label, $machine_name) { 'type' => $machine_name, 'label' => $label, ]); - $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE); $node_type->save(); + $workflow = Workflow::load('typical'); + $workflow + ->applyToEntityTypeAndBundle('node', $machine_name) + ->save(); + // Clear bundle cache so workflow gets added or removed from the bundle + // information. + \Drupal::service('entity_type.bundle.info')->clearCachedBundles(); + // Clear field cache so extra field is added or removed. + \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); return $node_type; } diff --git a/core/modules/content_moderation/tests/src/Kernel/ContentModerationSchemaTest.php b/core/modules/content_moderation/tests/src/Kernel/ContentModerationSchemaTest.php deleted file mode 100644 index 8b382c1..0000000 --- a/core/modules/content_moderation/tests/src/Kernel/ContentModerationSchemaTest.php +++ /dev/null @@ -1,89 +0,0 @@ -installConfig(['content_moderation']); - $typed_config = \Drupal::service('config.typed'); - $moderation_states = ModerationState::loadMultiple(); - foreach ($moderation_states as $moderation_state) { - $this->assertConfigSchema($typed_config, $moderation_state->getEntityType()->getConfigPrefix() . '.' . $moderation_state->id(), $moderation_state->toArray()); - } - $moderation_state_transitions = ModerationStateTransition::loadMultiple(); - foreach ($moderation_state_transitions as $moderation_state_transition) { - $this->assertConfigSchema($typed_config, $moderation_state_transition->getEntityType()->getConfigPrefix() . '.' . $moderation_state_transition->id(), $moderation_state_transition->toArray()); - } - - } - - /** - * Tests content moderation third party schema for node types. - */ - public function testContentModerationNodeTypeConfig() { - $this->installEntitySchema('node'); - $this->installEntitySchema('user'); - $this->installConfig(['content_moderation']); - $typed_config = \Drupal::service('config.typed'); - $moderation_states = ModerationState::loadMultiple(); - $node_type = NodeType::create([ - 'type' => 'example', - ]); - $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE); - $node_type->setThirdPartySetting('content_moderation', 'allowed_moderation_states', array_keys($moderation_states)); - $node_type->setThirdPartySetting('content_moderation', 'default_moderation_state', ''); - $node_type->save(); - $this->assertConfigSchema($typed_config, $node_type->getEntityType()->getConfigPrefix() . '.' . $node_type->id(), $node_type->toArray()); - } - - /** - * Tests content moderation third party schema for block content types. - */ - public function testContentModerationBlockContentTypeConfig() { - $this->installEntitySchema('block_content'); - $this->installEntitySchema('user'); - $this->installConfig(['content_moderation']); - $typed_config = \Drupal::service('config.typed'); - $moderation_states = ModerationState::loadMultiple(); - $block_content_type = BlockContentType::create([ - 'id' => 'basic', - 'label' => 'basic', - 'revision' => TRUE, - ]); - $block_content_type->setThirdPartySetting('content_moderation', 'enabled', TRUE); - $block_content_type->setThirdPartySetting('content_moderation', 'allowed_moderation_states', array_keys($moderation_states)); - $block_content_type->setThirdPartySetting('content_moderation', 'default_moderation_state', ''); - $block_content_type->save(); - $this->assertConfigSchema($typed_config, $block_content_type->getEntityType()->getConfigPrefix() . '.' . $block_content_type->id(), $block_content_type->toArray()); - } - -} diff --git a/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php b/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php index 3ba37a2..2b743b8 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php @@ -3,12 +3,12 @@ namespace Drupal\Tests\content_moderation\Kernel; use Drupal\content_moderation\Entity\ContentModerationState; -use Drupal\content_moderation\Entity\ModerationState; use Drupal\KernelTests\KernelTestBase; use Drupal\language\Entity\ConfigurableLanguage; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; use Drupal\node\NodeInterface; +use Drupal\workflow\Entity\Workflow; /** * Tests links between a content entity and a content_moderation_state entity. @@ -22,6 +22,7 @@ class ContentModerationStateTest extends KernelTestBase { */ public static $modules = [ 'node', + 'workflow', 'content_moderation', 'user', 'system', @@ -49,24 +50,26 @@ public function testBasicModeration() { $node_type = NodeType::create([ 'type' => 'example', ]); - $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE); - $node_type->setThirdPartySetting('content_moderation', 'allowed_moderation_states', ['draft', 'published']); - $node_type->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft'); $node_type->save(); + + $workflow = Workflow::load('typical'); + $workflow + ->applyToEntityTypeAndBundle('node', 'example') + ->save(); + $node = Node::create([ 'type' => 'example', 'title' => 'Test title', ]); $node->save(); $node = $this->reloadNode($node); - $this->assertEquals('draft', $node->moderation_state->entity->id()); + $this->assertEquals('draft', $node->moderation_state->value); - $published = ModerationState::load('published'); - $node->moderation_state->entity = $published; + $node->moderation_state->value = 'published'; $node->save(); $node = $this->reloadNode($node); - $this->assertEquals('published', $node->moderation_state->entity->id()); + $this->assertEquals('published', $node->moderation_state->value); // Change the state without saving the node. $content_moderation_state = ContentModerationState::load(1); @@ -75,7 +78,7 @@ public function testBasicModeration() { $content_moderation_state->save(); $node = $this->reloadNode($node, 3); - $this->assertEquals('draft', $node->moderation_state->entity->id()); + $this->assertEquals('draft', $node->moderation_state->value); $this->assertFalse($node->isPublished()); // Get the default revision. @@ -83,11 +86,11 @@ public function testBasicModeration() { $this->assertTrue($node->isPublished()); $this->assertEquals(2, $node->getRevisionId()); - $node->moderation_state->target_id = 'published'; + $node->moderation_state->value = 'published'; $node->save(); $node = $this->reloadNode($node, 4); - $this->assertEquals('published', $node->moderation_state->entity->id()); + $this->assertEquals('published', $node->moderation_state->value); // Get the default revision. $node = $this->reloadNode($node); @@ -105,10 +108,13 @@ public function testMultilingualModeration() { $node_type = NodeType::create([ 'type' => 'example', ]); - $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE); - $node_type->setThirdPartySetting('content_moderation', 'allowed_moderation_states', ['draft', 'published']); - $node_type->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft'); $node_type->save(); + + $workflow = Workflow::load('typical'); + $workflow + ->applyToEntityTypeAndBundle('node', 'example') + ->save(); + $english_node = Node::create([ 'type' => 'example', 'title' => 'Test title', @@ -117,7 +123,7 @@ public function testMultilingualModeration() { $english_node ->setPublished(FALSE) ->save(); - $this->assertEquals('draft', $english_node->moderation_state->entity->id()); + $this->assertEquals('draft', $english_node->moderation_state->value); $this->assertFalse($english_node->isPublished()); // Create a French translation. @@ -126,34 +132,34 @@ public function testMultilingualModeration() { // Revision 1 (fr). $french_node->save(); $french_node = $this->reloadNode($english_node)->getTranslation('fr'); - $this->assertEquals('draft', $french_node->moderation_state->entity->id()); + $this->assertEquals('draft', $french_node->moderation_state->value); $this->assertFalse($french_node->isPublished()); // Move English node to create another draft. $english_node = $this->reloadNode($english_node); - $english_node->moderation_state->target_id = 'draft'; + $english_node->moderation_state->value = 'draft'; // Revision 2 (en, fr). $english_node->save(); $english_node = $this->reloadNode($english_node); - $this->assertEquals('draft', $english_node->moderation_state->entity->id()); + $this->assertEquals('draft', $english_node->moderation_state->value); // French node should still be in draft. $french_node = $this->reloadNode($english_node)->getTranslation('fr'); - $this->assertEquals('draft', $french_node->moderation_state->entity->id()); + $this->assertEquals('draft', $french_node->moderation_state->value); // Publish the French node. - $french_node->moderation_state->target_id = 'published'; + $french_node->moderation_state->value = 'published'; // Revision 3 (en, fr). $french_node->save(); $french_node = $this->reloadNode($french_node)->getTranslation('fr'); $this->assertTrue($french_node->isPublished()); - $this->assertEquals('published', $french_node->moderation_state->entity->id()); + $this->assertEquals('published', $french_node->moderation_state->value); $this->assertTrue($french_node->isPublished()); $english_node = $french_node->getTranslation('en'); - $this->assertEquals('draft', $english_node->moderation_state->entity->id()); + $this->assertEquals('draft', $english_node->moderation_state->value); // Publish the English node. - $english_node->moderation_state->target_id = 'published'; + $english_node->moderation_state->value = 'published'; // Revision 4 (en, fr). $english_node->save(); $english_node = $this->reloadNode($english_node); @@ -162,7 +168,7 @@ public function testMultilingualModeration() { // Move the French node back to draft. $french_node = $this->reloadNode($english_node)->getTranslation('fr'); $this->assertTrue($french_node->isPublished()); - $french_node->moderation_state->target_id = 'draft'; + $french_node->moderation_state->value = 'draft'; // Revision 5 (en, fr). $french_node->save(); $french_node = $this->reloadNode($english_node, 5)->getTranslation('fr'); @@ -170,7 +176,7 @@ public function testMultilingualModeration() { $this->assertTrue($french_node->getTranslation('en')->isPublished()); // Republish the French node. - $french_node->moderation_state->target_id = 'published'; + $french_node->moderation_state->value = 'published'; // Revision 6 (en, fr). $french_node->save(); $french_node = $this->reloadNode($english_node)->getTranslation('fr'); @@ -184,9 +190,9 @@ public function testMultilingualModeration() { $content_moderation_state->save(); $english_node = $this->reloadNode($french_node, $french_node->getRevisionId() + 1); - $this->assertEquals('draft', $english_node->moderation_state->entity->id()); + $this->assertEquals('draft', $english_node->moderation_state->value); $french_node = $this->reloadNode($english_node)->getTranslation('fr'); - $this->assertEquals('published', $french_node->moderation_state->entity->id()); + $this->assertEquals('published', $french_node->moderation_state->value); // This should unpublish the French node. $content_moderation_state = ContentModerationState::load(1); @@ -197,9 +203,9 @@ public function testMultilingualModeration() { $content_moderation_state->save(); $english_node = $this->reloadNode($english_node, $english_node->getRevisionId()); - $this->assertEquals('draft', $english_node->moderation_state->entity->id()); + $this->assertEquals('draft', $english_node->moderation_state->value); $french_node = $this->reloadNode($english_node, '8')->getTranslation('fr'); - $this->assertEquals('draft', $french_node->moderation_state->entity->id()); + $this->assertEquals('draft', $french_node->moderation_state->value); // Switching the moderation state to an unpublished state should update the // entity. $this->assertFalse($french_node->isPublished()); diff --git a/core/modules/content_moderation/tests/src/Kernel/EntityOperationsTest.php b/core/modules/content_moderation/tests/src/Kernel/EntityOperationsTest.php index 929356e..3a9f35f 100644 --- a/core/modules/content_moderation/tests/src/Kernel/EntityOperationsTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/EntityOperationsTest.php @@ -4,9 +4,9 @@ use Drupal\KernelTests\KernelTestBase; -use Drupal\content_moderation\Entity\ModerationState; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; +use Drupal\workflow\Entity\Workflow; /** * @coversDefaultClass \Drupal\content_moderation\EntityOperations @@ -20,6 +20,7 @@ class EntityOperationsTest extends KernelTestBase { */ public static $modules = [ 'content_moderation', + 'workflow', 'node', 'user', 'system', @@ -47,8 +48,11 @@ protected function createNodeType() { 'type' => 'page', 'label' => 'Page', ]); - $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE); $node_type->save(); + $workflow = Workflow::load('typical'); + $workflow + ->applyToEntityTypeAndBundle('node', 'page') + ->save(); } /** @@ -60,7 +64,7 @@ public function testForwardRevisions() { 'type' => 'page', 'title' => 'A', ]); - $page->moderation_state->target_id = 'draft'; + $page->moderation_state->value = 'draft'; $page->save(); $id = $page->id(); @@ -75,7 +79,7 @@ public function testForwardRevisions() { // Moderate the entity to published. $page->setTitle('B'); - $page->moderation_state->target_id = 'published'; + $page->moderation_state->value = 'published'; $page->save(); // Verify the entity is now published and public. @@ -86,7 +90,7 @@ public function testForwardRevisions() { // Make a new forward-revision in Draft. $page->setTitle('C'); - $page->moderation_state->target_id = 'draft'; + $page->moderation_state->value = 'draft'; $page->save(); // Verify normal loads return the still-default previous version. @@ -105,7 +109,7 @@ public function testForwardRevisions() { $this->assertEquals('C', $page->getTitle()); $page->setTitle('D'); - $page->moderation_state->target_id = 'published'; + $page->moderation_state->value = 'published'; $page->save(); // Verify normal loads return the still-default previous version. @@ -116,7 +120,7 @@ public function testForwardRevisions() { // Now check that we can immediately add a new published revision over it. $page->setTitle('E'); - $page->moderation_state->target_id = 'published'; + $page->moderation_state->value = 'published'; $page->save(); $page = Node::load($id); @@ -134,7 +138,7 @@ public function testPublishedCreation() { 'type' => 'page', 'title' => 'A', ]); - $page->moderation_state->target_id = 'published'; + $page->moderation_state->value = 'published'; $page->save(); $id = $page->id(); @@ -151,29 +155,12 @@ public function testPublishedCreation() { * Verifies that an unpublished state may be made the default revision. */ public function testArchive() { - $published_id = $this->randomMachineName(); - $published_state = ModerationState::create([ - 'id' => $published_id, - 'label' => $this->randomString(), - 'published' => TRUE, - 'default_revision' => TRUE, - ]); - $published_state->save(); - - $archived_id = $this->randomMachineName(); - $archived_state = ModerationState::create([ - 'id' => $archived_id, - 'label' => $this->randomString(), - 'published' => FALSE, - 'default_revision' => TRUE, - ]); - $archived_state->save(); - $page = Node::create([ 'type' => 'page', 'title' => $this->randomString(), ]); - $page->moderation_state->target_id = $published_id; + + $page->moderation_state->value = 'published'; $page->save(); $id = $page->id(); @@ -184,7 +171,7 @@ public function testArchive() { // When the page is moderated to the archived state, then the latest // revision should be the default revision, and it should be unpublished. - $page->moderation_state->target_id = $archived_id; + $page->moderation_state->value = 'archived'; $page->save(); $new_revision_id = $page->getRevisionId(); diff --git a/core/modules/content_moderation/tests/src/Kernel/EntityRevisionConverterTest.php b/core/modules/content_moderation/tests/src/Kernel/EntityRevisionConverterTest.php index 89c84f9..7a509c3 100644 --- a/core/modules/content_moderation/tests/src/Kernel/EntityRevisionConverterTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/EntityRevisionConverterTest.php @@ -6,6 +6,7 @@ use Drupal\KernelTests\KernelTestBase; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; +use Drupal\workflow\Entity\Workflow; /** * @coversDefaultClass \Drupal\content_moderation\ParamConverter\EntityRevisionConverter @@ -18,6 +19,7 @@ class EntityRevisionConverterTest extends KernelTestBase { 'entity_test', 'system', 'content_moderation', + 'workflow', 'node', ]; @@ -59,17 +61,22 @@ public function testConvertNonRevisionableEntityType() { * @covers ::convert */ public function testConvertWithRevisionableEntityType() { + $this->installConfig(['content_moderation']); $node_type = NodeType::create([ 'type' => 'article', ]); - $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE); $node_type->save(); + $workflow = Workflow::load('typical'); + $workflow + ->applyToEntityTypeAndBundle('node', 'article') + ->save(); $revision_ids = []; $node = Node::create([ 'title' => 'test', 'type' => 'article', ]); + $node->moderation_state->value = 'published'; $node->save(); $revision_ids[] = $node->getRevisionId(); @@ -79,7 +86,7 @@ public function testConvertWithRevisionableEntityType() { $revision_ids[] = $node->getRevisionId(); $node->setNewRevision(TRUE); - $node->isDefaultRevision(FALSE); + $node->moderation_state->value = 'draft'; $node->save(); $revision_ids[] = $node->getRevisionId(); diff --git a/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php b/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php index 97e61f1..8f8d464 100644 --- a/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php @@ -6,6 +6,7 @@ use Drupal\language\Entity\ConfigurableLanguage; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; +use Drupal\workflow\Entity\Workflow; /** * @coversDefaultClass \Drupal\content_moderation\Plugin\Validation\Constraint\ModerationStateConstraintValidator @@ -18,6 +19,7 @@ class EntityStateChangeValidationTest extends KernelTestBase { */ public static $modules = [ 'node', + 'workflow', 'content_moderation', 'user', 'system', @@ -47,20 +49,24 @@ public function testValidTransition() { $node_type = NodeType::create([ 'type' => 'example', ]); - $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE); $node_type->save(); + $workflow = Workflow::load('typical'); + $workflow + ->applyToEntityTypeAndBundle('node', 'example') + ->save(); + $node = Node::create([ 'type' => 'example', 'title' => 'Test title', ]); - $node->moderation_state->target_id = 'draft'; + $node->moderation_state->value = 'draft'; $node->save(); - $node->moderation_state->target_id = 'published'; + $node->moderation_state->value = 'published'; $this->assertCount(0, $node->validate()); $node->save(); - $this->assertEquals('published', $node->moderation_state->entity->id()); + $this->assertEquals('published', $node->moderation_state->value); } /** @@ -72,16 +78,19 @@ public function testInvalidTransition() { $node_type = NodeType::create([ 'type' => 'example', ]); - $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE); $node_type->save(); + $workflow = Workflow::load('typical'); + $workflow + ->applyToEntityTypeAndBundle('node', 'example') + ->save(); $node = Node::create([ 'type' => 'example', 'title' => 'Test title', ]); - $node->moderation_state->target_id = 'draft'; + $node->moderation_state->value = 'draft'; $node->save(); - $node->moderation_state->target_id = 'archived'; + $node->moderation_state->value = 'archived'; $violations = $node->validate(); $this->assertCount(1, $violations); @@ -106,12 +115,10 @@ public function testLegacyContent() { $nid = $node->id(); // Enable moderation for our node type. - /** @var NodeType $node_type */ - $node_type = NodeType::load('example'); - $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE); - $node_type->setThirdPartySetting('content_moderation', 'allowed_moderation_states', ['draft', 'published']); - $node_type->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft'); - $node_type->save(); + $workflow = Workflow::load('typical'); + $workflow + ->applyToEntityTypeAndBundle('node', 'example') + ->save(); $node = Node::load($nid); @@ -155,12 +162,10 @@ public function testLegacyMultilingualContent() { $node_fr->save(); // Enable moderation for our node type. - /** @var NodeType $node_type */ - $node_type = NodeType::load('example'); - $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE); - $node_type->setThirdPartySetting('content_moderation', 'allowed_moderation_states', ['draft', 'published']); - $node_type->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft'); - $node_type->save(); + $workflow = Workflow::load('typical'); + $workflow + ->applyToEntityTypeAndBundle('node', 'example') + ->save(); // Reload the French version of the node. $node = Node::load($nid); diff --git a/core/modules/content_moderation/tests/src/Kernel/ModerationStateEntityTest.php b/core/modules/content_moderation/tests/src/Kernel/ModerationStateEntityTest.php deleted file mode 100644 index f312cde..0000000 --- a/core/modules/content_moderation/tests/src/Kernel/ModerationStateEntityTest.php +++ /dev/null @@ -1,69 +0,0 @@ -installEntitySchema('moderation_state'); - } - - /** - * Verify moderation state methods based on entity properties. - * - * @covers ::isPublishedState - * @covers ::isDefaultRevisionState - * - * @dataProvider moderationStateProvider - */ - public function testModerationStateProperties($published, $default_revision, $is_published, $is_default) { - $moderation_state_id = $this->randomMachineName(); - $moderation_state = ModerationState::create([ - 'id' => $moderation_state_id, - 'label' => $this->randomString(), - 'published' => $published, - 'default_revision' => $default_revision, - ]); - $moderation_state->save(); - - $moderation_state = ModerationState::load($moderation_state_id); - $this->assertEquals($is_published, $moderation_state->isPublishedState()); - $this->assertEquals($is_default, $moderation_state->isDefaultRevisionState()); - } - - /** - * Data provider for ::testModerationStateProperties. - */ - public function moderationStateProvider() { - return [ - // Draft, Needs review; should not touch the default revision. - [FALSE, FALSE, FALSE, FALSE], - // Published; this state should update and publish the default revision. - [TRUE, TRUE, TRUE, TRUE], - // Archive; this state should update but not publish the default revision. - [FALSE, TRUE, FALSE, TRUE], - // We try to prevent creating this state via the UI, but when a moderation - // state is a published state, it should also become the default revision. - [TRUE, FALSE, TRUE, TRUE], - ]; - } - -} diff --git a/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php b/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php index c57963e..76809c6 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php @@ -5,6 +5,7 @@ use Drupal\KernelTests\KernelTestBase; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; +use Drupal\workflow\Entity\Workflow; /** * @coversDefaultClass \Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList @@ -18,6 +19,7 @@ class ModerationStateFieldItemListTest extends KernelTestBase { */ public static $modules = [ 'node', + 'workflow', 'content_moderation', 'user', 'system', @@ -44,10 +46,12 @@ protected function setUp() { $node_type = NodeType::create([ 'type' => 'example', ]); - $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE); - $node_type->setThirdPartySetting('content_moderation', 'allowed_moderation_states', ['draft']); - $node_type->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft'); $node_type->save(); + $workflow = Workflow::load('typical'); + $workflow + ->applyToEntityTypeAndBundle('node', 'example') + ->save(); + $this->testNode = Node::create([ 'type' => 'example', 'title' => 'Test title', @@ -61,7 +65,7 @@ protected function setUp() { * Test the field item list when accessing an index. */ public function testArrayIndex() { - $this->assertEquals('draft', $this->testNode->moderation_state[0]->entity->id()); + $this->assertEquals('draft', $this->testNode->moderation_state[0]->value); } /** @@ -70,7 +74,7 @@ public function testArrayIndex() { public function testArrayIteration() { $states = []; foreach ($this->testNode->moderation_state as $item) { - $states[] = $item->entity->id(); + $states[] = $item->value; } $this->assertEquals(['draft'], $states); } diff --git a/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php b/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php index c869619..c8a4dc0 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php @@ -6,6 +6,7 @@ use Drupal\node\Entity\NodeType; use Drupal\Tests\views\Kernel\ViewsKernelTestBase; use Drupal\views\Views; +use Drupal\workflow\Entity\Workflow; /** * Tests the views integration of content_moderation. @@ -21,6 +22,7 @@ class ViewsDataIntegrationTest extends ViewsKernelTestBase { 'content_moderation_test_views', 'node', 'content_moderation', + 'workflow', ]; /** @@ -39,8 +41,11 @@ protected function setUp($import_test_views = TRUE) { $node_type = NodeType::create([ 'type' => 'page', ]); - $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE); $node_type->save(); + $workflow = Workflow::load('typical'); + $workflow + ->applyToEntityTypeAndBundle('node', 'page') + ->save(); } /** @@ -53,14 +58,14 @@ public function testViewsData() { 'type' => 'page', 'title' => 'Test title first revision', ]); - $node->moderation_state->target_id = 'published'; + $node->moderation_state->value = 'published'; $node->save(); $revision = clone $node; $revision->setNewRevision(TRUE); $revision->isDefaultRevision(FALSE); $revision->title->value = 'Test title second revision'; - $revision->moderation_state->target_id = 'draft'; + $revision->moderation_state->value = 'draft'; $revision->save(); $view = Views::getView('test_content_moderation_latest_revision'); @@ -90,14 +95,14 @@ public function testContentModerationStateRevisionJoin() { 'type' => 'page', 'title' => 'Test title first revision', ]); - $node->moderation_state->target_id = 'published'; + $node->moderation_state->value = 'published'; $node->save(); $revision = clone $node; $revision->setNewRevision(TRUE); $revision->isDefaultRevision(FALSE); $revision->title->value = 'Test title second revision'; - $revision->moderation_state->target_id = 'draft'; + $revision->moderation_state->value = 'draft'; $revision->save(); $view = Views::getView('test_content_moderation_revision_test'); @@ -124,14 +129,14 @@ public function testContentModerationStateBaseJoin() { 'type' => 'page', 'title' => 'Test title first revision', ]); - $node->moderation_state->target_id = 'published'; + $node->moderation_state->value = 'published'; $node->save(); $revision = clone $node; $revision->setNewRevision(TRUE); $revision->isDefaultRevision(FALSE); $revision->title->value = 'Test title second revision'; - $revision->moderation_state->target_id = 'draft'; + $revision->moderation_state->value = 'draft'; $revision->save(); $view = Views::getView('test_content_moderation_base_table_test'); diff --git a/core/modules/content_moderation/tests/src/Unit/ModerationInformationTest.php b/core/modules/content_moderation/tests/src/Unit/ModerationInformationTest.php index ff46e41..632b9bc 100644 --- a/core/modules/content_moderation/tests/src/Unit/ModerationInformationTest.php +++ b/core/modules/content_moderation/tests/src/Unit/ModerationInformationTest.php @@ -3,10 +3,9 @@ namespace Drupal\Tests\content_moderation\Unit; use Drupal\content_moderation\Entity\Handler\ModerationHandler; -use Drupal\Core\Config\Entity\ConfigEntityInterface; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\ContentEntityType; -use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Session\AccountInterface; use Drupal\content_moderation\ModerationInformation; @@ -30,43 +29,44 @@ protected function getUser() { /** * Returns a mock Entity Type Manager. * - * @param \Drupal\Core\Entity\EntityStorageInterface $entity_bundle_storage - * Entity bundle storage. - * * @return EntityTypeManagerInterface * The mocked entity type manager. */ - protected function getEntityTypeManager(EntityStorageInterface $entity_bundle_storage) { + protected function getEntityTypeManager() { $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); - $entity_type_manager->getStorage('entity_test_bundle')->willReturn($entity_bundle_storage); return $entity_type_manager->reveal(); } /** * Sets up content moderation and entity manager mocking. * - * @param bool $status - * TRUE if content_moderation should be enabled, FALSE if not. + * @param string $bundle + * The bundle ID. + * @param string|null $workflow + * The workflow ID. If nul no workflow information is added to the bundle. + * @param string|null $default_state + * The default state for the workflow. * * @return \Drupal\Core\Entity\EntityTypeManagerInterface * The mocked entity type manager. */ - public function setupModerationEntityManager($status) { - $bundle = $this->prophesize(ConfigEntityInterface::class); - $bundle->getThirdPartySetting('content_moderation', 'enabled', FALSE)->willReturn($status); - - $entity_storage = $this->prophesize(EntityStorageInterface::class); - $entity_storage->load('test_bundle')->willReturn($bundle->reveal()); - - return $this->getEntityTypeManager($entity_storage->reveal()); + public function setupModerationBundleInfo($bundle, $workflow = NULL, $default_state = NULL) { + $bundle_info_array = []; + if ($workflow) { + $bundle_info_array['workflow'] = $workflow; + } + $bundle_info = $this->prophesize(EntityTypeBundleInfoInterface::class); + $bundle_info->getBundleInfo("test_entity_type")->willReturn([$bundle => $bundle_info_array]); + + return $bundle_info->reveal(); } /** - * @dataProvider providerBoolean + * @dataProvider providerWorkflow * @covers ::isModeratedEntity */ - public function testIsModeratedEntity($status) { - $moderation_information = new ModerationInformation($this->setupModerationEntityManager($status), $this->getUser()); + public function testIsModeratedEntity($workflow, $default_state, $expected) { + $moderation_information = new ModerationInformation($this->getEntityTypeManager(), $this->setupModerationBundleInfo('test_bundle', $workflow, $default_state)); $entity_type = new ContentEntityType([ 'id' => 'test_entity_type', @@ -77,50 +77,32 @@ public function testIsModeratedEntity($status) { $entity->getEntityType()->willReturn($entity_type); $entity->bundle()->willReturn('test_bundle'); - $this->assertEquals($status, $moderation_information->isModeratedEntity($entity->reveal())); - } - - /** - * @covers ::isModeratedEntity - */ - public function testIsModeratedEntityForNonBundleEntityType() { - $entity_type = new ContentEntityType([ - 'id' => 'test_entity_type', - ]); - $entity = $this->prophesize(ContentEntityInterface::class); - $entity->getEntityType()->willReturn($entity_type); - $entity->bundle()->willReturn('test_entity_type'); - - $entity_storage = $this->prophesize(EntityStorageInterface::class); - $entity_type_manager = $this->getEntityTypeManager($entity_storage->reveal()); - $moderation_information = new ModerationInformation($entity_type_manager, $this->getUser()); - - $this->assertEquals(FALSE, $moderation_information->isModeratedEntity($entity->reveal())); + $this->assertEquals($expected, $moderation_information->isModeratedEntity($entity->reveal())); } /** - * @dataProvider providerBoolean + * @dataProvider providerWorkflow * @covers ::shouldModerateEntitiesOfBundle */ - public function testShouldModerateEntities($status) { + public function testShouldModerateEntities($workflow, $default_state, $expected) { $entity_type = new ContentEntityType([ 'id' => 'test_entity_type', 'bundle_entity_type' => 'entity_test_bundle', 'handlers' => ['moderation' => ModerationHandler::class], ]); - $moderation_information = new ModerationInformation($this->setupModerationEntityManager($status), $this->getUser()); + $moderation_information = new ModerationInformation($this->getEntityTypeManager(), $this->setupModerationBundleInfo('test_bundle', $workflow, $default_state)); - $this->assertEquals($status, $moderation_information->shouldModerateEntitiesOfBundle($entity_type, 'test_bundle')); + $this->assertEquals($expected, $moderation_information->shouldModerateEntitiesOfBundle($entity_type, 'test_bundle')); } /** * Data provider for several tests. */ - public function providerBoolean() { + public function providerWorkflow() { return [ - [FALSE], - [TRUE], + [NULL, NULL, FALSE], + ['workflow', 'draft', TRUE], ]; } diff --git a/core/modules/content_moderation/tests/src/Unit/StateTransitionValidationTest.php b/core/modules/content_moderation/tests/src/Unit/StateTransitionValidationTest.php index b057478..9dbea3c 100644 --- a/core/modules/content_moderation/tests/src/Unit/StateTransitionValidationTest.php +++ b/core/modules/content_moderation/tests/src/Unit/StateTransitionValidationTest.php @@ -2,13 +2,11 @@ namespace Drupal\Tests\content_moderation\Unit; -use Drupal\Core\Entity\EntityStorageInterface; -use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Entity\Query\QueryFactory; +use Drupal\content_moderation\ModerationInformationInterface; +use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Session\AccountInterface; -use Drupal\content_moderation\ModerationStateInterface; -use Drupal\content_moderation\ModerationStateTransitionInterface; use Drupal\content_moderation\StateTransitionValidation; +use Drupal\workflow\Entity\Workflow; use Prophecy\Argument; /** @@ -18,216 +16,6 @@ class StateTransitionValidationTest extends \PHPUnit_Framework_TestCase { /** - * Builds a mock storage object for Transitions. - * - * @return EntityStorageInterface - * The mocked storage object for Transitions. - */ - protected function setupTransitionStorage() { - $entity_storage = $this->prophesize(EntityStorageInterface::class); - - $list = $this->setupTransitionEntityList(); - $entity_storage->loadMultiple()->willReturn($list); - $entity_storage->loadMultiple(Argument::type('array'))->will(function ($args) use ($list) { - $keys = $args[0]; - if (empty($keys)) { - return $list; - } - - $return = array_map(function($key) use ($list) { - return $list[$key]; - }, $keys); - - return $return; - }); - return $entity_storage->reveal(); - } - - /** - * Builds an array of mocked Transition objects. - * - * @return ModerationStateTransitionInterface[] - * An array of mocked Transition objects. - */ - protected function setupTransitionEntityList() { - $transition = $this->prophesize(ModerationStateTransitionInterface::class); - $transition->id()->willReturn('draft__needs_review'); - $transition->getFromState()->willReturn('draft'); - $transition->getToState()->willReturn('needs_review'); - $list[$transition->reveal()->id()] = $transition->reveal(); - - $transition = $this->prophesize(ModerationStateTransitionInterface::class); - $transition->id()->willReturn('needs_review__staging'); - $transition->getFromState()->willReturn('needs_review'); - $transition->getToState()->willReturn('staging'); - $list[$transition->reveal()->id()] = $transition->reveal(); - - $transition = $this->prophesize(ModerationStateTransitionInterface::class); - $transition->id()->willReturn('staging__published'); - $transition->getFromState()->willReturn('staging'); - $transition->getToState()->willReturn('published'); - $list[$transition->reveal()->id()] = $transition->reveal(); - - $transition = $this->prophesize(ModerationStateTransitionInterface::class); - $transition->id()->willReturn('needs_review__draft'); - $transition->getFromState()->willReturn('needs_review'); - $transition->getToState()->willReturn('draft'); - $list[$transition->reveal()->id()] = $transition->reveal(); - - $transition = $this->prophesize(ModerationStateTransitionInterface::class); - $transition->id()->willReturn('draft__draft'); - $transition->getFromState()->willReturn('draft'); - $transition->getToState()->willReturn('draft'); - $list[$transition->reveal()->id()] = $transition->reveal(); - - $transition = $this->prophesize(ModerationStateTransitionInterface::class); - $transition->id()->willReturn('needs_review__needs_review'); - $transition->getFromState()->willReturn('needs_review'); - $transition->getToState()->willReturn('needs_review'); - $list[$transition->reveal()->id()] = $transition->reveal(); - - $transition = $this->prophesize(ModerationStateTransitionInterface::class); - $transition->id()->willReturn('published__published'); - $transition->getFromState()->willReturn('published'); - $transition->getToState()->willReturn('published'); - $list[$transition->reveal()->id()] = $transition->reveal(); - - return $list; - } - - /** - * Builds a mock storage object for States. - * - * @return EntityStorageInterface - * The mocked storage object for States. - */ - protected function setupStateStorage() { - $entity_storage = $this->prophesize(EntityStorageInterface::class); - - $state = $this->prophesize(ModerationStateInterface::class); - $state->id()->willReturn('draft'); - $state->label()->willReturn('Draft'); - $state->isPublishedState()->willReturn(FALSE); - $state->isDefaultRevisionState()->willReturn(FALSE); - $states['draft'] = $state->reveal(); - - $state = $this->prophesize(ModerationStateInterface::class); - $state->id()->willReturn('needs_review'); - $state->label()->willReturn('Needs Review'); - $state->isPublishedState()->willReturn(FALSE); - $state->isDefaultRevisionState()->willReturn(FALSE); - $states['needs_review'] = $state->reveal(); - - $state = $this->prophesize(ModerationStateInterface::class); - $state->id()->willReturn('staging'); - $state->label()->willReturn('Staging'); - $state->isPublishedState()->willReturn(FALSE); - $state->isDefaultRevisionState()->willReturn(FALSE); - $states['staging'] = $state->reveal(); - - $state = $this->prophesize(ModerationStateInterface::class); - $state->id()->willReturn('published'); - $state->label()->willReturn('Published'); - $state->isPublishedState()->willReturn(TRUE); - $state->isDefaultRevisionState()->willReturn(TRUE); - $states['published'] = $state->reveal(); - - $state = $this->prophesize(ModerationStateInterface::class); - $state->id()->willReturn('archived'); - $state->label()->willReturn('Archived'); - $state->isPublishedState()->willReturn(TRUE); - $state->isDefaultRevisionState()->willReturn(TRUE); - $states['archived'] = $state->reveal(); - - $entity_storage->loadMultiple()->willReturn($states); - - foreach ($states as $id => $state) { - $entity_storage->load($id)->willReturn($state); - } - - return $entity_storage->reveal(); - } - - /** - * Builds a mocked Entity Type Manager. - * - * @return EntityTypeManagerInterface - * The mocked Entity Type Manager. - */ - protected function setupEntityTypeManager(EntityStorageInterface $storage) { - $entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); - $entityTypeManager->getStorage('moderation_state')->willReturn($storage); - $entityTypeManager->getStorage('moderation_state_transition')->willReturn($this->setupTransitionStorage()); - - return $entityTypeManager->reveal(); - } - - /** - * Builds a mocked query factory that does nothing. - * - * @return QueryFactory - * The mocked query factory that does nothing. - */ - protected function setupQueryFactory() { - $factory = $this->prophesize(QueryFactory::class); - - return $factory->reveal(); - } - - /** - * @covers ::isTransitionAllowed - * @covers ::calculatePossibleTransitions - * - * @dataProvider providerIsTransitionAllowedWithValidTransition - */ - public function testIsTransitionAllowedWithValidTransition($from_id, $to_id) { - $storage = $this->setupStateStorage(); - $state_transition_validation = new StateTransitionValidation($this->setupEntityTypeManager($storage), $this->setupQueryFactory()); - $this->assertTrue($state_transition_validation->isTransitionAllowed($storage->load($from_id), $storage->load($to_id))); - } - - /** - * Data provider for self::testIsTransitionAllowedWithValidTransition(). - */ - public function providerIsTransitionAllowedWithValidTransition() { - return [ - ['draft', 'draft'], - ['draft', 'needs_review'], - ['needs_review', 'needs_review'], - ['needs_review', 'staging'], - ['staging', 'published'], - ['needs_review', 'draft'], - ]; - } - - /** - * @covers ::isTransitionAllowed - * @covers ::calculatePossibleTransitions - * - * @dataProvider providerIsTransitionAllowedWithInValidTransition - */ - public function testIsTransitionAllowedWithInValidTransition($from_id, $to_id) { - $storage = $this->setupStateStorage(); - $state_transition_validation = new StateTransitionValidation($this->setupEntityTypeManager($storage), $this->setupQueryFactory()); - $this->assertFalse($state_transition_validation->isTransitionAllowed($storage->load($from_id), $storage->load($to_id))); - } - - /** - * Data provider for self::testIsTransitionAllowedWithInValidTransition(). - */ - public function providerIsTransitionAllowedWithInValidTransition() { - return [ - ['published', 'needs_review'], - ['published', 'staging'], - ['staging', 'needs_review'], - ['staging', 'staging'], - ['needs_review', 'published'], - ['published', 'archived'], - ['archived', 'published'], - ]; - } - - /** * Verifies user-aware transition validation. * * @param string $from_id @@ -250,10 +38,29 @@ public function testUserSensitiveValidTransitions($from_id, $to_id, $permission, $user->hasPermission($permission)->willReturn($allowed); $user->hasPermission(Argument::type('string'))->willReturn(FALSE); - $storage = $this->setupStateStorage(); - $validator = new Validator($this->setupEntityTypeManager($storage), $this->setupQueryFactory()); + $entity = $this->prophesize(ContentEntityInterface::class); + $entity = $entity->reveal(); + $entity->moderation_state = new \stdClass(); + $entity->moderation_state->value = $from_id; + + $validator = new StateTransitionValidation($this->setUpModerationInformation($entity)); + + $this->assertEquals($result, $validator->userMayTransition($entity, $to_id, $user->reveal())); + } - $this->assertEquals($result, $validator->userMayTransition($storage->load($from_id), $storage->load($to_id), $user->reveal())); + protected function setUpModerationInformation(ContentEntityInterface $entity) { + $workflow = new Workflow(['id' => 'process'], 'workflow'); + $workflow + ->addState('draft', 'draft') + ->addState('needs_review', 'needs_review') + ->addState('published', 'published') + ->addTransition('draft', 'draft') + ->addTransition('draft', 'needs_review') + ->addTransition('needs_review', 'published') + ->addTransition('published', 'published'); + $moderation_info = $this->prophesize(ModerationInformationInterface::class); + $moderation_info->getWorkFlowForEntity($entity)->willReturn($workflow); + return $moderation_info->reveal(); } /** @@ -261,37 +68,18 @@ public function testUserSensitiveValidTransitions($from_id, $to_id, $permission, */ public function userTransitionsProvider() { // The user has the right permission, so let it through. - $ret[] = ['draft', 'draft', 'use draft__draft transition', TRUE, TRUE]; + $ret[] = ['draft', 'draft', 'use process transition from draft to draft', TRUE, TRUE]; // The user doesn't have the right permission, block it. - $ret[] = ['draft', 'draft', 'use draft__draft transition', FALSE, FALSE]; + $ret[] = ['draft', 'draft', 'use process transition from draft to draft', FALSE, FALSE]; // The user has some other permission that doesn't matter. - $ret[] = ['draft', 'draft', 'use draft__needs_review transition', TRUE, FALSE]; + $ret[] = ['draft', 'draft', 'use process transition from draft to needs_review', TRUE, FALSE]; // The user has permission, but the transition isn't allowed anyway. - $ret[] = ['published', 'needs_review', 'use published__needs_review transition', TRUE, FALSE]; + $ret[] = ['published', 'needs_review', 'use process transition from published to needs_review', TRUE, FALSE]; return $ret; } } - -/** - * Testable subclass for selected tests. - * - * EntityQuery is beyond untestable, so we have to subclass and override the - * method that uses it. - */ -class Validator extends StateTransitionValidation { - - /** - * {@inheritdoc} - */ - protected function getTransitionFromStates(ModerationStateInterface $from, ModerationStateInterface $to) { - if ($from->id() === 'draft' && $to->id() === 'draft') { - return $this->transitionStorage()->loadMultiple(['draft__draft'])[0]; - } - } - -} diff --git a/core/modules/workflow/config/schema/workflow.schema.yml b/core/modules/workflow/config/schema/workflow.schema.yml new file mode 100644 index 0000000..3972eab --- /dev/null +++ b/core/modules/workflow/config/schema/workflow.schema.yml @@ -0,0 +1,47 @@ +workflow.workflow.*: + type: config_entity + label: 'Workflow' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Label' + states: + type: sequence + label: 'States' + sequence: + type: mapping + label: 'State' + mapping: + label: + type: label + label: 'Label' + published: + type: boolean + label: 'Is published' + default_revision: + type: boolean + label: 'Is default revision' + weight: + type: integer + label: 'Weight' + transitions: + type: sequence + label: 'Transitions' + sequence: + type: sequence + label: 'Transition from state to state' + sequence: + type: label + label: 'Transition label' + applies: + type: sequence + label: 'Entity types' + sequence: + type: sequence + label: 'Bundles' + sequence: + type: string + label: 'Default moderation state' diff --git a/core/modules/workflow/src/Entity/Workflow.php b/core/modules/workflow/src/Entity/Workflow.php new file mode 100644 index 0000000..c104381 --- /dev/null +++ b/core/modules/workflow/src/Entity/Workflow.php @@ -0,0 +1,292 @@ +applies[$entity_type_id][$bundle_id]); + } + + public function applyToEntityTypeAndBundle($entity_type_id, $bundle_id) { + $this->applies[$entity_type_id][$bundle_id] = ''; + return $this; + } + + public function removeEntityTypeAndBundle($entity_type_id, $bundle_id) { + unset($this->applies[$entity_type_id][$bundle_id]); + return $this; + } + + public function getInitialState() { + $ordered_states = $this->getStates(); + return key($ordered_states); + } + + public function getStateOptions() { + return array_map(function (array $state) { + return $state['label']; + }, $this->getStates()); + } + + public function canTranstion($from_state, $to_state) { + return isset($this->transitions[$from_state][$to_state]); + } + + public function getPossibleStates($from_state) { + $to_states = []; + if (isset($this->transitions[$from_state])) { + $to_states = array_keys($this->transitions[$from_state]); + } + return $to_states; + } + + public function getApplies() { + return $this->applies; + } + + public function getStateLabel($state) { + if (!isset($this->states[$state]['label'])) { + // @todo + throw new \InvalidArgumentException(); + } + return $this->states[$state]['label']; + } + + /** + * {@inheritdoc} + */ + public function getStates() { + $values = $this->states; + uasort($values, function ($a, $b) { + return SortArray::sortByKeyInt($a, $b, 'weight'); + }); + return $values; + } + + /** + * {@inheritdoc} + */ + public function hasState($state) { + return isset($this->states[$state]); + } + + + public function isDefaultRevisionState($state) { + if (!isset($this->states[$state])) { + // @todo + throw new \InvalidArgumentException(); + } + return $this->states[$state]['default_revision']; + } + + public function isPublishedState($state) { + if (!isset($this->states[$state])) { + // @todo + throw new \InvalidArgumentException(); + } + return $this->states[$state]['published']; + } + + public function setStateWeight($state, $weight) { + if (!isset($this->states[$state])) { + // @todo + throw new \InvalidArgumentException(); + } + $this->states[$state]['weight'] = $weight; + return $this; + } + + public function setStateLabel($state, $label) { + if (!isset($this->states[$state])) { + // @todo + throw new \InvalidArgumentException(); + } + $this->states[$state]['label'] = $label; + return $this; + } + + public function setStatePublished($state, $published) { + if (!isset($this->states[$state])) { + // @todo + throw new \InvalidArgumentException(); + } + $this->states[$state]['published'] = $published; + return $this; + } + + public function setStateDefaultRevision($state, $default_revision) { + if (!isset($this->states[$state])) { + // @todo + throw new \InvalidArgumentException(); + } + $this->states[$state]['default_revision'] = $default_revision; + return $this; + } + + public function addState($state, $label, $published = FALSE, $default_revision = FALSE) { + if (isset($this->states[$state])) { + // @todo + throw new \InvalidArgumentException(); + } + // Always add to the end. + $weight = array_reduce($this->states, function ($carry, $state_info) { + return $carry = max($carry, $state_info['weight'] + 1); + }, 0); + $this->states[$state] = [ + 'label' => $label, + 'published' => $published, + 'default_revision' => $default_revision, + 'weight' => $weight, + ]; + // By default allow same state transitions as this is allowed in most common + // workflows. + $this->addTransition($state, $state); + return $this; + } + + public function deleteState($state) { + if (!isset($this->states[$state])) { + // @todo + throw new \InvalidArgumentException(); + } + unset($this->states[$state]); + return $this; + } + + public function getTransitionLabel($from_state, $to_state) { + if (!$this->canTranstion($from_state, $to_state)) { + // @todo + throw new \InvalidArgumentException(); + } + return $this->transitions[$from_state][$to_state]; + } + + public function addTransition($from_state, $to_state) { + if (!$this->canTranstion($from_state, $to_state)) { + // @todo default label? + $this->transitions[$from_state][$to_state] = ''; + } + return $this; + } + + public function setTransitionLabel($from_state, $to_state, $label) { + if (!$this->canTranstion($from_state, $to_state)) { + throw new \InvalidArgumentException(); + } + $this->transitions[$from_state][$to_state] = $label; + return $this; + } + + public function deleteTransition($from_state, $to_state) { + if (!$this->canTranstion($from_state, $to_state)) { + throw new \InvalidArgumentException(); + } + unset($this->transitions[$from_state][$to_state]); + return $this; + } + +} diff --git a/core/modules/workflow/src/Form/WorkflowAddForm.php b/core/modules/workflow/src/Form/WorkflowAddForm.php new file mode 100644 index 0000000..c9087bf --- /dev/null +++ b/core/modules/workflow/src/Form/WorkflowAddForm.php @@ -0,0 +1,69 @@ +entity; + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => $workflow->label(), + '#description' => $this->t('Label for the Workflow.'), + '#required' => TRUE, + ]; + + $form['id'] = [ + '#type' => 'machine_name', + '#default_value' => $workflow->id(), + '#machine_name' => [ + 'exists' => [Workflow::class, 'load'], + ], + '#disabled' => !$workflow->isNew(), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + /* @var \Drupal\workflow\WorkflowInterface $workflow */ + $workflow = $this->entity; + $workflow->save(); + drupal_set_message($this->t('Created the %label Worflow.', [ + '%label' => $workflow->label(), + ])); + $form_state->setRedirectUrl($workflow->toUrl('collection')); + } + + /** + * {@inheritdoc} + */ + protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) { + // This form can only set the workflow's ID, label and the weights for each + // state. + /** @var \Drupal\workflow\WorkflowInterface $entity */ + $values = $form_state->getValues(); + $entity->set('label', $values['label']); + $entity->set('id', $values['id']); + } + +} diff --git a/core/modules/content_moderation/src/Form/ModerationStateDeleteForm.php b/core/modules/workflow/src/Form/WorkflowDeleteForm.php similarity index 74% rename from core/modules/content_moderation/src/Form/ModerationStateDeleteForm.php rename to core/modules/workflow/src/Form/WorkflowDeleteForm.php index 43e2b36..97c3220 100644 --- a/core/modules/content_moderation/src/Form/ModerationStateDeleteForm.php +++ b/core/modules/workflow/src/Form/WorkflowDeleteForm.php @@ -1,15 +1,15 @@ entity->delete(); drupal_set_message($this->t( - 'Moderation state %label deleted.', + 'Workflow %label deleted.', ['%label' => $this->entity->label()] )); diff --git a/core/modules/workflow/src/Form/WorkflowEditForm.php b/core/modules/workflow/src/Form/WorkflowEditForm.php new file mode 100644 index 0000000..08133ca --- /dev/null +++ b/core/modules/workflow/src/Form/WorkflowEditForm.php @@ -0,0 +1,127 @@ +entity; + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => $workflow->label(), + '#description' => $this->t('Label for the Workflow.'), + '#required' => TRUE, + ]; + + $form['id'] = [ + '#type' => 'machine_name', + '#default_value' => $workflow->id(), + '#machine_name' => [ + 'exists' => [Workflow::class, 'load'], + ], + '#disabled' => !$workflow->isNew(), + ]; + + $header = [ + 'state' => $this->t('State'), + 'weight' => $this->t('Weight'), + 'transitions' => $this->t('Allowed transitions'), + 'operations' => $this->t('Operations') + ]; + $form['states'] = array( + '#type' => 'table', + '#header' => $header, + '#empty' => $this->t('There are no states yet.'), + '#tabledrag' => array( + array( + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => 'state-weight', + ), + ), + ); + foreach ($workflow->getStates() as $state_id => $state) { + $transitions = array_map(function ($to_state) use ($workflow) { + return $workflow->getStateLabel($to_state); + }, $workflow->getPossibleStates($state_id)); + + $links['edit'] = [ + 'title' => $this->t('Edit'), + 'url' => Url::fromRoute('entity.workflow.edit_state_form', ['workflow' => $workflow->id(), 'workflow_state' => $state_id]), + ]; + $links['delete'] = array( + 'title' => t('Delete'), + 'url' => Url::fromRoute('entity.workflow.delete_state_form', ['workflow' => $workflow->id(), 'workflow_state' => $state_id]), + ); + $form['states'][$state_id] = [ + '#attributes' => ['class' => ['draggable']], + 'state' => ['#markup' => $state['label']], + '#weight' => $state['weight'], + 'weight' => [ + '#type' => 'weight', + '#title' => t('Weight for @title', ['@title' => $state['label']]), + '#title_display' => 'invisible', + '#default_value' => $state['weight'], + '#attributes' => ['class' => ['state-weight']], + ], + 'transitions' => [ + '#theme' => 'item_list', + '#items' => $transitions + ], + 'operations' => [ + '#type' => 'operations', + '#links' => $links, + ], + ]; + } + $form['state_add'] = array( + '#markup' => $workflow->toLink($this->t('Add a new state'), 'add-state-form')->toString(), + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + /* @var \Drupal\workflow\WorkflowInterface $workflow */ + $workflow = $this->entity; + $workflow->save(); + drupal_set_message($this->t('Saved the %label Workflow.', ['%label' => $workflow->label()])); + $form_state->setRedirectUrl($workflow->toUrl('collection')); + } + + /** + * {@inheritdoc} + */ + protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) { + // This form can only set the workflow's ID, label and the weights for each + // state. + /** @var \Drupal\workflow\WorkflowInterface $entity */ + $values = $form_state->getValues(); + $entity->set('label', $values['label']); + $entity->set('id', $values['id']); + foreach ($values['states'] as $state_id => $state_values) { + $entity->setStateWeight($state_id, $state_values['weight']); + } + } + +} diff --git a/core/modules/workflow/src/Form/WorkflowStateDeleteForm.php b/core/modules/workflow/src/Form/WorkflowStateDeleteForm.php new file mode 100644 index 0000000..c2d4164 --- /dev/null +++ b/core/modules/workflow/src/Form/WorkflowStateDeleteForm.php @@ -0,0 +1,86 @@ +t('Are you sure you want to delete %state from %workflow?', array('%state' => $this->workflow->getStateLabel($this->stateId), '%workflow' => $this->workflow->label())); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return $this->workflow->toUrl(); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + * + * @param string $ban_id + * The IP address record ID to unban. + */ + public function buildForm(array $form, FormStateInterface $form_state, WorkflowInterface $workflow = NULL, $workflow_state = NULL) { + if (!$workflow->hasState($workflow_state)) { + throw new NotFoundHttpException(); + } + $this->workflow = $workflow; + $this->stateId = $workflow_state; + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + + $workflow_label = $this->workflow->getStateLabel($this->stateId); + $this->workflow + ->deleteState($this->stateId) + ->save(); + + drupal_set_message($this->t( + 'State %label deleted.', + ['%label' => $workflow_label] + )); + + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/core/modules/workflow/src/Form/WorkflowStateForm.php b/core/modules/workflow/src/Form/WorkflowStateForm.php new file mode 100644 index 0000000..86e7a94 --- /dev/null +++ b/core/modules/workflow/src/Form/WorkflowStateForm.php @@ -0,0 +1,221 @@ +stateId = $workflow_state; + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + + /* @var \Drupal\workflow\WorkflowInterface $workflow */ + $workflow = $this->getEntity(); + $form['label'] = array( + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => isset($this->stateId) ? $workflow->getStateLabel($this->stateId) : '', + '#description' => $this->t('Label for the state.'), + '#required' => TRUE, + ); + + $form['id'] = array( + '#type' => 'machine_name', + '#default_value' => $this->stateId, + '#machine_name' => array( + 'exists' => [$this, 'exists'], + ), + '#disabled' => isset($this->stateId), + ); + + $form['published'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Published'), + '#description' => $this->t('When content reaches this state it should be published.'), + '#default_value' => isset($this->stateId) ? $workflow->isPublishedState($this->stateId) : FALSE, + ]; + + $form['default_revision'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Default revision'), + '#description' => $this->t('When content reaches this state it should be made the default revision; this is implied for published states.'), + '#default_value' => isset($this->stateId) ? $workflow->isDefaultRevisionState($this->stateId) : FALSE, + // @todo Add form #state to force "make default" on when "published" is + // on for a state. + // @see https://www.drupal.org/node/2645614 + ]; + + $form['transitions'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Allowed transitions'), + '#options' => $workflow->getStateOptions(), + '#default_value' => isset($this->stateId) ? $workflow->getPossibleStates($this->stateId) : [], + ]; + $header = [ + 'allowed' => $this->t('Allowed'), + 'state' => $this->t('To'), + 'label' => $this->t('Label') + ]; + $form['transitions'] = [ + '#type' => 'table', + '#header' => $header, + '#empty' => $this->t('There are no states yet.'), + ]; + foreach (array_keys($workflow->getStates()) as $to_state_id) { + $can_transition = $workflow->canTranstion($this->stateId, $to_state_id); + $form['transitions'][$to_state_id] = [ + 'allowed' => [ + '#type' => 'checkbox', + '#default_value' => isset($this->stateId) ? $can_transition : FALSE, + ], + 'state' => [ + '#markup' => $workflow->getStateLabel($to_state_id), + ], + 'label' => [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#title_display' => 'invisible', + '#maxlength' => 255, + '#default_value' => isset($this->stateId) && $can_transition ? $workflow->getTransitionLabel($this->stateId, $to_state_id) : '', + '#description' => $this->t('Label for the transition.'), + '#description_display' => 'invisible', + ], + + ]; + } + + return $form; + } + + /** + * Determines if the workflow state already exists. + * + * @param string $state_id + * The workflow state ID. + * + * @return bool + * TRUE if the workflow state exists, FALSE otherwise. + */ + public function exists($state_id) { + $original_workflow = \Drupal::entityTypeManager()->getStorage('workflow')->loadUnchanged($this->getEntity()->id()); + return $original_workflow->hasState($state_id); + } + + /** + * Copies top-level form values to entity properties + * + * This form can only change values for a state, which is part of workflow. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity the current form should operate upon. + * @param array $form + * A nested array of form elements comprising the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) { + /** @var \Drupal\workflow\WorkflowInterface $entity */ + $values = $form_state->getValues(); + $values['published'] = (bool) $values['published']; + $values['default_revision'] = (bool) $values['default_revision']; + if (!$entity->hasState($values['id'])) { + $entity->addState($values['id'], $values['label'], $values['published'], $values['default_revision']); + } + else { + $entity->setStateLabel($values['id'], $values['label']); + $entity->setStatePublished($values['id'], $values['published']); + $entity->setStateDefaultRevision($values['id'], $values['default_revision']); + } + if (!empty($values['transitions'])) { + foreach ($values['transitions'] as $to_state_id => $transition) { + $can_transition = $entity->canTranstion($values['id'], $to_state_id); + if ($transition['allowed']) { + if (!$can_transition) { + $entity->addTransition($values['id'], $to_state_id); + } + $entity->setTransitionLabel($values['id'], $to_state_id, $transition['label']); + } + elseif ($can_transition) { + $entity->deleteTransition($values['id'], $to_state_id); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + /** @var \Drupal\workflow\WorkflowInterface $workflow */ + $workflow = $this->entity; + // + $workflow->save(); + + if (isset($this->stateId)) { + drupal_set_message($this->t('Saved %label state.', [ + '%label' => $workflow->getStateLabel($this->stateId), + ])); + } + else { + drupal_set_message($this->t('Created %label state.', [ + '%label' => $workflow->getStateLabel($form_state->getValue('id')), + ])); + } + $form_state->setRedirectUrl($workflow->toUrl('edit-form')); + } + + /** + * {@inheritdoc} + */ + protected function actions(array $form, FormStateInterface $form_state) { + $actions['submit'] = array( + '#type' => 'submit', + '#value' => $this->t('Save'), + '#submit' => array('::submitForm', '::save'), + ); + + if (isset($this->stateId)) { + $actions['delete'] = array( + '#type' => 'link', + '#title' => $this->t('Delete'), + // Deleting a state is editting a workflow. + '#access' => $this->entity->access('edit'), + '#attributes' => array( + 'class' => array('button', 'button--danger'), + ), + '#url' => Url::fromRoute('entity.workflow.delete_state_form', [ + 'workflow' => $this->entity->id(), + 'workflow_state' => $this->stateId + ]) + ); + } + + return $actions; + } + +} diff --git a/core/modules/workflow/src/Tests/WorkflowUiTest.php b/core/modules/workflow/src/Tests/WorkflowUiTest.php new file mode 100644 index 0000000..d2ad617 --- /dev/null +++ b/core/modules/workflow/src/Tests/WorkflowUiTest.php @@ -0,0 +1,66 @@ + 'typical']); + $workflow + ->addState('published', 'Published') + ->save(); + + $paths = [ + 'admin/config/workflow/workflows', + 'admin/config/workflow/workflows/add', + 'admin/config/workflow/workflows/manage/typical', + 'admin/config/workflow/workflows/manage/typical/delete', + 'admin/config/workflow/workflows/manage/typical/add_state', + 'admin/config/workflow/workflows/manage/typical/state/published', + 'admin/config/workflow/workflows/manage/typical/state/published/delete', + ]; + + foreach ($paths as $path) { + $this->drupalGet($path); + // No access. + $this->assertResponse(403); + } + $this->drupalLogin($this->createUser(['administer workflows'])); + foreach ($paths as $path) { + $this->drupalGet($path); + // User has access. + $this->assertResponse(200); + } + } + + /** + * Tests the creation of a workflow through the UI. + */ + public function testWorkflowCreation() { + $this->drupalLogin($this->createUser(['access administration pages', 'administer workflows'])); + $this->drupalGet('admin/config/workflow'); + $this->assertLinkByHref('admin/config/workflow/workflows'); + $this->clickLink('Workflows'); + $this->assertText('There is no Workflow yet.'); + } + +} diff --git a/core/modules/content_moderation/src/ModerationStateAccessControlHandler.php b/core/modules/workflow/src/WorkflowAccessControlHandler.php similarity index 73% rename from core/modules/content_moderation/src/ModerationStateAccessControlHandler.php rename to core/modules/workflow/src/WorkflowAccessControlHandler.php index b2c86d7..b88717a 100644 --- a/core/modules/content_moderation/src/ModerationStateAccessControlHandler.php +++ b/core/modules/workflow/src/WorkflowAccessControlHandler.php @@ -1,6 +1,6 @@ orIf($admin_access); + return AccessResult::allowedIfHasPermission($account, 'view content moderation')->orIf($admin_access); } return $admin_access; diff --git a/core/modules/workflow/src/WorkflowInterface.php b/core/modules/workflow/src/WorkflowInterface.php new file mode 100644 index 0000000..78ad92d --- /dev/null +++ b/core/modules/workflow/src/WorkflowInterface.php @@ -0,0 +1,52 @@ +t('Workflow'); + $header['states'] = $this->t('States'); + + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + /** @var \Drupal\workflow\WorkflowInterface $entity */ + $row['label'] = $entity->label(); + + $items = array_map(function ($state) { + return $state['label']; + }, $entity->getStates()); + $row['states']['data'] = [ + '#theme' => 'item_list', + '#context' => ['list_style' => 'comma-list'], + '#items' => $items, + ]; + + return $row + parent::buildRow($entity); + } + +} diff --git a/core/modules/workflow/workflow.info.yml b/core/modules/workflow/workflow.info.yml new file mode 100644 index 0000000..e1859ef --- /dev/null +++ b/core/modules/workflow/workflow.info.yml @@ -0,0 +1,7 @@ +name: 'Workflow' +type: module +description: 'Provides workflows and a UI to create them. This module can be used with the Content moderation module to add highly cusomisable workflows to content.' +version: VERSION +core: 8.x +package: Core (Experimental) +configure: workflow.overview diff --git a/core/modules/workflow/workflow.links.action.yml b/core/modules/workflow/workflow.links.action.yml new file mode 100644 index 0000000..e3a80f7 --- /dev/null +++ b/core/modules/workflow/workflow.links.action.yml @@ -0,0 +1,5 @@ +entity.workflow.add_form: + route_name: 'entity.workflow.add_form' + title: 'Add workflow' + appears_on: + - entity.workflow.collection diff --git a/core/modules/workflow/workflow.links.menu.yml b/core/modules/workflow/workflow.links.menu.yml new file mode 100644 index 0000000..a6ac512 --- /dev/null +++ b/core/modules/workflow/workflow.links.menu.yml @@ -0,0 +1,7 @@ +# Workflow menu items definition +entity.workflow.collection: + title: 'Workflows' + route_name: entity.workflow.collection + description: 'Configure workflows.' + parent: system.admin_config_workflow + diff --git a/core/modules/workflow/workflow.module b/core/modules/workflow/workflow.module new file mode 100644 index 0000000..031aa0e --- /dev/null +++ b/core/modules/workflow/workflow.module @@ -0,0 +1,22 @@ +' . t('About') . ''; + $output .= '

' . t('The Workflow module provides a UI for creating workflows content. This lets site admins define workflows and their states, and then define transitions between those states. For more information, see the online documentation for the Workflow module.', [':workflow' => 'https://www.drupal.org/documentation/modules/workflow']) . '

'; + return $output; + } +} diff --git a/core/modules/workflow/workflow.permissions.yml b/core/modules/workflow/workflow.permissions.yml new file mode 100644 index 0000000..88573b6 --- /dev/null +++ b/core/modules/workflow/workflow.permissions.yml @@ -0,0 +1,4 @@ +'administer workflows': + title: 'Administer workflows' + description: 'Create and edit workflows.' + 'restrict access': TRUE diff --git a/core/modules/workflow/workflow.routing.yml b/core/modules/workflow/workflow.routing.yml new file mode 100644 index 0000000..0b5e549 --- /dev/null +++ b/core/modules/workflow/workflow.routing.yml @@ -0,0 +1,23 @@ +entity.workflow.add_state_form: + path: '/admin/config/workflow/workflows/manage/{workflow}/add_state' + defaults: + _entity_form: 'workflow.add-state' + _title: 'Add state' + requirements: + _entity_access: 'workflow.edit' + +entity.workflow.edit_state_form: + path: '/admin/config/workflow/workflows/manage/{workflow}/state/{workflow_state}' + defaults: + _entity_form: 'workflow.edit-state' + _title: 'Edit state' + requirements: + _entity_access: 'workflow.edit' + +entity.workflow.delete_state_form: + path: '/admin/config/workflow/workflows/manage/{workflow}/state/{workflow_state}/delete' + defaults: + _form: '\Drupal\workflow\Form\WorkflowStateDeleteForm' + _title: 'Delete state' + requirements: + _entity_access: 'workflow.edit' \ No newline at end of file diff --git a/core/modules/workflow/workflow.services.yml b/core/modules/workflow/workflow.services.yml new file mode 100644 index 0000000..4c71203 --- /dev/null +++ b/core/modules/workflow/workflow.services.yml @@ -0,0 +1 @@ +services: { }