diff --git a/core/composer.json b/core/composer.json index 53ab8fa..7cfb286 100644 --- a/core/composer.json +++ b/core/composer.json @@ -140,7 +140,8 @@ "drupal/update": "self.version", "drupal/user": "self.version", "drupal/views": "self.version", - "drupal/views_ui": "self.version" + "drupal/views_ui": "self.version", + "drupal/workflow": "self.version" }, "minimum-stability": "dev", "prefer-stable": true, 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..e454aa0 --- /dev/null +++ b/core/modules/content_moderation/config/install/workflow.workflow.typical.yml @@ -0,0 +1,41 @@ +langcode: en +status: true +dependencies: + module: + - content_moderation +id: typical +label: 'Typical workflow' +states: + archived: + label: Archived + weight: 5 + draft: + label: Draft + weight: -5 + published: + label: Published + 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 +type: content_moderation +type_settings: + states: + archived: + published: false + default_revision: true + draft: + published: false + default_revision: false + published: + published: true + default_revision: true + entity_types: { } 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..b791b67 100644 --- a/core/modules/content_moderation/config/schema/content_moderation.schema.yml +++ b/core/modules/content_moderation/config/schema/content_moderation.schema.yml @@ -1,79 +1,33 @@ -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' +views.filter.latest_revision: + type: views_filter + label: 'Latest revision' mapping: - id: - type: string - label: 'ID' - label: - type: label - label: 'Label' - stateFrom: - type: string - label: 'From state' - stateTo: + value: type: string - label: 'To state' - weight: - type: integer - label: 'Weight' + label: 'Value' -node.type.*.third_party.content_moderation: +workflow.type_settings.content_moderation: type: mapping - label: 'Enable moderation states for this node type' mapping: - enabled: - type: boolean - label: 'Moderation states enabled' - allowed_moderation_states: + states: type: sequence + label: 'Additional state configuration for content moderation' 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: mapping + label: 'States' + mapping: + published: + type: boolean + label: 'Is published' + default_revision: + type: boolean + label: 'Is default revision' + entity_types: type: sequence + label: 'Entity types' 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' - mapping: - value: - type: string - label: 'Value' + type: sequence + label: 'Bundles' + sequence: + type: string + label: 'Bundle ID' 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..6c10e5a 100644 --- a/core/modules/content_moderation/content_moderation.module +++ b/core/modules/content_moderation/content_moderation.module @@ -11,6 +11,7 @@ use Drupal\content_moderation\Plugin\Action\ModerationOptOutPublishNode; use Drupal\content_moderation\Plugin\Action\ModerationOptOutUnpublishNode; use Drupal\content_moderation\Plugin\Menu\EditTab; +use Drupal\content_moderation\Plugin\WorkflowType\ContentModeration; use Drupal\Core\Access\AccessResult; use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Entity\EntityInterface; @@ -21,6 +22,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 +33,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 +180,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 +222,22 @@ 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) { + /** @var \Drupal\workflow\WorkflowInterface $workflow */ + foreach (Workflow::loadMultiple() as $workflow) { + $plugin = $workflow->getTypePlugin(); + if ($plugin instanceof ContentModeration) { + foreach ($plugin->getEntityTypes() as $entity_type_id) { + foreach ($plugin->getBundlesForEntityType($entity_type_id) as $bundle_id) { + if (isset($bundles[$entity_type_id][$bundle_id])) { + $bundles[$entity_type_id][$bundle_id]['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/ContentModerationState.php b/core/modules/content_moderation/src/ContentModerationState.php new file mode 100644 index 0000000..0a7487e --- /dev/null +++ b/core/modules/content_moderation/src/ContentModerationState.php @@ -0,0 +1,120 @@ +state = $state; + $this->published = $published; + $this->defaultRevision = $default_revision; + } + + /** + * Determines if entities should be published if in this state. + * + * @return bool + */ + public function isPublished() { + return $this->published; + } + + /** + * Determines if entities should be the default revision if in this state. + * + * @return bool + */ + public function isDefaultRevision() { + return $this->defaultRevision; + } + + /** + * {@inheritdoc} + */ + public function id() { + return $this->state->id(); + } + + /** + * {@inheritdoc} + */ + public function label() { + return $this->state->label(); + } + + /** + * {@inheritdoc} + */ + public function weight() { + return $this->state->weight(); + } + + /** + * {@inheritdoc} + */ + public function getNextStates() { + return $this->state->getNextStates(); + } + + /** + * {@inheritdoc} + */ + public function canTransitionTo($to_state_id) { + return $this->state->canTransitionTo($to_state_id); + } + + /** + * {@inheritdoc} + */ + public function getTransition($to_state_id) { + return $this->state->getTransition($to_state_id); + } + + /** + * {@inheritdoc} + */ + public function getTransitions() { + return $this->state->getTransitions(); + } + +} 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..ff2c2d3 100644 --- a/core/modules/content_moderation/src/EntityOperations.php +++ b/core/modules/content_moderation/src/EntityOperations.php @@ -2,14 +2,16 @@ namespace Drupal\content_moderation; -use Drupal\content_moderation\Entity\ContentModerationState; +use Drupal\content_moderation\Entity\ContentModerationState as ContentModerationStateEntity; 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->getState($entity->moderation_state->value)->isPublished(); // 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->getState($entity->moderation_state->value)->isDefaultRevision() + || !$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()->id(); } - // @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(); @@ -184,7 +190,7 @@ protected function updateOrCreateFromEntity(EntityInterface $entity) { // Create the ContentModerationState entity for the inserted entity. $content_moderation_state->set('content_entity_revision_id', $entity_revision_id); $content_moderation_state->set('moderation_state', $moderation_state); - ContentModerationState::updateOrCreateFromEntity($content_moderation_state); + ContentModerationStateEntity::updateOrCreateFromEntity($content_moderation_state); } /** @@ -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->getState($default_revision->moderation_state->value)->isPublished(); } } 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..fa97c95 100644 --- a/core/modules/content_moderation/src/Form/BundleModerationConfigurationForm.php +++ b/core/modules/content_moderation/src/Form/BundleModerationConfigurationForm.php @@ -2,12 +2,11 @@ namespace Drupal\content_moderation\Form; -use Drupal\Core\Config\Entity\ThirdPartySettingsInterface; +use Drupal\content_moderation\Plugin\WorkflowType\ContentModeration; +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,128 +49,64 @@ 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() && $workflow->getTypePlugin() instanceof ContentModeration; })); - $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) { + $plugin = $workflow->getTypePlugin(); + if ($plugin instanceof ContentModeration && $plugin->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['original_workflow'] = [ + '#type' => 'hidden', + '#value' => $selected_workflow, ]; - // 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['bundle'] = [ + '#type' => 'hidden', + '#value' => $bundle->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_type'] = [ + '#type' => 'hidden', + '#value' => $bundle_of_entity_type->id(), ]; - $form['#entity_builders'][] = [$this, 'formBuilderCallback']; - - 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')); + // 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], + // ], + // ], + ]; } - } - /** - * {@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.')); - } - } + return parent::form($form, $form_state); } /** @@ -180,16 +115,38 @@ public function validateForm(array &$form, FormStateInterface $form_state) { 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(); + drupal_set_message($this->t('Your settings have been saved.')); + } - $this->entityTypeManager->getHandler($bundle->getEntityType()->getBundleOf(), 'moderation')->onBundleModerationConfigurationFormSubmit($bundle); + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $entity_type_id = $form_state->getValue('entity_type'); + $bundle_id = $form_state->getValue('bundle'); + $new_workflow_id = $form_state->getValue('workflow'); + $original_workflow_id = $form_state->getValue('original_workflow'); + if ($new_workflow_id === $original_workflow_id) { + // Nothing to do. + return; } - - parent::submitForm($form, $form_state); - - drupal_set_message($this->t('Your settings have been saved.')); + if ($original_workflow_id) { + /* @var \Drupal\workflow\WorkflowInterface $workflow */ + $workflow = $this->entityTypeManager->getStorage('workflow')->load($original_workflow_id); + $workflow->getTypePlugin()->removeEntityTypeAndBundle($entity_type_id, $bundle_id); + $workflow->save(); + } + if ($new_workflow_id) { + /* @var \Drupal\workflow\WorkflowInterface $workflow */ + $workflow = $this->entityTypeManager->getStorage('workflow')->load($new_workflow_id); + $workflow->getTypePlugin()->addEntityTypeAndBundle($entity_type_id, $bundle_id); + $workflow->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..4c62004 100644 --- a/core/modules/content_moderation/src/Form/EntityModerationForm.php +++ b/core/modules/content_moderation/src/Form/EntityModerationForm.php @@ -3,12 +3,11 @@ 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 Drupal\workflow\Transition; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -31,26 +30,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 +48,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 +63,21 @@ 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); + /** @var \Drupal\Workflow\Transition[] $transitions */ $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 $transition) use ($current_state) { + return $transition->to()->id() != $current_state; }); $target_states = []; - /** @var ModerationStateTransition $transition */ + foreach ($transitions as $transition) { - $target_states[$transition->getToState()] = $transition->label(); + $target_states[$transition->to()->id()] = $transition->to()->label(); } if (!count($target_states)) { @@ -99,7 +88,7 @@ public function buildForm(array $form, FormStateInterface $form_state, ContentEn $form['current'] = [ '#type' => 'item', '#title' => $this->t('Status'), - '#markup' => $current_state->label(), + '#markup' => $workflow->getState($current_state)->label(), ]; } @@ -139,21 +128,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); - + $new_state = $this->moderationInfo->getWorkFlowForEntity($entity)->getState($new_state); // The page we're on likely won't be visible if we just set the entity to // the default state, as we hide that latest-revision tab if there is no // forward revision. Redirect to the canonical URL instead, since that will // still exist. - if ($state->isDefaultRevisionState()) { + if ($new_state->isDefaultRevision()) { $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..7e7dba7 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->getState($entity->moderation_state->value)->isPublished(); + } + + /** + * @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/ModerationStateAccessControlHandler.php b/core/modules/content_moderation/src/ModerationStateAccessControlHandler.php deleted file mode 100644 index b2c86d7..0000000 --- a/core/modules/content_moderation/src/ModerationStateAccessControlHandler.php +++ /dev/null @@ -1,31 +0,0 @@ -orIf($admin_access); - } - - return $admin_access; - } - -} 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..df3b930 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,19 @@ 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(), - ]), - ]; + + /** @var \Drupal\workflow\WorkflowInterface $workflow */ + foreach (Workflow::loadMultiple() as $id => $workflow) { + foreach ($workflow->getStates() as $from_state) { + foreach ($from_state->getNextStates() as $to_state) { + $perms['use ' . $workflow->id() . ' transition from ' . $from_state->id() . ' to ' . $to_state->id()] = [ + 'title' => $this->t('Move content from %from state to %to state.', [ + '%from' => $from_state->label(), + '%to' => $to_state->label(), + ]), + ]; + } + } } 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..11598d7 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,18 @@ 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->getState($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())); } + /** @var \Drupal\workflow\Transition[] $transitions */ $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->to()->id()] = $transition->label(); } // @todo https://www.drupal.org/node/2779933 write a test for this. @@ -171,8 +135,8 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen '#access' => FALSE, '#type' => 'select', '#options' => $target_states, - '#default_value' => $default, - '#published' => $default ? $default_state->isPublishedState() : FALSE, + '#default_value' => $default->id(), + '#published' => $default->isPublished(), '#key_column' => $this->column, ]; $element['#element_validate'][] = array(get_class($this), 'validateElement'); @@ -197,7 +161,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..bfe5c9d 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()->id(); } } @@ -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..d90e19e 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,13 @@ 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 = $workflow->getState($entity->moderation_state->value) ?: $workflow->getInitialState(); + $original_state = $workflow->getState($original_entity->moderation_state->value); + // @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 (!$original_state->canTransitionTo($new_state->id())) { + $this->context->addViolation($constraint->message, ['%from' => $original_state->label(), '%to' => $new_state->label()]); } } @@ -126,9 +117,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/Plugin/WorkflowType/ContentModeration.php b/core/modules/content_moderation/src/Plugin/WorkflowType/ContentModeration.php new file mode 100644 index 0000000..669a4f1 --- /dev/null +++ b/core/modules/content_moderation/src/Plugin/WorkflowType/ContentModeration.php @@ -0,0 +1,150 @@ +configuration['states'][$state->id()])) { + $state = new ContentModerationState($state, $this->configuration['states'][$state->id()]['published'], $this->configuration['states'][$state->id()]['default_revision']); + } + else { + $state = new ContentModerationState($state); + } + return $state; + } + + /** + * {@inheritdoc} + */ + public function buildStateConfigurationForm(FormStateInterface $form_state, ContentModerationState $state = NULL) { + $form = []; + $form['published'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Published'), + '#description' => $this->t('When content reaches this state it should be published.'), + '#default_value' => isset($state) ? $state->isPublished() : 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($state) ? $state->isDefaultRevision() : FALSE, + // @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; + } + + /** + * Gets the entity types the workflow is applied to. + * + * @return string[] + * The entity types the workflow is applied to. + */ + public function getEntityTypes() { + return array_keys($this->configuration['entity_types']); + } + + /** + * Gets the bundles of the entity type the workflow is applied to. + * + * @param string $entity_type_id + * The entity type ID to get the bundles for. + * + * @return string[] + * The bundles of the entity type the workflow is applied to. + */ + public function getBundlesForEntityType($entity_type_id) { + return $this->configuration['entity_types'][$entity_type_id]; + } + + /** + * Checks if the workflow applies to the supplied entity type and bundle. + * + * @return bool + * TRUE if the workflow applies to the supplied entity type and bundle. + * FALSE if not. + */ + public function appliesToEntityTypeAndBundle($entity_type_id, $bundle_id) { + if (isset($this->configuration['entity_types'][$entity_type_id])) { + return in_array($bundle_id, $this->configuration['entity_types'][$entity_type_id], TRUE); + } + return FALSE; + } + + /** + * Removes an entity type ID / bundle ID from the workflow. + * + * @param string $entity_type_id + * The entity type ID to remove. + * @param string $bundle_id + * The bundle ID to remove. + */ + public function removeEntityTypeAndBundle($entity_type_id, $bundle_id) { + $key = array_search($bundle_id, $this->configuration['entity_types'][$entity_type_id], TRUE); + if ($key !== FALSE) { + unset($this->configuration['entity_types'][$entity_type_id][$key]); + if (empty($this->configuration['entity_types'][$entity_type_id])) { + unset($this->configuration['entity_types'][$entity_type_id]); + } + else { + $this->configuration['entity_types'][$entity_type_id] = array_values($this->configuration['entity_types'][$entity_type_id]); + } + } + } + + /** + * Add an entity type ID / bundle ID to the workflow. + * + * @param string $entity_type_id + * The entity type ID to add. + * @param string $bundle_id + * The bundle ID to add. + */ + public function addEntityTypeAndBundle($entity_type_id, $bundle_id) { + if (!$this->appliesToEntityTypeAndBundle($entity_type_id, $bundle_id)) { + $this->configuration['entity_types'][$entity_type_id][] = $bundle_id; + natsort($this->configuration['entity_types'][$entity_type_id]); + } + } + + /** + * {@inheritDoc} + */ + public function defaultConfiguration() { + $default_configuration = parent::defaultConfiguration(); + $default_configuration['entity_types'] = []; + return $default_configuration; + } + + /** + * @inheritDoc + */ + public function calculateDependencies() { + return []; + // TODO: Implement calculateDependencies() method. + } + +} 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..1185d25 100644 --- a/core/modules/content_moderation/src/StateTransitionValidation.php +++ b/core/modules/content_moderation/src/StateTransitionValidation.php @@ -3,10 +3,8 @@ 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; +use Drupal\workflow\Transition; /** * Validates whether a certain state transition is allowed. @@ -14,18 +12,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 +28,23 @@ 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. - */ - 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. + * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info + * The moderation information service. */ - 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()); - - /** @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'); - - // Determine the states that are legal on this bundle. - $legal_bundle_states = $bundle->getThirdPartySetting('content_moderation', 'allowed_moderation_states', []); + $workflow = $this->moderationInfo->getWorkFlowForEntity($entity); + $current_state = $entity->moderation_state->value ? $workflow->getState($entity->moderation_state->value) : $workflow->getInitialState(); - // 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($current_state->getTransitions(), function(Transition $transition) use ($workflow, $user) { + return $user->hasPermission('use ' . $workflow->id() . ' transition from ' . $transition->from()->id() . ' to ' . $transition->to()->id()); }); - - 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(); - - $transitions = $this->transitionStorage()->loadMultiple($from); - - 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); - } - 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..b239bf3 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 @@ -32,40 +18,9 @@ public function getValidTransitionTargets(ContentEntityInterface $entity, Accoun * @param \Drupal\Core\Session\AccountInterface $user * The account that wants to perform a transition. * - * @return \Drupal\content_moderation\Entity\ModerationStateTransition[] + * @return \Drupal\workflow\Transition[] * The list of transitions that are legal for this user on this entity. */ public function getValidTransitions(ContentEntityInterface $entity, AccountInterface $user); - /** - * Determines if a user is allowed to transition from one state to another. - * - * 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 - * The destination state. - * @param \Drupal\Core\Session\AccountInterface $user - * The user to validate. - * - * @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); - } 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..89c66c0 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,14 @@ 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->getTypePlugin()->addEntityTypeAndBundle('node', 'test'); + $workflow->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 +62,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 +72,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 +86,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..843b6fa 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,16 @@ 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->getTypePlugin()->addEntityTypeAndBundle('node', $machine_name); + $workflow->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..836ff91 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,25 @@ 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->getTypePlugin()->addEntityTypeAndBundle('node', 'example'); + $workflow->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 +77,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 +85,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 +107,12 @@ 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->getTypePlugin()->addEntityTypeAndBundle('node', 'example'); + $workflow->save(); + $english_node = Node::create([ 'type' => 'example', 'title' => 'Test title', @@ -117,7 +121,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 +130,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 +166,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 +174,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 +188,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 +201,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..bada0f4 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,10 @@ protected function createNodeType() { 'type' => 'page', 'label' => 'Page', ]); - $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE); $node_type->save(); + $workflow = Workflow::load('typical'); + $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'page'); + $workflow->save(); } /** @@ -60,7 +63,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 +78,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 +89,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 +108,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 +119,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 +137,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 +154,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 +170,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..7b62475 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,21 @@ 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->getTypePlugin()->addEntityTypeAndBundle('node', 'article'); + $workflow->save(); $revision_ids = []; $node = Node::create([ 'title' => 'test', 'type' => 'article', ]); + $node->moderation_state->value = 'published'; $node->save(); $revision_ids[] = $node->getRevisionId(); @@ -79,7 +85,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..0bf08d2 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,23 @@ public function testValidTransition() { $node_type = NodeType::create([ 'type' => 'example', ]); - $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE); $node_type->save(); + $workflow = Workflow::load('typical'); + $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example'); + $workflow->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 +77,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->getTypePlugin()->addEntityTypeAndBundle('node', 'example'); + $workflow->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 +114,9 @@ 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->getTypePlugin()->addEntityTypeAndBundle('node', 'example'); + $workflow->save(); $node = Node::load($nid); @@ -155,12 +160,9 @@ 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->getTypePlugin()->addEntityTypeAndBundle('node', 'example'); + $workflow->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..29ccd34 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,11 @@ 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->getTypePlugin()->addEntityTypeAndBundle('node', 'example'); + $workflow->save(); + $this->testNode = Node::create([ 'type' => 'example', 'title' => 'Test title', @@ -61,7 +64,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 +73,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..75d9a75 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,10 @@ 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->getTypePlugin()->addEntityTypeAndBundle('node', 'page'); + $workflow->save(); } /** @@ -53,14 +57,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 +94,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 +128,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..413e814 100644 --- a/core/modules/content_moderation/tests/src/Unit/StateTransitionValidationTest.php +++ b/core/modules/content_moderation/tests/src/Unit/StateTransitionValidationTest.php @@ -2,13 +2,14 @@ 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\DependencyInjection\ContainerBuilder; +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 Drupal\workflow\WorkflowTypeInterface; +use Drupal\workflow\WorkflowTypeManager; use Prophecy\Argument; /** @@ -18,216 +19,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 @@ -239,7 +30,7 @@ public function providerIsTransitionAllowedWithInValidTransition() { * @param bool $allowed * Whether or not to grant a user this permission. * @param bool $result - * Whether userMayTransition() is expected to return TRUE or FALSE. + * Whether getValidTransitions() is expected to have the. * * @dataProvider userTransitionsProvider */ @@ -250,10 +41,45 @@ 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)); + $has_transition = FALSE; + foreach ($validator->getValidTransitions($entity, $user->reveal()) as $transition) { + if ($transition->to()->id() === $to_id) { + $has_transition = TRUE; + break; + } + } + $this->assertSame($result, $has_transition); + } - $this->assertEquals($result, $validator->userMayTransition($storage->load($from_id), $storage->load($to_id), $user->reveal())); + protected function setUpModerationInformation(ContentEntityInterface $entity) { + // Create a container so that the plugin manager and workflow type can be + // mocked. + $container = new ContainerBuilder(); + $workflow_type = $this->prophesize(WorkflowTypeInterface::class); + $workflow_type->decorateState(Argument::any())->willReturnArgument(0); + $workflow_manager = $this->prophesize(WorkflowTypeManager::class); + $workflow_manager->createInstance('content_moderation', Argument::any())->willReturn($workflow_type->reveal()); + $container->set('workflow.plugin.manager.workflow_type', $workflow_manager->reveal()); + \Drupal::setContainer($container); + + $workflow = new Workflow(['id' => 'process', 'type' => 'content_moderation'], '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 +87,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..86b0b90 --- /dev/null +++ b/core/modules/workflow/config/schema/workflow.schema.yml @@ -0,0 +1,38 @@ +workflow.workflow.*: + type: config_entity + label: 'Workflow' + mapping: + id: + type: string + label: 'ID' + label: + type: label + label: 'Label' + type: + type: string + label: 'Workflow type' + type_settings: + type: workflow.type_settings.[%parent.type] + label: 'Custom settings for workflow type' + states: + type: sequence + label: 'States' + sequence: + type: mapping + label: 'State' + mapping: + label: + type: label + label: 'Label' + 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' diff --git a/core/modules/workflow/src/Annotation/WorkflowType.php b/core/modules/workflow/src/Annotation/WorkflowType.php new file mode 100644 index 0000000..deff23e --- /dev/null +++ b/core/modules/workflow/src/Annotation/WorkflowType.php @@ -0,0 +1,40 @@ +states[$state_id])) { + // @todo + throw new \InvalidArgumentException("The state '$state_id' already exists in workflow '{$this->id()}'"); + } + // 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_id] = [ + 'label' => $label, + 'weight' => $weight, + ]; + // By default allow same state transitions as this is allowed in most common + // workflows. + $this->addTransition($state_id, $state_id); + return $this; + } + + /** + * {@inheritdoc} + */ + public function hasState($state_id) { + return isset($this->states[$state_id]); + } + + /** + * {@inheritdoc} + */ + public function getStates($state_ids = NULL) { + if ($state_ids === NULL) { + $state_ids = array_keys($this->states); + } + $states = array_combine($state_ids, array_map([$this, 'getState'], $state_ids)); + uasort($states, [State::class, 'sort']); + return $states; + } + + /** + * {@inheritdoc} + */ + public function getState($state_id) { + if (!isset($this->states[$state_id])) { + throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow '{$this->id()}'"); + } + $state = new State( + $this, + $state_id, + $this->states[$state_id]['label'], + $this->states[$state_id]['weight'], + isset($this->transitions[$state_id]) ? $this->transitions[$state_id] : [] + ); + return $this->getTypePlugin()->decorateState($state); + } + + /** + * {@inheritdoc} + */ + public function setStateLabel($state_id, $label) { + if (!isset($this->states[$state_id])) { + throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow '{$this->id()}'"); + } + $this->states[$state_id]['label'] = $label; + return $this; + } + + /** + * {@inheritdoc} + */ + public function setStateWeight($state_id, $weight) { + if (!isset($this->states[$state_id])) { + throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow '{$this->id()}'"); + } + $this->states[$state_id]['weight'] = $weight; + return $this; + } + + /** + * {@inheritdoc} + */ + public function deleteState($state_id) { + if (!isset($this->states[$state_id])) { + throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow '{$this->id()}'"); + } + unset($this->states[$state_id]); + unset($this->transitions[$state_id]); + // Remove any supported transitions for the other states. + foreach ($this->getStates() as $remaining_state) { + if ($remaining_state->canTransitionTo($state_id)) { + $this->deleteTransition($remaining_state->id(), $state_id); + } + } + $this->getTypePlugin()->deleteState($state_id); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getInitialState() { + $ordered_states = $this->getStates(); + return reset($ordered_states); + } + + /** + * {@inheritdoc} + */ + public function addTransition($from_state_id, $to_state_id) { + if (!$this->getState($from_state_id)->canTransitionTo($to_state_id)) { + // Ensure the to state exists. The from state is checked by calling + // \Drupal\workflow\Entity\Workflow::getState(). + if (!$this->hasState($to_state_id)) { + throw new \InvalidArgumentException("The state '$to_state_id' does not exist in workflow '{$this->id()}'"); + } + $this->transitions[$from_state_id][$to_state_id] = ''; + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setTransitionLabel($from_state_id, $to_state_id, $label) { + if ($this->getState($from_state_id)->canTransitionTo($to_state_id)) { + $this->transitions[$from_state_id][$to_state_id] = $label; + } + else { + if (!$this->hasState($to_state_id)) { + throw new \InvalidArgumentException("The state '$to_state_id' does not exist in workflow '{$this->id()}'"); + } + else { + throw new \InvalidArgumentException("The state '$from_state_id' cannot transition to state '$to_state_id' in workflow '{$this->id()}'"); + } + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function deleteTransition($from_state_id, $to_state_id) { + if ($this->getState($from_state_id)->canTransitionTo($to_state_id)) { + unset($this->transitions[$from_state_id][$to_state_id]); + } + else { + if (!$this->hasState($to_state_id)) { + throw new \InvalidArgumentException("The state '$to_state_id' does not exist in workflow '{$this->id()}'"); + } + else { + throw new \InvalidArgumentException("The state '$from_state_id' cannot transition to state '$to_state_id' in workflow '{$this->id()}'"); + } + } + return $this; + } + + /** + * {@inheritDoc} + */ + public function getTypePlugin() { + return $this->getPluginCollection()->get($this->type); + } + + /** + * {@inheritDoc} + */ + public function getPluginCollections() { + return ['type_settings' => $this->getPluginCollection()]; + } + + /** + * Encapsulates the creation of the workflow's plugin collection. + * + * @return \Drupal\Core\Plugin\DefaultSingleLazyPluginCollection + * The workflow's plugin collection. + */ + protected function getPluginCollection() { + if (!$this->pluginCollection && $this->type) { + $this->pluginCollection = new DefaultSingleLazyPluginCollection(\Drupal::service('workflow.plugin.manager.workflow_type'), $this->type, $this->type_settings); + } + return $this->pluginCollection; + } + +} diff --git a/core/modules/workflow/src/Form/WorkflowAddForm.php b/core/modules/workflow/src/Form/WorkflowAddForm.php new file mode 100644 index 0000000..7cbdfbf --- /dev/null +++ b/core/modules/workflow/src/Form/WorkflowAddForm.php @@ -0,0 +1,107 @@ +workflowTypePluginManager = $workflow_type_plugin_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('workflow.plugin.manager.workflow_type') + ); + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + + /* @var \Drupal\workflow\WorkflowInterface $workflow */ + $workflow = $this->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'], + ], + ]; + + $workflow_types = array_map(function ($plugin_definition) { + return $plugin_definition['label']; + }, $this->workflowTypePluginManager->getDefinitions()); + $form['workflow_type'] = [ + '#type' => 'select', + '#title' => $this->t('Workflow type'), + '#required' => TRUE, + '#options' => $workflow_types, + ]; + + 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 Workflow.', [ + '%label' => $workflow->label(), + ])); + $form_state->setRedirectUrl($workflow->toUrl()); + } + + /** + * {@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']); + $entity->set('type', $values['workflow_type']); + } + +} 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..042e76e --- /dev/null +++ b/core/modules/workflow/src/Form/WorkflowEditForm.php @@ -0,0 +1,126 @@ +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' => TRUE, + ]; + + $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([State::class, 'labelCallback'], $state->getNextStates()); + + $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..f5d837b --- /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->getState($this->stateId)->label(), '%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->getState($this->stateId)->label(); + $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..fd81d50 --- /dev/null +++ b/core/modules/workflow/src/Form/WorkflowStateForm.php @@ -0,0 +1,205 @@ +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(); + $state = isset($this->stateId) ? $workflow->getState($this->stateId) : NULL; + $form['label'] = array( + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => isset($state) ? $state->label() : '', + '#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($state), + ); + + // Add additional form fields from the workflow type plugin. + $form['type_settings'] = [ + $workflow->get('type') => $workflow->getTypePlugin()->buildStateConfigurationForm($form_state, $state), + '#tree' => TRUE, + ]; + + $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 ($workflow->getStates() as $to_state) { + $can_transition = isset($state) ? $state->canTransitionTo($to_state->id()) : FALSE; + $form['transitions'][$to_state->id()] = [ + 'allowed' => [ + '#type' => 'checkbox', + '#default_value' => $can_transition, + ], + 'state' => [ + '#markup' => $to_state->label(), + ], + 'label' => [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#title_display' => 'invisible', + '#maxlength' => 255, + '#default_value' => $can_transition ? $state->getTransition($to_state->id())->label() : '', + '#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(); + if (!$entity->hasState($values['id'])) { + $entity->addState($values['id'], $values['label']); + } + else { + $entity->setStateLabel($values['id'], $values['label']); + } + if (!empty($values['transitions'])) { + foreach ($values['transitions'] as $to_state_id => $transition) { + $can_transition = $entity->getState($values['id'])->canTransitionTo($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); + } + } + } + if (isset($values['type_settings'])) { + $configuration = $entity->getTypePlugin()->getConfiguration(); + $configuration['states'][$values['id']] = $values['type_settings'][$entity->getTypePlugin()->getPluginId()]; + $entity->set('type_settings', $configuration); + } + } + + /** + * {@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->getState($this->stateId)->label(), + ])); + } + else { + drupal_set_message($this->t('Created %label state.', [ + '%label' => $workflow->getState($form_state->getValue('id'))->label(), + ])); + } + $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/Plugin/WorkflowTypeBase.php b/core/modules/workflow/src/Plugin/WorkflowTypeBase.php new file mode 100644 index 0000000..c7798512 --- /dev/null +++ b/core/modules/workflow/src/Plugin/WorkflowTypeBase.php @@ -0,0 +1,77 @@ +getPluginDefinition(); + // The label can be an object. + // @see \Drupal\Core\StringTranslation\TranslatableMarkup + return $definition['label']; + } + + public function decorateState(StateInterface $state) { + return $state; + } + + /** + * {@inheritDoc} + */ + public function deleteState($state_id) { + unset($this->configuration['states'][$state_id]); + } + + /** + * {@inheritdoc} + */ + public function buildStateConfigurationForm(FormStateInterface $form_state) { + return []; + } + + /** + * {@inheritDoc} + */ + public function getConfiguration() { + return $this->configuration; + } + + /** + * {@inheritDoc} + */ + public function setConfiguration(array $configuration) { + $this->configuration = NestedArray::mergeDeep( + $this->defaultConfiguration(), + $configuration + ); + } + + /** + * {@inheritDoc} + */ + public function defaultConfiguration() { + return [ + 'states' => [], + ]; + } + + /** + * {@inheritDoc} + */ + public function calculateDependencies() { + return []; + } + +} diff --git a/core/modules/workflow/src/State.php b/core/modules/workflow/src/State.php new file mode 100644 index 0000000..3f06773 --- /dev/null +++ b/core/modules/workflow/src/State.php @@ -0,0 +1,150 @@ +workflow = $workflow; + $this->id = $id; + $this->label = $label; + $this->weight = $weight; + $this->transitions = $transitions; + } + + /** + * {@inheritdoc} + */ + public function id() { + return $this->id; + } + + /** + * {@inheritdoc} + */ + public function label() { + return $this->label; + } + + /** + * {@inheritdoc} + */ + public function weight() { + return $this->weight; + } + + /** + * {@inheritdoc} + */ + public function getNextStates() { + return $this->workflow->getStates(array_keys($this->transitions)); + } + + /** + * {@inheritdoc} + */ + public function canTransitionTo($to_state_id) { + return isset($this->transitions[$to_state_id]); + } + + /** + * {@inheritdoc} + */ + public function getTransition($to_state_id) { + if (!$this->canTransitionTo($to_state_id)) { + throw new \InvalidArgumentException("Can not transition to '$to_state_id' state"); + } + return new Transition($this->transitions[$to_state_id], $this, $this->workflow->getState($to_state_id)); + } + + /** + * {@inheritdoc} + */ + public function getTransitions() { + return array_map(function($to_state_id) { + return new Transition($this->transitions[$to_state_id], $this, $this->workflow->getState($to_state_id)); + }, array_keys($this->transitions)); + } + + /** + * Helper method to sort states. + * + * @param \Drupal\workflow\StateInterface $a + * First state for comparison. + * @param \Drupal\workflow\StateInterface $b + * Second state for comparison. + * + * @return int + */ + public static function sort(StateInterface $a, StateInterface $b) { + if ($a->weight() == $b->weight()) { + return strnatcasecmp($a->label(), $b->label()); + } + return ($a->weight() < $b->weight()) ? -1 : 1; + } + + /** + * Helper method to convert a list of states to labels + * + * @param \Drupal\workflow\State $state + * + * @return string + * The label of the state. + */ + public static function labelCallback(StateInterface $state) { + return $state->label(); + } + +} diff --git a/core/modules/workflow/src/StateInterface.php b/core/modules/workflow/src/StateInterface.php new file mode 100644 index 0000000..8c89669 --- /dev/null +++ b/core/modules/workflow/src/StateInterface.php @@ -0,0 +1,76 @@ +label = $label; + $this->from = $from; + $this->to = $to; + } + + /** + * Gets the transition's label. + * + * @return string + * The transition's label. + */ + public function label() { + return $this->label; + } + + /** + * Gets the transition's from state. + * + * @return \Drupal\workflow\StateInterface + * The transition's from state. + */ + public function from() { + return $this->from; + } + + /** + * Gets the transition's to state. + * + * @return \Drupal\workflow\StateInterface + * The transition's to state. + */ + public function to() { + return $this->to; + } + +} diff --git a/core/modules/workflow/src/WorkflowAccessControlHandler.php b/core/modules/workflow/src/WorkflowAccessControlHandler.php new file mode 100644 index 0000000..0749681 --- /dev/null +++ b/core/modules/workflow/src/WorkflowAccessControlHandler.php @@ -0,0 +1,75 @@ +get('workflow.plugin.manager.workflow_type') + ); + } + + /** + * Constructs the workflow access control handler instance. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type definition. + * @param \Drupal\Component\Plugin\PluginManagerInterface $workflow_type_manager + * The workflow type plugin manager. + */ + public function __construct(EntityTypeInterface $entity_type, PluginManagerInterface $workflow_type_manager) { + parent::__construct($entity_type); + $this->workflowTypeManager = $workflow_type_manager; + } + + /** + * {@inheritdoc} + */ + protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { + $admin_access = parent::checkAccess($entity, $operation, $account); + + // Allow view with other permission. + if ($operation === 'view') { + return AccessResult::allowedIfHasPermission($account, 'view content moderation')->orIf($admin_access); + } + + return $admin_access; + } + + /** + * {@inheritdoc} + */ + protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { + $workflow_types_count = count(\Drupal::service('workflow.plugin.manager.workflow_type')->getDefinitions()); + $admin_access = parent::checkCreateAccess($account, $context, $entity_bundle); + // Cacheable until extensions change. + return $admin_access->andIf(AccessResult::allowedIf($workflow_types_count > 0))->addCacheTags(['config:core.extension']); + } + +} diff --git a/core/modules/workflow/src/WorkflowInterface.php b/core/modules/workflow/src/WorkflowInterface.php new file mode 100644 index 0000000..b050d25 --- /dev/null +++ b/core/modules/workflow/src/WorkflowInterface.php @@ -0,0 +1,165 @@ +get('entity.manager')->getStorage($entity_type->id()), + $container->get('workflow.plugin.manager.workflow_type') + ); + } + + /** + * Constructs a new WorkflowListBuilder object. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type definition. + * @param \Drupal\Core\Entity\EntityStorageInterface $storage + * The entity storage class. + * @param \Drupal\Component\Plugin\PluginManagerInterface $workflow_type_manager + * The workflow type plugin manager. + */ + public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, PluginManagerInterface $workflow_type_manager) { + parent::__construct($entity_type, $storage); + $this->workflowTypeManager = $workflow_type_manager; + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'workflow_admin_overview_form'; + } + + /** + * {@inheritdoc} + */ + public function buildHeader() { + $header['label'] = $this->t('Workflow'); + $header['type'] = $this->t('Type'); + $header['states'] = $this->t('States'); + + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + /** @var \Drupal\workflow\WorkflowInterface $entity */ + $row['label'] = $entity->label(); + + $row['type']['data'] = [ + '#markup' => $entity->getTypePlugin()->label() + ]; + + $items = array_map([State::class, 'labelCallback'], $entity->getStates()); + $row['states']['data'] = [ + '#theme' => 'item_list', + '#context' => ['list_style' => 'comma-list'], + '#items' => $items, + ]; + + return $row + parent::buildRow($entity); + } + + /** + * {@inheritdoc} + */ + public function render() { + $build = parent::render(); + $workflow_types_count = count($this->workflowTypeManager->getDefinitions()); + if ($workflow_types_count === 0) { + $build['table']['#empty'] = $this->t('There are no workflow types available. In order to create workflows you need to install a module that provides a workflow type. For example, the Content Moderation module provides a workflow type that enables workflows for content entities.'); + } + return $build; + } + +} diff --git a/core/modules/workflow/src/WorkflowTypeInterface.php b/core/modules/workflow/src/WorkflowTypeInterface.php new file mode 100644 index 0000000..e132a6f --- /dev/null +++ b/core/modules/workflow/src/WorkflowTypeInterface.php @@ -0,0 +1,56 @@ +alterInfo('workflow_type_info'); + $this->setCacheBackend($cache_backend, 'workflow_type_info'); + } + +} diff --git a/core/modules/workflow/tests/modules/workflow_type_test/config/schema/workflow_type_test.schema.yml b/core/modules/workflow/tests/modules/workflow_type_test/config/schema/workflow_type_test.schema.yml new file mode 100644 index 0000000..c9f9a81 --- /dev/null +++ b/core/modules/workflow/tests/modules/workflow_type_test/config/schema/workflow_type_test.schema.yml @@ -0,0 +1,8 @@ +workflow.type_settings.workflow_type_test: + type: mapping + label: 'Fallback for workflow type settings' + mapping: + states: + type: sequence + sequence: + type: ignore diff --git a/core/modules/workflow/tests/modules/workflow_type_test/src/Plugin/WorkflowType/TestType.php b/core/modules/workflow/tests/modules/workflow_type_test/src/Plugin/WorkflowType/TestType.php new file mode 100644 index 0000000..4d9ba8c --- /dev/null +++ b/core/modules/workflow/tests/modules/workflow_type_test/src/Plugin/WorkflowType/TestType.php @@ -0,0 +1,16 @@ +drupalPlaceBlock('local_actions_block'); + } + + /** + * Tests the creation of a workflow through the UI. + */ + public function testWorkflowUiWithNoType() { + $this->drupalLogin($this->createUser(['access administration pages', 'administer workflows'])); + $this->drupalGet('admin/config/workflow/workflows/add'); + // There are no workflow types so this should be a 403. + $this->assertSession()->statusCodeEquals(403); + + $this->drupalGet('admin/config/workflow/workflows'); + $this->assertSession()->pageTextContains('There are no workflow types available. In order to create workflows you need to install a module that provides a workflow type. For example, the Content Moderation module provides a workflow type that enables workflows for content entities.'); + $this->assertSession()->pageTextNotContains('Add workflow'); + + $this->container->get('module_installer')->install(['workflow_type_test']); + // The render cache needs to be cleared because although the cache tags are + // correctly set the render cache does not pick it up. + \Drupal::cache('render')->deleteAll(); + + $this->drupalGet('admin/config/workflow/workflows'); + $this->assertSession()->pageTextNotContains('There are no workflow types available. In order to create workflows you need to install a module that provides a workflow type. For example, the Content Moderation module provides a workflow type that enables workflows for content entities.'); + $this->assertSession()->linkExists('Add workflow'); + $this->assertSession()->pageTextContains('There is no Workflow yet.'); + } + +} diff --git a/core/modules/workflow/tests/src/Functional/WorkflowUiTest.php b/core/modules/workflow/tests/src/Functional/WorkflowUiTest.php new file mode 100644 index 0000000..6ab4c4f --- /dev/null +++ b/core/modules/workflow/tests/src/Functional/WorkflowUiTest.php @@ -0,0 +1,125 @@ +drupalPlaceBlock('local_actions_block'); + } + + /** + * Tests route access/permissions. + */ + public function testAccess() { + // Create a minimal workflow for testing. + $workflow = Workflow::create(['id' => 'test', 'type' => 'workflow_type_test']); + $workflow + ->addState('published', 'Published') + ->save(); + + $paths = [ + 'admin/config/workflow/workflows', + 'admin/config/workflow/workflows/add', + 'admin/config/workflow/workflows/manage/test', + 'admin/config/workflow/workflows/manage/test/delete', + 'admin/config/workflow/workflows/manage/test/add_state', + 'admin/config/workflow/workflows/manage/test/state/published', + 'admin/config/workflow/workflows/manage/test/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->assertSession()->linkByHrefExists('admin/config/workflow/workflows'); + $this->clickLink('Workflows'); + $this->assertSession()->pageTextContains('There is no Workflow yet.'); + $this->clickLink('Add workflow'); + $this->submitForm(['label' => 'Test', 'id' => 'test', 'workflow_type' => 'workflow_type_test'], 'Save'); + $this->assertSession()->pageTextContains('Created the Test Workflow.'); + $this->assertSession()->pageTextContains('There are no states yet.'); + $this->clickLink('Add a new state'); + $this->submitForm(['label' => 'Published', 'id' => 'published'], 'Save'); + $this->assertSession()->pageTextContains('Created Published state.'); + $this->clickLink('Add a new state'); + $this->submitForm(['label' => 'Draft', 'id' => 'draft', 'transitions[published][allowed]' => TRUE, 'transitions[published][label]' => 'Create new draft'], 'Save'); + $this->assertSession()->pageTextContains('Created Draft state.'); + $this->assertSession()->elementContains('css', 'ul[data-drupal-selector=edit-states-draft-transitions]', 'Published'); + $this->assertSession()->elementContains('css', 'ul[data-drupal-selector=edit-states-draft-transitions]', 'Draft'); + $this->assertSession()->elementContains('css', 'ul[data-drupal-selector=edit-states-published-transitions]', 'Published'); + $this->assertSession()->elementNotContains('css', 'ul[data-drupal-selector=edit-states-published-transitions]', 'Draft'); + // The fist state to edit on the page should be published. + $this->clickLink('Edit'); + $this->assertSession()->fieldValueEquals('label', 'Published'); + // Allowed published to draft. + $this->submitForm(['transitions[draft][allowed]' => TRUE, 'transitions[draft][label]' => 'Create new draft'], 'Save'); + $this->assertSession()->elementContains('css', 'ul[data-drupal-selector=edit-states-published-transitions]', 'Draft'); + + // Ensure that weight changes the state ordering. + $this->assertSession()->elementContains('css', 'table#edit-states > tbody > tr:nth-child(1) > td:nth-child(1)', 'Published'); + $this->assertSession()->elementContains('css', 'table#edit-states > tbody > tr:nth-child(2) > td:nth-child(1)', 'Draft'); + $this->submitForm(['states[draft][weight]' => '-1'], 'Save'); + // This will take us to the list of workflows, so we need to edit the + // workflow again. + $this->clickLink('Edit'); + $this->assertSession()->elementContains('css', 'table#edit-states > tbody > tr:nth-child(1) > td:nth-child(1)', 'Draft'); + $this->assertSession()->elementContains('css', 'table#edit-states > tbody > tr:nth-child(2) > td:nth-child(1)', 'Published'); + // Delete the Draft state. + $this->clickLink('Delete'); + $this->assertSession()->pageTextContains('Are you sure you want to delete Draft from Test?'); + $this->submitForm([], 'Delete'); + $this->assertSession()->pageTextContains('State Draft deleted.'); + // Ensure the draft state has been removed. + $this->assertSession()->elementNotContains('css', 'table#edit-states', 'Draft'); + $this->assertSession()->elementContains('css', 'table#edit-states', 'Published'); + // Delete the published state. + $this->clickLink('Delete'); + $this->assertSession()->pageTextContains('Are you sure you want to delete Published from Test?'); + $this->submitForm([], 'Delete'); + $this->assertSession()->pageTextContains('State Published deleted.'); + $this->assertSession()->pageTextContains('There are no states yet.'); + // Delete the entire workflow. + $this->clickLink('Delete'); + $this->assertSession()->pageTextContains('Are you sure you want to delete Test?'); + $this->submitForm([], 'Delete'); + $this->assertSession()->pageTextContains('Workflow Test deleted.'); + $this->assertSession()->pageTextContains('There is no Workflow yet.'); + } + +} diff --git a/core/modules/workflow/tests/src/Unit/StateTest.php b/core/modules/workflow/tests/src/Unit/StateTest.php new file mode 100644 index 0000000..63e1515 --- /dev/null +++ b/core/modules/workflow/tests/src/Unit/StateTest.php @@ -0,0 +1,110 @@ +prophesize(WorkflowInterface::class)->reveal(), 'to', 'To', 1, []); + // Create a workflow that can get the future state. + $workflow = $this->prophesize(WorkflowInterface::class); + $workflow->getStates(['to'])->willReturn([$future_state]); + // Create a state that allows transitions to the future state. + $state = new State($workflow->reveal(), 'from', 'From', 0, ['to' => 'Move to To']); + $this->assertEquals([$future_state], $state->getNextStates()); + } + + /** + * @covers ::canTransitionTo + */ + public function testCanTransitionTo() { + $state = new State($this->prophesize(WorkflowInterface::class)->reveal(), 'draft', 'Draft', 0, ['draft' => 'Draft', 'published' => 'Publish']); + $this->assertTrue($state->canTransitionTo('draft')); + $this->assertTrue($state->canTransitionTo('published')); + $this->assertFalse($state->canTransitionTo('some_other_state')); + + $state = new State($this->prophesize(WorkflowInterface::class)->reveal(), 'draft', 'Draft', 0, []); + $this->assertFalse($state->canTransitionTo('draft')); + } + + /** + * @covers ::getTransition + * @covers \Drupal\workflow\Transition::from + * @covers \Drupal\workflow\Transition::to + * @covers \Drupal\workflow\Transition::label + */ + public function testGetTransition() { + // Create a future state to transition to. + $future_state = new State($this->prophesize(WorkflowInterface::class)->reveal(), 'to', 'To', 1, []); + // Create a workflow that can get the future state. + $workflow = $this->prophesize(WorkflowInterface::class); + $workflow->getState('to')->willReturn($future_state); + // Create a state that allows transitions to the future state. + $state = new State($workflow->reveal(), 'from', 'From', 0, ['to' => 'Move to To']); + $transition = $state->getTransition('to'); + $this->assertEquals($state, $transition->from()); + $this->assertEquals($future_state, $transition->to()); + $this->assertEquals('Move to To', $transition->label()); + } + + /** + * @covers ::getTransition + */ + public function testGetTransitionException() { + $this->setExpectedException(\InvalidArgumentException::class, "Can not transition to 'missing_state' state"); + $state = new State($this->prophesize(WorkflowInterface::class)->reveal(), 'state', 'state'); + $state->getTransition('missing_state'); + } + + /** + * @covers ::getTransitions + * @covers \Drupal\workflow\Transition::from + * @covers \Drupal\workflow\Transition::to + */ + public function testGetTransitions() { + // Create a future state to transition to. + $draft = new State($this->prophesize(WorkflowInterface::class)->reveal(), 'draft', 'Draft', 0, []); + $archived = new State($this->prophesize(WorkflowInterface::class)->reveal(), 'archived', 'Archive', 2, []); + // Create a workflow that can get the future state. + $workflow = $this->prophesize(WorkflowInterface::class); + $workflow->getState('draft')->willReturn($draft); + $workflow->getState('archived')->willReturn($archived); + // Create a state that allows transitions to the future state. + $state = new State($workflow->reveal(), 'published', 'published', 1, ['draft' => 'Create new draft', 'archived' => 'Archive']); + $transitions = $state->getTransitions(); + $this->assertCount(2, $transitions); + $this->assertEquals($state, $transitions[0]->from()); + $this->assertEquals($draft, $transitions[0]->to()); + $this->assertEquals($state, $transitions[1]->from()); + $this->assertEquals($archived, $transitions[1]->to()); + } + + /** + * @covers ::sort + */ + public function testSort() { + $states[] = new State($this->prophesize(WorkflowInterface::class)->reveal(), 'fourth', 'Fourth', 2, []); + $states[] = new State($this->prophesize(WorkflowInterface::class)->reveal(), 'second', 'Second', 0, []); + $states[] = new State($this->prophesize(WorkflowInterface::class)->reveal(), 'first', 'First', -2, []); + $states[] = new State($this->prophesize(WorkflowInterface::class)->reveal(), 'third', 'Third', 0, []); + usort($states, [State::class, 'sort']); + $this->assertEquals('first', $states[0]->id()); + $this->assertEquals('second', $states[1]->id()); + $this->assertEquals('third', $states[2]->id()); + $this->assertEquals('fourth', $states[3]->id()); + } + +} diff --git a/core/modules/workflow/tests/src/Unit/WorkflowTest.php b/core/modules/workflow/tests/src/Unit/WorkflowTest.php new file mode 100644 index 0000000..e85da9a --- /dev/null +++ b/core/modules/workflow/tests/src/Unit/WorkflowTest.php @@ -0,0 +1,372 @@ +prophesize(WorkflowTypeInterface::class); + $workflow_type->decorateState(Argument::any())->willReturnArgument(0); + $workflow_manager = $this->prophesize(WorkflowTypeManager::class); + $workflow_manager->createInstance('test_type', Argument::any())->willReturn($workflow_type->reveal()); + $container->set('workflow.plugin.manager.workflow_type', $workflow_manager->reveal()); + \Drupal::setContainer($container); + } + + /** + * @covers ::addState + * @covers ::hasState + */ + public function testAddAndHasState() { + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $this->assertFalse($workflow->hasState('draft')); + + // By default states are ordered in the order added. + $workflow->addState('draft', 'Draft'); + $this->assertTrue($workflow->hasState('draft')); + $this->assertFalse($workflow->hasState('published')); + $this->assertEquals(0, $workflow->getState('draft')->weight()); + // Adding a state sets up a transition to itself. + $this->assertEquals(['draft' => $workflow->getState('draft')], $workflow->getState('draft')->getNextStates()); + + // New states are added with a new weight 1 more than the current highest + // weight. + $workflow->addState('published', 'Published'); + $this->assertEquals(1, $workflow->getState('published')->weight()); + } + + /** + * @covers ::addState + */ + public function testAddStateException() { + $this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' already exists in workflow 'test'"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->addState('draft', 'Draft'); + $workflow->addState('draft', 'Draft'); + } + + /** + * @covers ::getStates + */ + public function testGetStates() { + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + + // Get states works when there are none. + $this->assertArrayEquals([], array_keys($workflow->getStates())); + $this->assertArrayEquals([], array_keys($workflow->getStates([]))); + + // By default states are ordered in the order added. + $workflow + ->addState('draft', 'Draft') + ->addState('published', 'Published') + ->addState('archived', 'Archived'); + + // Ensure we're returning state objects. + $this->assertInstanceOf(State::class, $workflow->getStates()['draft']); + + // Passing in no IDs returns all states. + $this->assertArrayEquals(['draft', 'published', 'archived'], array_keys($workflow->getStates())); + + // The order of states is by weight. + $workflow->setStateWeight('published', -1); + $this->assertArrayEquals(['published', 'draft', 'archived'], array_keys($workflow->getStates())); + + // You can limit the states returned by passing in states IDs. + $this->assertArrayEquals(['draft', 'archived'], array_keys($workflow->getStates(['draft', 'archived']))); + + // An empty array does not load all states. + $this->assertArrayEquals([], array_keys($workflow->getStates([]))); + } + + /** + * @covers ::getStates + */ + public function testGetStatesException() { + $this->setExpectedException(\InvalidArgumentException::class, "The state 'state_that_does_not_exist' does not exist in workflow 'test'"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->getStates(['state_that_does_not_exist']); + } + + /** + * @covers ::getState + */ + public function testGetState() { + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + // By default states are ordered in the order added. + $workflow + ->addState('draft', 'Draft') + ->addState('published', 'Published') + ->addState('archived', 'Archived') + ->addTransition('draft', 'published') + ->setTransitionLabel('draft', 'published', 'Publish'); + + // Ensure we're returning state objects and they are set up correctly + $this->assertInstanceOf(State::class, $workflow->getState('draft')); + $this->assertEquals('archived', $workflow->getState('archived')->id()); + $this->assertEquals('Archived', $workflow->getState('archived')->label()); + + $draft = $workflow->getState('draft'); + $this->assertTrue($draft->canTransitionTo('draft')); + $this->assertTrue($draft->canTransitionTo('published')); + $this->assertFalse($draft->canTransitionTo('archived')); + $this->assertEquals('Publish', $draft->getTransition('published')->label()); + $this->assertEquals(0, $draft->weight()); + $this->assertEquals(1, $workflow->getState('published')->weight()); + $this->assertEquals(2, $workflow->getState('archived')->weight()); + } + + /** + * @covers ::getState + */ + public function testGetStateException() { + $this->setExpectedException(\InvalidArgumentException::class, "The state 'state_that_does_not_exist' does not exist in workflow 'test'"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->getState('state_that_does_not_exist'); + } + + /** + * @covers ::setStateLabel + */ + public function testSetStateLabel() { + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->addState('draft', 'Draft'); + $this->assertEquals('Draft', $workflow->getState('draft')->label()); + $workflow->setStateLabel('draft', 'Unpublished'); + $this->assertEquals('Unpublished', $workflow->getState('draft')->label()); + } + + /** + * @covers ::setStateLabel + */ + public function testSetStateLabelException() { + $this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' does not exist in workflow 'test'"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->setStateLabel('draft', 'Draft'); + } + + /** + * @covers ::setStateWeight + */ + public function testSetStateWeight() { + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->addState('draft', 'Draft'); + $this->assertEquals(0, $workflow->getState('draft')->weight()); + $workflow->setStateWeight('draft', -10); + $this->assertEquals(-10, $workflow->getState('draft')->weight()); + } + + /** + * @covers ::setStateWeight + */ + public function testSetStateWeightException() { + $this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' does not exist in workflow 'test'"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->setStateWeight('draft', 10); + } + + /** + * @covers ::deleteState + */ + public function testDeleteState() { + // Create a container so that the plugin manager and workflow type can be + // mocked and test that + // \Drupal\workflow\WorkflowTypeInterface::deleteState() is called + // correctly. + $container = new ContainerBuilder(); + $workflow_type = $this->prophesize(WorkflowTypeInterface::class); + $workflow_type->decorateState(Argument::any())->willReturnArgument(0); + $workflow_type->deleteState('draft')->shouldBeCalled(); + $workflow_manager = $this->prophesize(WorkflowTypeManager::class); + $workflow_manager->createInstance('test_type', Argument::any())->willReturn($workflow_type->reveal()); + $container->set('workflow.plugin.manager.workflow_type', $workflow_manager->reveal()); + \Drupal::setContainer($container); + + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow + ->addState('draft', 'Draft') + ->addState('published', 'Published') + ->addTransition('draft', 'published') + ->addTransition('published', 'draft'); + $this->assertCount(2, $workflow->getStates()); + $this->assertCount(2, $workflow->getState('published')->getTransitions()); + $workflow->deleteState('draft'); + $this->assertFalse($workflow->hasState('draft')); + $this->assertCount(1, $workflow->getStates()); + $this->assertCount(1, $workflow->getState('published')->getTransitions()); + } + + /** + * @covers ::deleteState + */ + public function testDeleteStateException() { + $this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' does not exist in workflow 'test'"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->deleteState('draft'); + } + + /** + * @covers ::getInitialState + */ + public function testGetInitialState() { + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + + // By default states are ordered in the order added. + $workflow + ->addState('draft', 'Draft') + ->addState('published', 'Published') + ->addState('archived', 'Archived'); + + $this->assertEquals('draft', $workflow->getInitialState()->id()); + + // Make published the first state. + $workflow->setStateWeight('published', -1); + $this->assertEquals('published', $workflow->getInitialState()->id()); + } + + /** + * @covers ::addTransition + */ + public function testAddTransition() { + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + + // By default states are ordered in the order added. + $workflow + ->addState('draft', 'Draft') + ->addState('published', 'Published'); + + $this->assertFalse($workflow->getState('draft')->canTransitionTo('published')); + $workflow->addTransition('draft', 'published'); + $this->assertTrue($workflow->getState('draft')->canTransitionTo('published')); + } + + /** + * @covers ::addTransition + */ + public function testAddTransitionMissingFromException() { + $this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' does not exist in workflow 'test'"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->addState('published', 'Published'); + $workflow->addTransition('draft', 'published'); + } + + /** + * @covers ::addTransition + */ + public function testAddTransitionMissingToException() { + $this->setExpectedException(\InvalidArgumentException::class, "The state 'published' does not exist in workflow 'test'"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->addState('draft', 'Draft'); + $workflow->addTransition('draft', 'published'); + } + + /** + * @covers ::setTransitionLabel + */ + public function testSetTransitionLabel() { + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow + ->addState('draft', 'Draft') + ->addState('published', 'Published') + ->addTransition('draft', 'published'); + $this->assertEquals('', $workflow->getState('draft')->getTransition('published')->label()); + $workflow->setTransitionLabel('draft', 'published', 'Publish'); + $this->assertEquals('Publish', $workflow->getState('draft')->getTransition('published')->label()); + } + + /** + * @covers ::setTransitionLabel + */ + public function testSetTransitionLabelMissingFromException() { + $this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' does not exist in workflow 'test'"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->addState('published', 'Published'); + $workflow->setTransitionLabel('draft', 'published', 'Publish'); + } + + /** + * @covers ::setTransitionLabel + */ + public function testSetTransitionLabelMissingToException() { + $this->setExpectedException(\InvalidArgumentException::class, "The state 'published' does not exist in workflow 'test'"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->addState('draft', 'Draft'); + $workflow->setTransitionLabel('draft', 'published', 'Publish'); + } + + /** + * @covers ::setTransitionLabel + */ + public function testSetTransitionLabelCannotTransitionException() { + $this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' cannot transition to state 'published' in workflow 'test'"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow + ->addState('draft', 'Draft') + ->addState('published', 'Published'); + $workflow->setTransitionLabel('draft', 'published', 'Publish'); + } + + /** + * @covers ::deleteTransition + */ + public function testDeleteTransition() { + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow + ->addState('draft', 'Draft') + ->addState('published', 'Published') + ->addTransition('draft', 'published'); + $this->assertTrue($workflow->getState('draft')->canTransitionTo('published')); + $workflow->deleteTransition('draft', 'published'); + $this->assertFalse($workflow->getState('draft')->canTransitionTo('published')); + $this->assertTrue($workflow->getState('draft')->canTransitionTo('draft')); + } + + /** + * @covers ::deleteTransition + */ + public function testDeleteTransitionMissingFromException() { + $this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' does not exist in workflow 'test'"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->addState('published', 'Published'); + $workflow->deleteTransition('draft', 'published'); + } + + /** + * @covers ::deleteTransition + */ + public function testDeleteTransitionMissingToException() { + $this->setExpectedException(\InvalidArgumentException::class, "The state 'published' does not exist in workflow 'test'"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->addState('draft', 'Draft'); + $workflow->deleteTransition('draft', 'published'); + } + + /** + * @covers ::deleteTransition + */ + public function testDeleteTransitionCannotTransitionException() { + $this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' cannot transition to state 'published' in workflow 'test'"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow + ->addState('draft', 'Draft') + ->addState('published', 'Published'); + $workflow->deleteTransition('draft', 'published'); + } + +} 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..38c3534 --- /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' diff --git a/core/modules/workflow/workflow.services.yml b/core/modules/workflow/workflow.services.yml new file mode 100644 index 0000000..e70dbe5 --- /dev/null +++ b/core/modules/workflow/workflow.services.yml @@ -0,0 +1,6 @@ +services: + workflow.plugin.manager.workflow_type: + class: Drupal\workflow\WorkflowTypeManager + parent: default_plugin_manager + tags: + - { name: plugin_manager_cache_clear } diff --git a/core/phpunit.xml.dist b/core/phpunit.xml.dist index 9658f2c..463f080 100644 --- a/core/phpunit.xml.dist +++ b/core/phpunit.xml.dist @@ -15,7 +15,7 @@ result printer that links to the html output results for functional tests. Unfortunately, this breaks the output of PHPStorm's PHPUnit runner. However, if using the command line you can add - - -printerClass="\Drupal\Tests\Listeners\HtmlOutputPrinter" to use it (note + - -printer="\Drupal\Tests\Listeners\HtmlOutputPrinter" to use it (note there should be no spaces between the hyphens). --> @@ -28,7 +28,7 @@ - +