diff --git a/core/composer.json b/core/composer.json index 99b5cd016d045664da2acb0d5f568bd78ef42ed3..71f07d029fdc3c63c2e12ac29cc5a0264fe58c95 100644 --- a/core/composer.json +++ b/core/composer.json @@ -142,7 +142,8 @@ "drupal/update": "self.version", "drupal/user": "self.version", "drupal/views": "self.version", - "drupal/views_ui": "self.version" + "drupal/views_ui": "self.version", + "drupal/workflows": "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 02794819c69fdf7a2a44064fff1b11e687ac8acb..0000000000000000000000000000000000000000 --- 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 c7eb64c6c6330445a0813f7e01380f6467e0f21a..0000000000000000000000000000000000000000 --- 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 8467e86c0cd3c5235708d2a622513dfbf21f5485..0000000000000000000000000000000000000000 --- 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 8fbf9c3dfc25883c28fcbe0110739b4cfb3c158b..0000000000000000000000000000000000000000 --- 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 4be7600aaaa4003617e614593a32a4a7f346a993..0000000000000000000000000000000000000000 --- 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 0ba0f34706d032675994d4ef6da9f156d5285552..0000000000000000000000000000000000000000 --- 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 cf95d3daba977b4f6a53acd460c0a716706ff527..0000000000000000000000000000000000000000 --- 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 f3a866a6afccea36b27eb7acfd44f5b09532ae02..0000000000000000000000000000000000000000 --- 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 bd25a31ef3cc05c7704344b7f0df99c3aee78855..0000000000000000000000000000000000000000 --- 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 3c09a85a0a109420f42883d61bac0bc09be1c33e..0000000000000000000000000000000000000000 --- 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/workflows.workflow.editorial.yml b/core/modules/content_moderation/config/install/workflows.workflow.editorial.yml new file mode 100644 index 0000000000000000000000000000000000000000..d0243b165ba4940a424dfe7ca5c701fc99b2bfbe --- /dev/null +++ b/core/modules/content_moderation/config/install/workflows.workflow.editorial.yml @@ -0,0 +1,63 @@ +langcode: en +status: true +dependencies: + module: + - content_moderation +id: editorial +label: 'Editorial workflow' +states: + archived: + label: Archived + weight: 5 + draft: + label: Draft + weight: -5 + published: + label: Published + weight: 0 +transitions: + archive: + label: Archive + from: + - published + to: archived + weight: 2 + archived_draft: + label: 'Un-archive to Draft' + from: + - archived + to: draft + weight: 3 + archived_published: + label: Un-archive + from: + - archived + to: published + weight: 4 + create_new_draft: + label: 'Create New Draft' + from: + - draft + - published + to: draft + weight: 0 + publish: + label: Publish + from: + - draft + - published + to: published + weight: 1 +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 7f9e8fdeff5cda3c2e5fd119afe23fca410b6048..b791b67b8018ad3d6b57a4eaf87179cd76004e9c 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 6d92b64d41dd2284080b6701032b9fcfce530f82..ca53b596c45ba457fd4863a37d97575aea06cc03 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: + - workflows 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 cbb2d3fce04cb43e7c377c044c598eb5b5fe0612..0000000000000000000000000000000000000000 --- 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 0fcb3ebd4a3841470228c50a06e655625e051b9a..0000000000000000000000000000000000000000 --- 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 d715219a05f80fdc0eb3975bf2bbeabcf99bd76b..f92e92e1cbea26a3d90786d8574587c936786457 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 582242b8f65aea5d74b93ccef0933defe961b0af..07ef7c083d656293c43743b60123b55cd419ed3b 100644 --- a/core/modules/content_moderation/content_moderation.module +++ b/core/modules/content_moderation/content_moderation.module @@ -18,9 +18,11 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\workflows\WorkflowInterface; use Drupal\node\NodeInterface; use Drupal\node\Plugin\Action\PublishNode; use Drupal\node\Plugin\Action\UnpublishNode; +use Drupal\workflows\Entity\Workflow; /** * Implements hook_help(). @@ -31,15 +33,13 @@ 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('Configuring workflows') . '
'; + $output .= '
' . t('Enable the Workflow UI module to create, edit and delete content moderation workflows.') . '

'; $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 .= '
' . t('Each transition 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 .= '
'; return $output; } @@ -182,15 +182,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 +224,39 @@ 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\workflows\WorkflowInterface $workflow */ + foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) { + /** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $plugin */ + $plugin = $workflow->getTypePlugin(); + 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(); + } + } + } + } +} + +/** + * Implements hook_ENTITY_TYPE_insert(). + */ +function content_moderation_workflow_insert(WorkflowInterface $entity) { + // 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(); +} + +/** + * Implements hook_ENTITY_TYPE_update(). + */ +function content_moderation_workflow_update(WorkflowInterface $entity) { + content_moderation_workflow_insert($entity); +} diff --git a/core/modules/content_moderation/content_moderation.permissions.yml b/core/modules/content_moderation/content_moderation.permissions.yml index 293a77d3de0ade58490f03c05a8e931905e2e484..af28bbf1add02c5701d25b690bdb74cf881b87ce 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 912eed89fb8225ca4baa91078cd0e77b1907c2a0..0000000000000000000000000000000000000000 --- 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 75f0c645be20303e1a13c1213d34445dd83b5b9f..904bc0dc335ae51100a6be8ecff01af4a7345230 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 0000000000000000000000000000000000000000..34262eb59f6f0a251cd8a53b0fa2fdf36f775848 --- /dev/null +++ b/core/modules/content_moderation/src/ContentModerationState.php @@ -0,0 +1,114 @@ +state = $state; + $this->published = $published; + $this->defaultRevision = $default_revision; + } + + /** + * Determines if entities should be published if in this state. + * + * @return bool + */ + public function isPublishedState() { + return $this->published; + } + + /** + * Determines if entities should be the default revision if in this state. + * + * @return bool + */ + public function isDefaultRevisionState() { + 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 canTransitionTo($to_state_id) { + return $this->state->canTransitionTo($to_state_id); + } + + /** + * {@inheritdoc} + */ + public function getTransitionTo($to_state_id) { + return $this->state->getTransitionTo($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 978408fddb00921097ab2271d6cfc4ab010c0ca6..d60dad7619ec1706e312784e3c468755408130de 100644 --- a/core/modules/content_moderation/src/Entity/ContentModerationState.php +++ b/core/modules/content_moderation/src/Entity/ContentModerationState.php @@ -55,10 +55,17 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setTranslatable(TRUE) ->setRevisionable(TRUE); - $fields['moderation_state'] = BaseFieldDefinition::create('entity_reference') + $fields['workflow'] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('Workflow')) + ->setDescription(t('The workflow the moderation state is in.')) + ->setSetting('target_type', 'workflow') + ->setRequired(TRUE) + ->setTranslatable(TRUE) + ->setRevisionable(TRUE); + + $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 +162,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 83de1874cf7bdfd83e38c622b3df45c2596e7f6d..247d352a466cfec14c88eca4899ce1a80482d7b2 100644 --- a/core/modules/content_moderation/src/Entity/Handler/NodeModerationHandler.php +++ b/core/modules/content_moderation/src/Entity/Handler/NodeModerationHandler.php @@ -2,14 +2,43 @@ 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. */ 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} */ @@ -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 379ff60d16be0f606a13d7189b956e085bcc9950..0000000000000000000000000000000000000000 --- 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 95a115b962cf8a1b1ac1df8a676a3619e6046510..0000000000000000000000000000000000000000 --- 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 d76a35b983f59515fde084d3ab8a254197cfe68b..b4d637ff94a10aa3c10076d650563ac29197ee60 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\workflows\WorkflowInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -45,6 +47,13 @@ class EntityOperations implements ContainerInjectionInterface { */ protected $tracker; + /** + * The entity bundle information service. + * + * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface + */ + protected $bundleInfo; + /** * Constructs a new EntityOperations object. * @@ -56,12 +65,15 @@ class EntityOperations implements ContainerInjectionInterface { * The form builder. * @param \Drupal\content_moderation\RevisionTrackerInterface $tracker * The revision tracker. + * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info + * The entity bundle information service. */ - 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_info) { $this->moderationInfo = $moderation_info; $this->entityTypeManager = $entity_type_manager; $this->formBuilder = $form_builder; $this->tracker = $tracker; + $this->bundleInfo = $bundle_info; } /** @@ -72,7 +84,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,20 +99,20 @@ 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); + /** @var \Drupal\content_moderation\ContentModerationState $current_state */ + $current_state = $workflow->getState($entity->moderation_state->value); // This entity is default if it is new, the default revision, or the // default revision is not published. $update_default_revision = $entity->isNew() - || $moderation_state->isDefaultRevisionState() - || !$this->isDefaultRevisionPublished($entity); + || $current_state->isDefaultRevisionState() + || !$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); + $this->entityTypeManager->getHandler($entity->getEntityTypeId(), 'moderation')->onPresave($entity, $update_default_revision, $current_state->isPublishedState()); } } @@ -140,15 +153,14 @@ 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; + $workflow = $this->moderationInfo->getWorkFlowForEntity($entity); /** @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 = $workflow->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(); @@ -157,6 +169,7 @@ protected function updateOrCreateFromEntity(EntityInterface $entity) { $entities = $storage->loadByProperties([ 'content_entity_type_id' => $entity_type_id, 'content_entity_id' => $entity_id, + 'workflow' => $workflow->id(), ]); /** @var \Drupal\content_moderation\ContentModerationStateInterface $content_moderation_state */ @@ -166,6 +179,7 @@ protected function updateOrCreateFromEntity(EntityInterface $entity) { 'content_entity_type_id' => $entity_type_id, 'content_entity_id' => $entity_id, ]); + $content_moderation_state->workflow->target_id = $workflow->id(); } else { // Create a new revision. @@ -186,7 +200,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); } /** @@ -241,11 +255,13 @@ public function entityView(array &$build, EntityInterface $entity, EntityViewDis * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity being saved. + * @param \Drupal\workflows\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()); @@ -260,7 +276,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)->isPublishedState(); } } diff --git a/core/modules/content_moderation/src/EntityTypeInfo.php b/core/modules/content_moderation/src/EntityTypeInfo.php index c35919e56b4f56daa65f86d86354f95b9b863b51..25abf6af3b7d35ee7a085b23b52c434a0a7e3bb2 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; @@ -49,6 +50,13 @@ class EntityTypeInfo implements ContainerInjectionInterface { */ protected $entityTypeManager; + /** + * The bundle information service. + * + * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface + */ + protected $bundleInfo; + /** * The current user. * @@ -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,9 +301,9 @@ public function entityBaseFieldInfo(EntityTypeInterface $entity_type) { } $fields = []; - $fields['moderation_state'] = BaseFieldDefinition::create('entity_reference') - ->setLabel($this->t('Moderation state')) - ->setDescription($this->t('The moderation state of this piece of content.')) + $fields['moderation_state'] = BaseFieldDefinition::create('string') + ->setLabel(t('Moderation state')) + ->setDescription(t('The moderation state of this piece of content.')) ->setComputed(TRUE) ->setClass(ModerationStateFieldItemList::class) ->setSetting('target_type', 'moderation_state') diff --git a/core/modules/content_moderation/src/Form/BundleModerationConfigurationForm.php b/core/modules/content_moderation/src/Form/BundleModerationConfigurationForm.php index 88bbf7ff61983a6b6f5a53628e95305c26ad152e..10b3d6aecfcc8c9215283ce15563b6bcfcdb4147 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\workflows\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,59 @@ 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(); + /* @var \Drupal\Core\Config\Entity\ConfigEntityInterface $bundle */ + $bundle = $this->getEntity(); + $bundle_of_entity_type = $this->entityTypeManager->getDefinition($bundle->getEntityType()->getBundleOf()); + /* @var \Drupal\workflows\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; })); - $options_unpublished = array_map($label, array_filter($states, function(ModerationState $state) { - return !$state->isPublishedState(); - })); - - $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' => 'value', + '#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' => 'value', + '#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' => 'value', + '#value' => $bundle_of_entity_type->id(), ]; - $form['#entity_builders'][] = '::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_workflow_note'] = [ + '#type' => 'item', + '#description' => $this->t('After disabling moderation, any existing forward drafts will be accessible via the "Revisions" tab.'), + '#access' => !empty($selected_workflow) + ]; } - } - /** - * {@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 +110,33 @@ 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; + } + if ($original_workflow_id) { + /* @var \Drupal\workflows\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\workflows\WorkflowInterface $workflow */ + $workflow = $this->entityTypeManager->getStorage('workflow')->load($new_workflow_id); + $workflow->getTypePlugin()->addEntityTypeAndBundle($entity_type_id, $bundle_id); + $workflow->save(); } - - parent::submitForm($form, $form_state); - - drupal_set_message($this->t('Your settings have been saved.')); } } diff --git a/core/modules/content_moderation/src/Form/EntityModerationForm.php b/core/modules/content_moderation/src/Form/EntityModerationForm.php index 39baec028cc9f1d480a5dd8fbfdd0eb6681bb760..6f9de1f5ef2c1c53f561c8f137830df69195acb4 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\workflows\Transition; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -30,13 +29,6 @@ class EntityModerationForm extends FormBase { */ protected $validation; - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - /** * EntityModerationForm constructor. * @@ -44,13 +36,10 @@ class EntityModerationForm extends FormBase { * 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\workflows\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->isDefaultRevisionState()) { $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 32d7a48373fa184682ade6eb842e896c547b3f0c..0000000000000000000000000000000000000000 --- 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 f153f1f938272764b9d71f1b45d4b7b3f8801d9e..0000000000000000000000000000000000000000 --- 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 8322c18e77d5fb785a9071417c47d2f7f2bca617..0000000000000000000000000000000000000000 --- 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 ed33902a5253dba3fa53a49e19908a311854d675..e8ebf39ebef3f4fe8587edb55ee072fff18102ab 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; @@ -19,16 +20,24 @@ 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,22 @@ 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)->isPublishedState(); + } + + /** + * {@inheritdoc} + */ + 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 95a658e65d6c5c7406bc54a3ccb8aac71c89f9d2..531057ecdb7f3d8b61fe6e0272cd5c7f15eecdf6 100644 --- a/core/modules/content_moderation/src/ModerationInformationInterface.php +++ b/core/modules/content_moderation/src/ModerationInformationInterface.php @@ -126,4 +126,15 @@ public function hasForwardRevision(ContentEntityInterface $entity); */ public function isLiveRevision(ContentEntityInterface $entity); + /** + * Gets the workflow for the given content entity. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The content entity to get the workflow for. + * + * @return \Drupal\workflows\WorkflowInterface|null + * The workflow entity. NULL if there is no workflow. + */ + 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 b2c86d7c727acc502ca44cd309f4cc46dc9ba0fd..0000000000000000000000000000000000000000 --- 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 99f664fa97cd3dad178df3fcf7530e91ef3962a2..0000000000000000000000000000000000000000 --- 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 91b5b13cafdcc939132019b528c106f6d28f0254..0000000000000000000000000000000000000000 --- 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 027684c2c6f9fa8ee614dee51e8f04e5c778805f..201239fc950e0e8a53c16fd996cad7d8c5267dbe 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\workflows\Entity\Workflow; /** * Defines a class for dynamic permissions based on transitions. @@ -22,19 +21,17 @@ 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\workflows\WorkflowInterface $workflow */ + foreach (Workflow::loadMultipleByType('content_moderation') as $id => $workflow) { + foreach ($workflow->getTransitions() as $transition) { + $perms['use ' . $workflow->id() . ' transition ' . $transition->id()] = [ + 'title' => $this->t('Use %transition transition from %workflow workflow.', [ + '%transition' => $transition->label(), + '%workflow' => $workflow->label(), + ]), + ]; + } } return $perms; diff --git a/core/modules/content_moderation/src/Plugin/Field/FieldFormatter/ContentModerationStateFormatter.php b/core/modules/content_moderation/src/Plugin/Field/FieldFormatter/ContentModerationStateFormatter.php new file mode 100644 index 0000000000000000000000000000000000000000..4e9da164c026d3110734d2b8f0f4b2841c1091e7 --- /dev/null +++ b/core/modules/content_moderation/src/Plugin/Field/FieldFormatter/ContentModerationStateFormatter.php @@ -0,0 +1,48 @@ +getEntity(); + $workflow = $entity->workflow->entity; + foreach ($items as $delta => $item) { + if (!$item->isEmpty()) { + $elements[$delta] = [ + '#markup' => $workflow->getState($item->value)->label(), + ]; + } + } + + return $elements; + } + + /** + * {@inheritdoc} + */ + public static function isApplicable(FieldDefinitionInterface $field_definition) { + return $field_definition->getName() === 'moderation_state'; + } + +} 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 d6dc89dcade8dce7ab0e4fc99bfcec0347527a0b..8c17d8281725dc651346bd79483105ce87ae23be 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" * } * ) */ @@ -36,20 +34,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. * @@ -64,13 +48,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. * @@ -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\workflows\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->isPublishedState(), '#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']; } } @@ -249,7 +213,7 @@ public static function processActions($element, FormStateInterface $form_state, * {@inheritdoc} */ public static function isApplicable(FieldDefinitionInterface $field_definition) { - return parent::isApplicable($field_definition) && $field_definition->getName() === 'moderation_state'; + return $field_definition->getName() === '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 c32521c44ae8d0d3e7c807eedd2f1ec80ed911bf..036f346f772b22947157c95b7b7df66a57cb2060 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; } @@ -32,6 +32,7 @@ protected function getModerationState() { ->condition('content_entity_type_id', $entity->getEntityTypeId()) ->condition('content_entity_id', $entity->id()) ->condition('content_entity_revision_id', $entity->getRevisionId()) + ->condition('workflow', $moderation_info->getWorkFlowForEntity($entity)->id()) ->allRevisions() ->sort('revision_id', 'DESC') ->execute(); @@ -53,17 +54,15 @@ protected function getModerationState() { } } - 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(); } } @@ -93,10 +92,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 ca75604a8547a4df9065d3abfa954044b1cb6710..d90e19e7477c7ccb705edde52cb70e5eb408c167 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 0000000000000000000000000000000000000000..e61f25ea1f969639153594abc760eca0e50f027e --- /dev/null +++ b/core/modules/content_moderation/src/Plugin/WorkflowType/ContentModeration.php @@ -0,0 +1,166 @@ +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, WorkflowInterface $workflow, StateInterface $state = NULL) { + /** @var \Drupal\content_moderation\ContentModerationState $state */ + $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->isPublishedState() : 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->isDefaultRevisionState() : 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() { + // This plugin does not store anything per transition. + return [ + 'states' => [], + 'entity_types' => [], + ]; + } + + /** + * @inheritDoc + */ + public function calculateDependencies() { + // @todo : Implement calculateDependencies() method. + return []; + } + +} diff --git a/core/modules/content_moderation/src/Routing/EntityTypeModerationRouteProvider.php b/core/modules/content_moderation/src/Routing/EntityTypeModerationRouteProvider.php index c722a67ee110ddba8e30ab2f1af47cd7abd18597..d1dcd2b11fd9a3e14d54d4ea99aee5f736ce9a99 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 2e2a4e24506e6c291d4d730f26630af542594cad..e3d337639772833c5b40c3a3d95cdf8cbb8620d3 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\workflows\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 ' . $transition->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 5ef0dd17d24b5f876509e442b249b328b47225ce..1acbf052fd064607aa015bfa94a7fcab83d9564c 100644 --- a/core/modules/content_moderation/src/StateTransitionValidationInterface.php +++ b/core/modules/content_moderation/src/StateTransitionValidationInterface.php @@ -10,20 +10,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. * @@ -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\workflows\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 d6c92b96a8a96d2d21d3d2f0aad20cd829b424b5..16da4e4548032a0dcfc01d737b16d0d883ebb00c 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 0d403563664f83dcfa3f9f4f06d050c46797c15f..a78c1048983c98a217ee8fef8c1e0be6ef56189e 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 03a4b0ee2d91c1d389ceb3ce08a2d0f1732ed205..e42f536b91d841d7c71c76e4dd3b59994cfc92de 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' => 'editorial']; $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 e2069b474821279f8bf5047a532654789c1c7b51..a89ec9fd240a8e96179772d5b570eb3e6d367438 100644 --- a/core/modules/content_moderation/src/Tests/ModerationStateNodeTest.php +++ b/core/modules/content_moderation/src/Tests/ModerationStateNodeTest.php @@ -4,6 +4,7 @@ use Drupal\Core\Url; use Drupal\node\Entity\Node; +use Drupal\workflows\Entity\Workflow; /** * Tests general content moderation workflow for nodes. @@ -18,13 +19,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 +30,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 +43,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 +52,40 @@ 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', ''); + // Ensure the parent environment is up-to-date. + // @see content_moderation_workflow_insert() + \Drupal::service('entity_type.bundle.info')->clearCachedBundles(); + \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; } + $this->assertEqual(NULL, $node->moderation_state->value); + + // \Drupal\content_moderation\Form\BundleModerationConfigurationForm() + // should not list workflows with no states. + $workflow = Workflow::create(['id' => 'stateless', 'label' => 'Stateless', 'type' => 'content_moderation']); + $workflow->save(); - $node = reset($nodes); - $this->assertEqual(NULL, $node->moderation_state->target_id); + $this->drupalGet('admin/structure/types/manage/moderated_content/moderation'); + $this->assertNoText('Stateless'); + $workflow + ->addState('draft', 'Draft') + ->addState('published', 'Published') + ->addTransition('publish', 'Publish', ['draft', 'published'], 'published') + ->save(); + $this->drupalGet('admin/structure/types/manage/moderated_content/moderation'); + $this->assertText('Stateless'); } /** diff --git a/core/modules/content_moderation/src/Tests/ModerationStateNodeTypeTest.php b/core/modules/content_moderation/src/Tests/ModerationStateNodeTypeTest.php index debb32c81ad6eff34028a9eb9c74a6aed8204321..39e1a2c3f03b6fa441edb8ccc37b9a32f15b2cc9 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'] = 'editorial'; + $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 3c5fd1471baba1ce576a930ca1cb50c16e2220d9..0000000000000000000000000000000000000000 --- 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 ddd275e6e4b4aced022c3a7f8cda1f582ad92ece..22307dfa8553acca3e77ba88bc35e111f9a0f6a3 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,18 +29,15 @@ abstract class ModerationStateTestBase extends WebTestBase { * @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', 'view latest version', 'view any unpublished content', 'access content overview', + 'use editorial transition create_new_draft', + 'use editorial transition publish', ]; /** @@ -67,6 +63,21 @@ protected function setUp() { $this->drupalPlaceBlock('local_actions_block', ['id' => 'actions_block']); } + /** + * Gets the permission machine name for a transition. + * + * @param string $workflow_id + * The workflow ID. + * @param string $transition_id + * The transition ID. + * + * @return string + * The permission machine name for a transition. + */ + protected function getWorkflowTransitionPermission($workflow_id, $transition_id) { + return 'use ' . $workflow_id . ' transition ' . $transition_id; + } + /** * Creates a content-type from the UI. * @@ -76,12 +87,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 = 'editorial') { $this->drupalGet('admin/structure/types'); $this->clickLink('Add content type'); $edit = [ @@ -91,7 +100,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 +109,16 @@ 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 = 'editorial') { + $edit['workflow'] = $workflow_id; + $this->drupalPostForm('admin/structure/types/manage/' . $content_type_id . '/moderation', $edit, t('Save')); + // Ensure the parent environment is up-to-date. + // @see content_moderation_workflow_insert() + \Drupal::service('entity_type.bundle.info')->clearCachedBundles(); + \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 703561bf3d7f9219569e6eff5db88b8c7808224c..0000000000000000000000000000000000000000 --- 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 1b054067bddd048dc8cc877f67c2e51974740103..7392a7e13de8d80e3a5b0f888bb881007c1e2868 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,7 @@ 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', + 'use editorial transition create_new_draft', 'view latest version', 'view any unpublished content', ]); @@ -92,7 +76,7 @@ public function testPageAccess() { // Now make another user, who should not be able to see forward revisions. $user = $this->createUser([ - 'use published_draft transition', + 'use editorial transition create_new_draft', ]); $this->drupalLogin($user); diff --git a/core/modules/content_moderation/src/ViewsData.php b/core/modules/content_moderation/src/ViewsData.php index 19bcfa508eb2bd929d98e0fe3c899a77b7754738..b357abcf26044d89ca97f6c19ab1d6f9c308174b 100644 --- a/core/modules/content_moderation/src/ViewsData.php +++ b/core/modules/content_moderation/src/ViewsData.php @@ -204,6 +204,7 @@ public function getViewsData() { ], ], ], + 'field' => ['default_formatter' => 'content_moderation_state'], ]; $revision_table = $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable(); @@ -222,6 +223,7 @@ public function getViewsData() { ], ], ], + 'field' => ['default_formatter' => 'content_moderation_state'], ]; } diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.latest.yml b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.latest.yml index 46a64ab583c2571f37f0c4c69992ff1db9df107c..62e972e7dd3ed39bf525f6c43219aaa16f36ba63 100644 --- a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.latest.yml +++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.latest.yml @@ -300,9 +300,7 @@ display: empty_zero: false hide_alter_empty: true click_sort_column: target_id - type: entity_reference_label - settings: - link: true + type: content_moderation_state group_column: target_id group_columns: { } group_rows: true diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_base_table_test.yml b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_base_table_test.yml index 6f9525116646181f4ced4dcc69f4e4000d3f6c3e..343806f90f5ad3340875b4b1ffd769be4ef51f87 100644 --- a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_base_table_test.yml +++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_base_table_test.yml @@ -193,9 +193,7 @@ display: empty_zero: false hide_alter_empty: true click_sort_column: target_id - type: entity_reference_label - settings: - link: false + type: content_moderation_state group_column: target_id group_columns: { } group_rows: true @@ -258,9 +256,7 @@ display: empty_zero: false hide_alter_empty: true click_sort_column: target_id - type: entity_reference_label - settings: - link: false + type: content_moderation_state group_column: target_id group_columns: { } group_rows: true @@ -323,8 +319,7 @@ display: empty_zero: false hide_alter_empty: true click_sort_column: target_id - type: entity_reference_entity_id - settings: { } + type: string group_column: target_id group_columns: { } group_rows: true diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_latest_revision.yml b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_latest_revision.yml index 76733940b50d84f1ce28908678afaf7e0b97970b..4727efa28d87e7dfd546cfe82060109682462eed 100644 --- a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_latest_revision.yml +++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_latest_revision.yml @@ -306,7 +306,7 @@ display: empty_zero: false hide_alter_empty: true click_sort_column: target_id - type: entity_reference_entity_id + type: string settings: { } group_column: target_id group_columns: { } @@ -370,7 +370,7 @@ display: empty_zero: false hide_alter_empty: true click_sort_column: target_id - type: entity_reference_entity_id + type: string settings: { } group_column: target_id group_columns: { } diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_revision_test.yml b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_revision_test.yml index 2362098d604cda9fe2b8bfb964d17e929fb595c1..78fca3827317a14f280c2bb9cee621e403643dc5 100644 --- a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_revision_test.yml +++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_revision_test.yml @@ -191,7 +191,7 @@ display: empty_zero: false hide_alter_empty: true click_sort_column: target_id - type: entity_reference_entity_id + type: string settings: { } group_column: target_id group_columns: { } diff --git a/core/modules/content_moderation/tests/src/Functional/LatestRevisionViewsFilterTest.php b/core/modules/content_moderation/tests/src/Functional/LatestRevisionViewsFilterTest.php index 77ae04627fc1b48eddc418f26512a27d7aaa4712..cf14b23297efd36795892bbb20ccf507aa0b099f 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\workflows\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,9 @@ 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('editorial'); + $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'test'); + $workflow->save(); // Make a node that is only ever in Draft. /** @var Node $node_1 */ @@ -55,7 +57,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 +67,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 +81,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 799d89a5117062c07c0b69291f5141cf54551ffe..7d2f74666e50042057cfc4e0e45e3e195eb96d03 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\workflows\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,26 +50,26 @@ 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. $this->drupalGet('/latest'); $page = $this->getSession()->getPage(); - $this->assertTrue($page->hasLink('Draft')); - $this->assertTrue($page->hasLink('Archived')); - $this->assertFalse($page->hasLink('Published')); + $this->assertTrue($page->hasContent('Draft')); + $this->assertTrue($page->hasContent('Archived')); + $this->assertFalse($page->hasContent('Published')); // Now log in as an admin and test the same thing. $permissions = [ 'access content', 'view all revisions', - 'administer moderation states', + 'administer content moderation', ]; $admin1 = $this->drupalCreateUser($permissions); $this->drupalLogin($admin1); @@ -76,9 +77,9 @@ public function testViewShowsCorrectStates() { $this->drupalGet('/latest'); $page = $this->getSession()->getPage(); $this->assertEquals(200, $this->getSession()->getStatusCode()); - $this->assertTrue($page->hasLink('Draft')); - $this->assertTrue($page->hasLink('Archived')); - $this->assertFalse($page->hasLink('Published')); + $this->assertTrue($page->hasContent('Draft')); + $this->assertTrue($page->hasContent('Archived')); + $this->assertFalse($page->hasContent('Published')); } /** @@ -98,9 +99,11 @@ protected function createNodeType($label, $machine_name) { 'type' => $machine_name, 'label' => $label, ]); - $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE); $node_type->save(); + $workflow = Workflow::load('editorial'); + $workflow->getTypePlugin()->addEntityTypeAndBundle('node', $machine_name); + $workflow->save(); 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 67ce175148bbad63c6dac8999c69c1238aa36071..0000000000000000000000000000000000000000 --- 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 67a7a749805fd348a0a72c7b9f9697e2c38ce656..b5b5a75157ae5db64d7b73c109e4c9a6ddf8e0f0 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php @@ -3,7 +3,6 @@ namespace Drupal\Tests\content_moderation\Kernel; use Drupal\content_moderation\Entity\ContentModerationState; -use Drupal\content_moderation\Entity\ModerationState; use Drupal\entity_test\Entity\EntityTestBundle; use Drupal\entity_test\Entity\EntityTestWithBundle; use Drupal\KernelTests\KernelTestBase; @@ -11,6 +10,7 @@ use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; use Drupal\node\NodeInterface; +use Drupal\workflows\Entity\Workflow; /** * Tests links between a content entity and a content_moderation_state entity. @@ -31,6 +31,7 @@ class ContentModerationStateTest extends KernelTestBase { 'language', 'content_translation', 'text', + 'workflows', ]; /** @@ -54,24 +55,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('editorial'); + $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); @@ -80,7 +82,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. @@ -88,11 +90,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); @@ -110,10 +112,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('editorial'); + $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example'); + $workflow->save(); + $english_node = Node::create([ 'type' => 'example', 'title' => 'Test title', @@ -122,7 +126,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. @@ -131,34 +135,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); @@ -167,7 +171,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'); @@ -175,7 +179,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'); @@ -189,9 +193,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); @@ -202,9 +206,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()); @@ -231,14 +235,12 @@ public function testNonTranslatableEntityTypeModeration() { $entity_test_bundle = EntityTestBundle::create([ 'id' => 'example', ]); - $entity_test_bundle->setThirdPartySetting('content_moderation', 'enabled', TRUE); - $entity_test_bundle->setThirdPartySetting('content_moderation', 'allowed_moderation_states', [ - 'draft', - 'published' - ]); - $entity_test_bundle->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft'); $entity_test_bundle->save(); + $workflow = Workflow::load('editorial'); + $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_with_bundle', 'example'); + $workflow->save(); + // Check that the tested entity type is not translatable. $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_with_bundle'); $this->assertFalse($entity_type->isTranslatable(), 'The test entity type is not translatable.'); @@ -248,12 +250,12 @@ public function testNonTranslatableEntityTypeModeration() { 'type' => 'example' ]); $entity_test_with_bundle->save(); - $this->assertEquals('draft', $entity_test_with_bundle->moderation_state->entity->id()); + $this->assertEquals('draft', $entity_test_with_bundle->moderation_state->value); - $entity_test_with_bundle->moderation_state->target_id = 'published'; + $entity_test_with_bundle->moderation_state->value = 'published'; $entity_test_with_bundle->save(); - $this->assertEquals('published', EntityTestWithBundle::load($entity_test_with_bundle->id())->moderation_state->entity->id()); + $this->assertEquals('published', EntityTestWithBundle::load($entity_test_with_bundle->id())->moderation_state->value); } /** @@ -275,14 +277,12 @@ public function testNonLangcodeEntityTypeModeration() { $entity_test_bundle = EntityTestBundle::create([ 'id' => 'example', ]); - $entity_test_bundle->setThirdPartySetting('content_moderation', 'enabled', TRUE); - $entity_test_bundle->setThirdPartySetting('content_moderation', 'allowed_moderation_states', [ - 'draft', - 'published' - ]); - $entity_test_bundle->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft'); $entity_test_bundle->save(); + $workflow = Workflow::load('editorial'); + $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_with_bundle', 'example'); + $workflow->save(); + // Check that the tested entity type is not translatable. $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_with_bundle'); $this->assertFalse($entity_type->isTranslatable(), 'The test entity type is not translatable.'); @@ -292,12 +292,12 @@ public function testNonLangcodeEntityTypeModeration() { 'type' => 'example' ]); $entity_test_with_bundle->save(); - $this->assertEquals('draft', $entity_test_with_bundle->moderation_state->entity->id()); + $this->assertEquals('draft', $entity_test_with_bundle->moderation_state->value); - $entity_test_with_bundle->moderation_state->target_id = 'published'; + $entity_test_with_bundle->moderation_state->value = 'published'; $entity_test_with_bundle->save(); - $this->assertEquals('published', EntityTestWithBundle::load($entity_test_with_bundle->id())->moderation_state->entity->id()); + $this->assertEquals('published', EntityTestWithBundle::load($entity_test_with_bundle->id())->moderation_state->value); } /** diff --git a/core/modules/content_moderation/tests/src/Kernel/EntityOperationsTest.php b/core/modules/content_moderation/tests/src/Kernel/EntityOperationsTest.php index 929356ec771cf4de75fa32be91d3e02667fa98a9..60e9edf648bab6e3612a35b94f24e4dacb8ee180 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\workflows\Entity\Workflow; /** * @coversDefaultClass \Drupal\content_moderation\EntityOperations @@ -23,6 +23,7 @@ class EntityOperationsTest extends KernelTestBase { 'node', 'user', 'system', + 'workflows', ]; /** @@ -47,8 +48,10 @@ protected function createNodeType() { 'type' => 'page', 'label' => 'Page', ]); - $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE); $node_type->save(); + $workflow = Workflow::load('editorial'); + $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 89c84f9c7523ca12eeaccf3dacebe0f4692ba0c7..6f209d13ae09cf8ce97d5c931a05c50a4d49891c 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\workflows\Entity\Workflow; /** * @coversDefaultClass \Drupal\content_moderation\ParamConverter\EntityRevisionConverter @@ -19,6 +20,7 @@ class EntityRevisionConverterTest extends KernelTestBase { 'system', 'content_moderation', 'node', + 'workflows', ]; /** @@ -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('editorial'); + $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 97e61f1ed8c88bac100c6e4d7ade87d348b47476..7c4f97d391e7a541350d880e94fad0b02d2a1ff3 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\workflows\Entity\Workflow; /** * @coversDefaultClass \Drupal\content_moderation\Plugin\Validation\Constraint\ModerationStateConstraintValidator @@ -23,6 +24,7 @@ class EntityStateChangeValidationTest extends KernelTestBase { 'system', 'language', 'content_translation', + 'workflows', ]; /** @@ -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('editorial'); + $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('editorial'); + $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('editorial'); + $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('editorial'); + $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 f312cde2499eb162dbcf0e99ec7fca5d4e3ba75a..0000000000000000000000000000000000000000 --- 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 c57963e070872571b617a740b93534e4aa7d1d44..3f984da4552448e5ed6b2609444bc642e64a8fff 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\workflows\Entity\Workflow; /** * @coversDefaultClass \Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList @@ -22,6 +23,7 @@ class ModerationStateFieldItemListTest extends KernelTestBase { 'user', 'system', 'language', + 'workflows', ]; /** @@ -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('editorial'); + $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 c869619c9555a7df1de76fbc5f30d04d6a364f2b..6b127c87cd410b9525094d4d02d3ff9b6dac3cb1 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\workflows\Entity\Workflow; /** * Tests the views integration of content_moderation. @@ -21,6 +22,7 @@ class ViewsDataIntegrationTest extends ViewsKernelTestBase { 'content_moderation_test_views', 'node', 'content_moderation', + 'workflows', ]; /** @@ -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('editorial'); + $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 ff46e41585bee9cac331ded502b234b777147df4..d7a737302b339054cc9ed44bb6d3fffb72927632 100644 --- a/core/modules/content_moderation/tests/src/Unit/ModerationInformationTest.php +++ b/core/modules/content_moderation/tests/src/Unit/ModerationInformationTest.php @@ -3,13 +3,14 @@ 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; +use Drupal\workflows\WorkflowInterface; /** * @coversDefaultClass \Drupal\content_moderation\ModerationInformation @@ -30,43 +31,42 @@ 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. * * @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) { + $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, $expected) { + $moderation_information = new ModerationInformation($this->getEntityTypeManager(), $this->setupModerationBundleInfo('test_bundle', $workflow)); $entity_type = new ContentEntityType([ 'id' => 'test_entity_type', @@ -77,50 +77,55 @@ public function testIsModeratedEntity($status) { $entity->getEntityType()->willReturn($entity_type); $entity->bundle()->willReturn('test_bundle'); - $this->assertEquals($status, $moderation_information->isModeratedEntity($entity->reveal())); + $this->assertEquals($expected, $moderation_information->isModeratedEntity($entity->reveal())); } /** - * @covers ::isModeratedEntity + * @dataProvider providerWorkflow + * @covers ::getWorkFlowForEntity */ - public function testIsModeratedEntityForNonBundleEntityType() { - $entity_type = new ContentEntityType([ - 'id' => 'test_entity_type', - ]); + public function testGetWorkFlowForEntity($workflow) { + $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); + if ($workflow) { + $workflow_entity = $this->prophesize(WorkflowInterface::class)->reveal(); + $workflow_storage = $this->prophesize(EntityStorageInterface::class); + $workflow_storage->load('workflow')->willReturn($workflow_entity)->shouldBeCalled(); + $entity_type_manager->getStorage('workflow')->willReturn($workflow_storage->reveal()); + } + else { + $workflow_entity = NULL; + } + $moderation_information = new ModerationInformation($entity_type_manager->reveal(), $this->setupModerationBundleInfo('test_bundle', $workflow)); $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()); + $entity->getEntityTypeId()->willReturn('test_entity_type'); + $entity->bundle()->willReturn('test_bundle'); - $this->assertEquals(FALSE, $moderation_information->isModeratedEntity($entity->reveal())); + $this->assertEquals($workflow_entity, $moderation_information->getWorkFlowForEntity($entity->reveal())); } /** - * @dataProvider providerBoolean + * @dataProvider providerWorkflow * @covers ::shouldModerateEntitiesOfBundle */ - public function testShouldModerateEntities($status) { + public function testShouldModerateEntities($workflow, $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)); - $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, FALSE], + ['workflow', TRUE], ]; } diff --git a/core/modules/content_moderation/tests/src/Unit/StateTransitionValidationTest.php b/core/modules/content_moderation/tests/src/Unit/StateTransitionValidationTest.php index b0574788724d160713df62ff4dc544e9de740b4c..e518aedeab1fdd51d250cf975076c5999c90114e 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\workflows\Entity\Workflow; +use Drupal\workflows\WorkflowTypeInterface; +use Drupal\workflows\WorkflowTypeManager; use Prophecy\Argument; /** @@ -17,216 +18,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. * @@ -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_type->decorateTransition(Argument::any())->willReturnArgument(0); + $workflow_manager = $this->prophesize(WorkflowTypeManager::class); + $workflow_manager->createInstance('content_moderation', Argument::any())->willReturn($workflow_type->reveal()); + $container->set('plugin.manager.workflows.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', ['draft'], 'draft') + ->addTransition('review', 'review', ['draft'], 'needs_review') + ->addTransition('publish', 'publish', ['needs_review', 'published'], 'published'); + $moderation_info = $this->prophesize(ModerationInformationInterface::class); + $moderation_info->getWorkFlowForEntity($entity)->willReturn($workflow); + return $moderation_info->reveal(); } /** @@ -261,37 +87,15 @@ 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 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 draft', FALSE, FALSE]; // The user has some other permission that doesn't matter. - $ret[] = ['draft', 'draft', 'use draft__needs_review transition', 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[] = ['draft', 'draft', 'use process transition 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/system/tests/modules/entity_test/config/schema/entity_test.schema.yml b/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml index 464a007ac61290092d9b413853efc1c3d3871f38..d34d94940dc0a5cbc29060a4d83232fabf68f55a 100644 --- a/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml +++ b/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml @@ -27,19 +27,3 @@ entity_test.entity_test_bundle.*: description: type: text label: 'Description' - -entity_test.entity_test_bundle.*.third_party.content_moderation: - type: mapping - label: 'Enable moderation states for this entity test type' - mapping: - enabled: - type: boolean - label: 'Moderation states enabled' - allowed_moderation_states: - type: sequence - sequence: - type: string - label: 'Moderation state' - default_moderation_state: - type: string - label: 'Moderation state for new entity test' diff --git a/core/modules/workflows/config/schema/workflows.schema.yml b/core/modules/workflows/config/schema/workflows.schema.yml new file mode 100644 index 0000000000000000000000000000000000000000..e19eb6b892a5a9b6df800cd02aee256baf33e439 --- /dev/null +++ b/core/modules/workflows/config/schema/workflows.schema.yml @@ -0,0 +1,51 @@ +workflows.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: mapping + label: 'Transition from state to state' + mapping: + label: + type: label + label: 'Transition label' + from: + type: sequence + label: 'From state IDs' + sequence: + type: string + label: 'From state ID' + to: + type: string + label: 'To state ID' + weight: + type: integer + label: 'Weight' diff --git a/core/modules/workflows/src/Annotation/WorkflowType.php b/core/modules/workflows/src/Annotation/WorkflowType.php new file mode 100644 index 0000000000000000000000000000000000000000..2aa3ff9eb2953fcb24ead3bd1e3613588364ff73 --- /dev/null +++ b/core/modules/workflows/src/Annotation/WorkflowType.php @@ -0,0 +1,44 @@ +states[$state_id])) { + throw new \InvalidArgumentException("The state '$state_id' already exists in workflow '{$this->id()}'"); + } + if (preg_match('/[^a-z0-9_]+/', $state_id)) { + throw new \InvalidArgumentException("The state ID '$state_id' must contain only lowercase letters, numbers, and underscores"); + } + $this->states[$state_id] = [ + 'label' => $label, + 'weight' => $this->getNextWeight($this->states), + ]; + 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); + } + /** @var \Drupal\workflows\StateInterface[] $states */ + $states = array_combine($state_ids, array_map([$this, 'getState'], $state_ids)); + if (count($states) > 1) { + // Sort states by weight and then label. + $weights = $labels = []; + foreach ($states as $id => $state) { + $weights[$id] = $state->weight(); + $labels[$id] = $state->label(); + } + array_multisort( + $weights, SORT_NUMERIC, SORT_ASC, + $labels, SORT_NATURAL, SORT_ASC + ); + $states = array_replace($weights, $states); + } + 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'] + ); + 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()}'"); + } + if (count($this->states) === 1) { + throw new \InvalidArgumentException("The state '$state_id' can not be deleted from workflow '{$this->id()}' as it is the only state"); + } + + foreach ($this->transitions as $transition_id => $transition) { + $from_key = array_search($state_id, $transition['from'], TRUE); + if ($from_key !== FALSE) { + // Remove state from the from array. + unset($transition['from'][$from_key]); + } + if (empty($transition['from']) || $transition['to'] === $state_id) { + $this->deleteTransition($transition_id); + } + elseif ($from_key !== FALSE) { + $this->setTransitionFromStates($transition_id, $transition['from']); + } + } + unset($this->states[$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($transition_id, $label, array $from_state_ids, $to_state_id) { + if (isset($this->transitions[$transition_id])) { + throw new \InvalidArgumentException("The transition '$transition_id' already exists in workflow '{$this->id()}'"); + } + if (preg_match('/[^a-z0-9_]+/', $transition_id)) { + throw new \InvalidArgumentException("The transition ID '$transition_id' must contain only lowercase letters, numbers, and underscores"); + } + + if (!$this->hasState($to_state_id)) { + throw new \InvalidArgumentException("The state '$to_state_id' does not exist in workflow '{$this->id()}'"); + } + $this->transitions[$transition_id] = [ + 'label' => $label, + 'from' => [], + 'to' => $to_state_id, + // Always add to the end. + 'weight' => $this->getNextWeight($this->transitions), + ]; + + try { + $this->setTransitionFromStates($transition_id, $from_state_ids); + } + catch (\InvalidArgumentException $e) { + unset($this->transitions[$transition_id]); + throw $e; + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getTransitions(array $transition_ids = NULL) { + if ($transition_ids === NULL) { + $transition_ids = array_keys($this->transitions); + } + /** @var \Drupal\workflows\TransitionInterface[] $transitions */ + $transitions = array_combine($transition_ids, array_map([$this, 'getTransition'], $transition_ids)); + if (count($transitions) > 1) { + // Sort transitions by weights and then labels. + $weights = $labels = []; + foreach ($transitions as $id => $transition) { + $weights[$id] = $transition->weight(); + $labels[$id] = $transition->label(); + } + array_multisort( + $weights, SORT_NUMERIC, SORT_ASC, + $labels, SORT_NATURAL, SORT_ASC + ); + $transitions = array_replace($weights, $transitions); + } + return $transitions; + } + + /** + * {@inheritdoc} + */ + public function getTransition($transition_id) { + if (!isset($this->transitions[$transition_id])) { + throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow '{$this->id()}'"); + } + $transition = new Transition( + $this, + $transition_id, + $this->transitions[$transition_id]['label'], + $this->transitions[$transition_id]['from'], + $this->transitions[$transition_id]['to'], + $this->transitions[$transition_id]['weight'] + ); + return $this->getTypePlugin()->decorateTransition($transition); + } + + /** + * {@inheritdoc} + */ + public function hasTransition($transition_id) { + return isset($this->transitions[$transition_id]); + } + + /** + * {@inheritdoc} + */ + public function getTransitionsForState($state_id, $direction = 'from') { + $transition_ids = array_keys(array_filter($this->transitions, function ($transition) use ($state_id, $direction) { + return in_array($state_id, (array) $transition[$direction], TRUE); + })); + return $this->getTransitions($transition_ids); + } + + /** + * {@inheritdoc} + */ + public function getTransitionFromStateToState($from_state_id, $to_state_id) { + $transition_id = $this->getTransitionIdFromStateToState($from_state_id, $to_state_id); + if (empty($transition_id)) { + throw new \InvalidArgumentException("The transition from '$from_state_id' to '$to_state_id' does not exist in workflow '{$this->id()}'"); + } + return $this->getTransition($transition_id); + } + + /** + * {@inheritdoc} + */ + public function hasTransitionFromStateToState($from_state_id, $to_state_id) { + return !empty($this->getTransitionIdFromStateToState($from_state_id, $to_state_id)); + } + + /** + * Gets the transition ID from state to state. + * + * @param string $from_state_id + * The state ID to transition from. + * @param string $to_state_id + * The state ID to transition to. + * + * @return string|null + * The transition ID, or NULL if no transition exists. + */ + protected function getTransitionIdFromStateToState($from_state_id, $to_state_id) { + foreach ($this->transitions as $transition_id => $transition) { + if (in_array($from_state_id, $transition['from'], TRUE) && $transition['to'] === $to_state_id) { + return $transition_id; + } + } + return NULL; + } + + /** + * {@inheritdoc} + */ + public function setTransitionLabel($transition_id, $label) { + if (isset($this->transitions[$transition_id])) { + $this->transitions[$transition_id]['label'] = $label; + } + else { + throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow '{$this->id()}'"); + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function setTransitionWeight($transition_id, $weight) { + if (isset($this->transitions[$transition_id])) { + $this->transitions[$transition_id]['weight'] = $weight; + } + else { + throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow '{$this->id()}'"); + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function setTransitionFromStates($transition_id, array $from_state_ids) { + if (!isset($this->transitions[$transition_id])) { + throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow '{$this->id()}'"); + } + + // Ensure that the states exist. + foreach ($from_state_ids as $from_state_id) { + if (!$this->hasState($from_state_id)) { + throw new \InvalidArgumentException("The state '$from_state_id' does not exist in workflow '{$this->id()}'"); + } + if ($this->hasTransitionFromStateToState($from_state_id, $this->transitions[$transition_id]['to'])) { + $transition = $this->getTransitionFromStateToState($from_state_id, $this->transitions[$transition_id]['to']); + if ($transition_id !== $transition->id()) { + throw new \InvalidArgumentException("The '{$transition->id()}' transition already allows '$from_state_id' to '{$this->transitions[$transition_id]['to']}' transitions in workflow '{$this->id()}'"); + } + } + } + + // Preserve the order of the state IDs in the from value and don't save any + // keys. + $from_state_ids = array_values($from_state_ids); + sort($from_state_ids); + $this->transitions[$transition_id]['from'] = $from_state_ids; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function deleteTransition($transition_id) { + if (isset($this->transitions[$transition_id])) { + unset($this->transitions[$transition_id]); + $this->getTypePlugin()->deleteTransition($transition_id); + } + else { + throw new \InvalidArgumentException("The transition '$transition_id' does not exist 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('plugin.manager.workflows.type'), $this->type, $this->type_settings); + } + return $this->pluginCollection; + } + + /** + * Loads all workflows of the provided type. + * + * @param string $type + * The workflow type to load all workflows for. + * + * @return static[] + * An array of workflow objects of the provided workflow type, indexed by + * their IDs. + * + * @see \Drupal\workflows\Annotation\WorkflowType + */ + public static function loadMultipleByType($type) { + return self::loadMultiple(\Drupal::entityQuery('workflow')->condition('type', $type)->execute()); + } + + /** + * Gets the weight for a new state or transition. + * + * @param array $items + * An array of states or transitions information where each item has a + * 'weight' key with a numeric value. + * + * @return int + * The weight for a new item in the array so that it has the highest weight. + */ + protected function getNextWeight(array $items) { + return array_reduce($items, function ($carry, $item) { + return max($carry, $item['weight'] + 1); + }, 0); + } + + /** + * {@inheritdoc} + */ + public function status() { + // In order for a workflow to be usable it must have at least one state. + return !empty($this->status) && !empty($this->states); + } + +} diff --git a/core/modules/workflows/src/Form/WorkflowAddForm.php b/core/modules/workflows/src/Form/WorkflowAddForm.php new file mode 100644 index 0000000000000000000000000000000000000000..c779b8fec1a4a3ef97d346c815db7f1c1a649157 --- /dev/null +++ b/core/modules/workflows/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('plugin.manager.workflows.type') + ); + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + + /* @var \Drupal\workflows\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\workflows\WorkflowInterface $workflow */ + $workflow = $this->entity; + $workflow->save(); + drupal_set_message($this->t('Created the %label Workflow. In order for the workflow to be enabled there needs to be at least one state.', [ + '%label' => $workflow->label(), + ])); + $form_state->setRedirectUrl($workflow->toUrl('add-state-form')); + } + + /** + * {@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\workflows\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/workflows/src/Form/WorkflowDeleteForm.php similarity index 64% rename from core/modules/content_moderation/src/Form/ModerationStateDeleteForm.php rename to core/modules/workflows/src/Form/WorkflowDeleteForm.php index 43e2b36d9666de41320016dd54a3d37f62cdf4a6..b9b833132b73ffb8c79b9a6eed722eabc336be97 100644 --- a/core/modules/content_moderation/src/Form/ModerationStateDeleteForm.php +++ b/core/modules/workflows/src/Form/WorkflowDeleteForm.php @@ -1,28 +1,28 @@ t('Are you sure you want to delete %name?', array('%name' => $this->entity->label())); + return $this->t('Are you sure you want to delete %name?', ['%name' => $this->entity->label()]); } /** * {@inheritdoc} */ public function getCancelUrl() { - return new Url('entity.moderation_state.collection'); + return new Url('entity.workflow.collection'); } /** @@ -39,7 +39,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $this->entity->delete(); drupal_set_message($this->t( - 'Moderation state %label deleted.', + 'Workflow %label deleted.', ['%label' => $this->entity->label()] )); diff --git a/core/modules/workflows/src/Form/WorkflowEditForm.php b/core/modules/workflows/src/Form/WorkflowEditForm.php new file mode 100644 index 0000000000000000000000000000000000000000..d8f99f935bf97f9a743b546ab875d48fdb7e0ba0 --- /dev/null +++ b/core/modules/workflows/src/Form/WorkflowEditForm.php @@ -0,0 +1,214 @@ +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'), + 'operations' => $this->t('Operations') + ]; + $form['states_container'] = [ + '#type' => 'details', + '#title' => $this->t('States'), + '#open' => TRUE, + '#collapsible' => 'FALSE', + ]; + $form['states_container']['states'] = [ + '#type' => 'table', + '#header' => $header, + '#title' => $this->t('States'), + '#empty' => $this->t('There are no states yet.'), + '#tabledrag' => [ + [ + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => 'state-weight', + ], + ], + ]; + + $states = $workflow->getStates(); + + // Warn the user if there are no states. + if (empty($states)) { + drupal_set_message( + $this->t( + 'This workflow has no states and will be disabled until there is at least one, add a new state.', + [':add-state' => $workflow->toUrl('add-state-form')->toString()] + ), + 'warning' + ); + } + + $delete_state_access = $this->entity->access('delete-state'); + foreach ($states as $state) { + $links['edit'] = [ + 'title' => $this->t('Edit'), + 'url' => Url::fromRoute('entity.workflow.edit_state_form', ['workflow' => $workflow->id(), 'workflow_state' => $state->id()]), + 'attributes' => ['aria-label' => $this->t('Edit @state state', ['@state' => $state->label()])], + ]; + if ($delete_state_access) { + $links['delete'] = [ + 'title' => t('Delete'), + 'url' => Url::fromRoute('entity.workflow.delete_state_form', [ + 'workflow' => $workflow->id(), + 'workflow_state' => $state->id() + ]), + 'attributes' => ['aria-label' => $this->t('Delete @state state', ['@state' => $state->label()])], + ]; + } + $form['states_container']['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']], + ], + 'operations' => [ + '#type' => 'operations', + '#links' => $links, + ], + ]; + } + $form['states_container']['state_add'] = [ + '#markup' => $workflow->toLink($this->t('Add a new state'), 'add-state-form')->toString(), + ]; + + $header = [ + 'label' => $this->t('Label'), + 'weight' => $this->t('Weight'), + 'from' => $this->t('From'), + 'to' => $this->t('To'), + 'operations' => $this->t('Operations') + ]; + $form['transitions_container'] = [ + '#type' => 'details', + '#title' => $this->t('Transitions'), + '#open' => TRUE, + ]; + $form['transitions_container']['transitions'] = [ + '#type' => 'table', + '#header' => $header, + '#title' => $this->t('Transitions'), + '#empty' => $this->t('There are no transitions yet.'), + '#tabledrag' => [ + [ + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => 'transition-weight', + ], + ], + ]; + foreach ($workflow->getTransitions() as $transition) { + $links['edit'] = [ + 'title' => $this->t('Edit'), + 'url' => Url::fromRoute('entity.workflow.edit_transition_form', ['workflow' => $workflow->id(), 'workflow_transition' => $transition->id()]), + 'attributes' => ['aria-label' => $this->t('Edit \'@transition\' transition', ['@transition' => $transition->label()])], + ]; + $links['delete'] = [ + 'title' => t('Delete'), + 'url' => Url::fromRoute('entity.workflow.delete_transition_form', ['workflow' => $workflow->id(), 'workflow_transition' => $transition->id()]), + 'attributes' => ['aria-label' => $this->t('Delete \'@transition\' transition', ['@transition' => $transition->label()])], + ]; + $form['transitions_container']['transitions'][$transition->id()] = [ + '#attributes' => ['class' => ['draggable']], + 'label' => ['#markup' => $transition->label()], + '#weight' => $transition->weight(), + 'weight' => [ + '#type' => 'weight', + '#title' => t('Weight for @title', ['@title' => $transition->label()]), + '#title_display' => 'invisible', + '#default_value' => $transition->weight(), + '#attributes' => ['class' => ['transition-weight']], + ], + 'from' => [ + '#theme' => 'item_list', + '#items' => array_map([State::class, 'labelCallback'], $transition->from()), + '#context' => ['list_style' => 'comma-list'], + ], + 'to' => ['#markup' => $transition->to()->label()], + 'operations' => [ + '#type' => 'operations', + '#links' => $links, + ], + ]; + } + $form['transitions_container']['transition_add'] = [ + '#markup' => $workflow->toLink($this->t('Add a new transition'), 'add-transition-form')->toString(), + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + /* @var \Drupal\workflows\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\workflows\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']); + } + foreach ($values['transitions'] as $transition_id => $transition_values) { + $entity->setTransitionWeight($transition_id, $transition_values['weight']); + } + } + +} diff --git a/core/modules/workflows/src/Form/WorkflowStateAddForm.php b/core/modules/workflows/src/Form/WorkflowStateAddForm.php new file mode 100644 index 0000000000000000000000000000000000000000..c9b46e2c35222cd92387b844b8c9a74ec06c78fa --- /dev/null +++ b/core/modules/workflows/src/Form/WorkflowStateAddForm.php @@ -0,0 +1,115 @@ +getEntity(); + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => '', + '#description' => $this->t('Label for the state.'), + '#required' => TRUE, + ]; + + $form['id'] = [ + '#type' => 'machine_name', + '#machine_name' => [ + 'exists' => [$this, 'exists'], + ], + ]; + + // Add additional form fields from the workflow type plugin. + $form['type_settings'] = [ + $workflow->get('type') => $workflow->getTypePlugin()->buildStateConfigurationForm($form_state, $workflow), + '#tree' => TRUE, + ]; + + 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) { + /** @var \Drupal\workflows\WorkflowInterface $original_workflow */ + $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\workflows\WorkflowInterface $entity */ + $values = $form_state->getValues(); + + // This is fired twice so we have to check that the entity does not already + // have the state. + if (!$entity->hasState($values['id'])) { + $entity->addState($values['id'], $values['label']); + 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\workflows\WorkflowInterface $workflow */ + $workflow = $this->entity; + $workflow->save(); + 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'] = [ + '#type' => 'submit', + '#value' => $this->t('Save'), + '#submit' => ['::submitForm', '::save'], + ]; + return $actions; + } + +} diff --git a/core/modules/workflows/src/Form/WorkflowStateDeleteForm.php b/core/modules/workflows/src/Form/WorkflowStateDeleteForm.php new file mode 100644 index 0000000000000000000000000000000000000000..c60045ca23a57487f38cea856e428fc0c46c010e --- /dev/null +++ b/core/modules/workflows/src/Form/WorkflowStateDeleteForm.php @@ -0,0 +1,99 @@ +t('Are you sure you want to delete %state from %workflow?', ['%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'); + } + + /** + * Form constructor. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param \Drupal\workflows\WorkflowInterface $workflow + * The workflow entity being edited. + * @param string|null $workflow_state + * The workflow state being deleted. + * + * @return array + * The form structure. + */ + 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/workflows/src/Form/WorkflowStateEditForm.php b/core/modules/workflows/src/Form/WorkflowStateEditForm.php new file mode 100644 index 0000000000000000000000000000000000000000..6ee33f3402c2fcd39e3ac46805990dd34c2c1214 --- /dev/null +++ b/core/modules/workflows/src/Form/WorkflowStateEditForm.php @@ -0,0 +1,167 @@ +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\workflows\WorkflowInterface $workflow */ + $workflow = $this->getEntity(); + $state = $workflow->getState($this->stateId); + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => $state->label(), + '#description' => $this->t('Label for the state.'), + '#required' => TRUE, + ]; + + $form['id'] = [ + '#type' => 'machine_name', + '#default_value' => $this->stateId, + '#machine_name' => [ + 'exists' => [$this, 'exists'], + ], + '#disabled' => TRUE, + ]; + + // Add additional form fields from the workflow type plugin. + $form['type_settings'] = [ + $workflow->get('type') => $workflow->getTypePlugin()->buildStateConfigurationForm($form_state, $workflow, $state), + '#tree' => TRUE, + ]; + + $header = [ + 'label' => $this->t('Transition'), + 'state' => $this->t('To'), + 'operations' => $this->t('Operations'), + ]; + $form['transitions'] = [ + '#type' => 'table', + '#header' => $header, + '#empty' => $this->t('There are no states yet.'), + ]; + foreach ($state->getTransitions() as $transition) { + $links['edit'] = [ + 'title' => $this->t('Edit'), + 'url' => Url::fromRoute('entity.workflow.edit_transition_form', [ + 'workflow' => $workflow->id(), + 'workflow_transition' => $transition->id() + ]), + ]; + $links['delete'] = [ + 'title' => t('Delete'), + 'url' => Url::fromRoute('entity.workflow.delete_transition_form', [ + 'workflow' => $workflow->id(), + 'workflow_transition' => $transition->id() + ]), + ]; + $form['transitions'][$transition->id()] = [ + 'label' => [ + '#markup' => $transition->label(), + ], + 'state' => [ + '#markup' => $transition->to()->label(), + ], + 'operations' => [ + '#type' => 'operations', + '#links' => $links, + ], + ]; + } + + return $form; + } + + /** + * 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\workflows\WorkflowInterface $entity */ + $values = $form_state->getValues(); + $entity->setStateLabel($values['id'], $values['label']); + 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\workflows\WorkflowInterface $workflow */ + $workflow = $this->entity; + $workflow->save(); + drupal_set_message($this->t('Saved %label state.', [ + '%label' => $workflow->getState($this->stateId)->label(), + ])); + $form_state->setRedirectUrl($workflow->toUrl('edit-form')); + } + + /** + * {@inheritdoc} + */ + protected function actions(array $form, FormStateInterface $form_state) { + $actions['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Save'), + '#submit' => ['::submitForm', '::save'], + ]; + + $actions['delete'] = [ + '#type' => 'link', + '#title' => $this->t('Delete'), + '#access' => $this->entity->access('delete-state'), + '#attributes' => [ + 'class' => ['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/workflows/src/Form/WorkflowTransitionAddForm.php b/core/modules/workflows/src/Form/WorkflowTransitionAddForm.php new file mode 100644 index 0000000000000000000000000000000000000000..1c557d929b00317b8bd0f8ab0ba59b51685d6bcf --- /dev/null +++ b/core/modules/workflows/src/Form/WorkflowTransitionAddForm.php @@ -0,0 +1,151 @@ +getEntity(); + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => '', + '#description' => $this->t('Label for the transition.'), + '#required' => TRUE, + ]; + + $form['id'] = [ + '#type' => 'machine_name', + '#machine_name' => [ + 'exists' => [$this, 'exists'], + ], + ]; + + // @todo https://www.drupal.org/node/2830584 Add some ajax to ensure that + // only valid transitions are selectable. + $states = array_map([State::class, 'labelCallback'], $workflow->getStates()); + $form['from'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('From'), + '#required' => TRUE, + '#default_value' => [], + '#options' => $states, + ]; + $form['to'] = [ + '#type' => 'radios', + '#title' => $this->t('To'), + '#required' => TRUE, + '#default_value' => [], + '#options' => $states, + ]; + + // Add additional form fields from the workflow type plugin. + $form['type_settings'] = [ + $workflow->get('type') => $workflow->getTypePlugin()->buildTransitionConfigurationForm($form_state, $workflow), + '#tree' => TRUE, + ]; + + return $form; + } + + /** + * Determines if the workflow transition already exists. + * + * @param string $transition_id + * The workflow transition ID. + * + * @return bool + * TRUE if the workflow transition exists, FALSE otherwise. + */ + public function exists($transition_id) { + /** @var \Drupal\workflows\WorkflowInterface $original_workflow */ + $original_workflow = \Drupal::entityTypeManager()->getStorage('workflow')->loadUnchanged($this->getEntity()->id()); + return $original_workflow->hasTransition($transition_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) { + if (!$form_state->isValidationComplete()) { + // Only do something once form validation is complete. + return; + } + /** @var \Drupal\workflows\WorkflowInterface $entity */ + $values = $form_state->getValues(); + $entity->addTransition($values['id'], $values['label'], array_filter($values['from']), $values['to']); + if (isset($values['type_settings'])) { + $configuration = $entity->getTypePlugin()->getConfiguration(); + $configuration['transitions'][$values['id']] = $values['type_settings'][$entity->getTypePlugin()->getPluginId()]; + $entity->set('type_settings', $configuration); + } + } + + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + /** @var \Drupal\workflows\WorkflowInterface $workflow */ + $workflow = $this->getEntity(); + $values = $form_state->getValues(); + foreach (array_filter($values['from']) as $from_state_id) { + if ($workflow->hasTransitionFromStateToState($from_state_id, $values['to'])) { + $form_state->setErrorByName('from][' . $from_state_id, $this->t('The transition from %from to %to already exists.', [ + '%from' => $workflow->getState($from_state_id)->label(), + '%to' => $workflow->getState($values['to'])->label(), + ])); + } + } + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + /** @var \Drupal\workflows\WorkflowInterface $workflow */ + $workflow = $this->entity; + $workflow->save(); + drupal_set_message($this->t('Created %label transition.', [ + '%label' => $form_state->getValue('label'), + ])); + $form_state->setRedirectUrl($workflow->toUrl('edit-form')); + } + + /** + * {@inheritdoc} + */ + protected function actions(array $form, FormStateInterface $form_state) { + $actions['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Save'), + '#submit' => ['::submitForm', '::save'], + ]; + return $actions; + } + +} diff --git a/core/modules/workflows/src/Form/WorkflowTransitionDeleteForm.php b/core/modules/workflows/src/Form/WorkflowTransitionDeleteForm.php new file mode 100644 index 0000000000000000000000000000000000000000..abcb41e6645b0774545903299283489407b9839f --- /dev/null +++ b/core/modules/workflows/src/Form/WorkflowTransitionDeleteForm.php @@ -0,0 +1,102 @@ +t('Are you sure you want to delete %transition from %workflow?', ['%transition' => $this->transition->label(), '%workflow' => $this->workflow->label()]); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return $this->workflow->toUrl(); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * Form constructor. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * @param \Drupal\workflows\WorkflowInterface $workflow + * The workflow entity being edited. + * @param string|null $workflow_transition + * The workflow transition being deleted. + * + * @return array + * The form structure. + */ + public function buildForm(array $form, FormStateInterface $form_state, WorkflowInterface $workflow = NULL, $workflow_transition = NULL) { + try { + $this->transition = $workflow->getTransition($workflow_transition); + } + catch (\InvalidArgumentException $e) { + throw new NotFoundHttpException(); + } + $this->workflow = $workflow; + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $this->workflow + ->deleteTransition($this->transition->id()) + ->save(); + + drupal_set_message($this->t('%transition transition deleted.', ['%transition' => $this->transition->label()])); + $form_state->setRedirectUrl($this->getCancelUrl()); + } + +} diff --git a/core/modules/workflows/src/Form/WorkflowTransitionEditForm.php b/core/modules/workflows/src/Form/WorkflowTransitionEditForm.php new file mode 100644 index 0000000000000000000000000000000000000000..b6f65a48a8e6a90d0fdc81eedaf4baf0e3e48758 --- /dev/null +++ b/core/modules/workflows/src/Form/WorkflowTransitionEditForm.php @@ -0,0 +1,171 @@ +transitionId = $workflow_transition; + return parent::buildForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + + /* @var \Drupal\workflows\WorkflowInterface $workflow */ + $workflow = $this->getEntity(); + $transition = $workflow->getTransition($this->transitionId); + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => $transition->label(), + '#description' => $this->t('Label for the transition.'), + '#required' => TRUE, + ]; + + $form['id'] = [ + '#type' => 'value', + '#value' => $this->transitionId, + ]; + + // @todo https://www.drupal.org/node/2830584 Add some ajax to ensure that + // only valid transitions are selectable. + $states = array_map([State::class, 'labelCallback'], $workflow->getStates()); + $form['from'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('From'), + '#required' => TRUE, + '#default_value' => array_keys($transition->from()), + '#options' => $states, + ]; + $form['to'] = [ + '#type' => 'radios', + '#title' => $this->t('To'), + '#required' => TRUE, + '#default_value' => $transition->to()->id(), + '#options' => $states, + '#disabled' => TRUE, + ]; + + // Add additional form fields from the workflow type plugin. + $form['type_settings'] = [ + $workflow->get('type') => $workflow->getTypePlugin()->buildTransitionConfigurationForm($form_state, $workflow, $transition), + '#tree' => TRUE, + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + /** @var \Drupal\workflows\WorkflowInterface $workflow */ + $workflow = $this->getEntity(); + $values = $form_state->getValues(); + foreach (array_filter($values['from']) as $from_state_id) { + if ($workflow->hasTransitionFromStateToState($from_state_id, $values['to'])) { + $transition = $workflow->getTransitionFromStateToState($from_state_id, $values['to']); + if ($transition->id() !== $values['id']) { + $form_state->setErrorByName('from][' . $from_state_id, $this->t('The transition from %from to %to already exists.', [ + '%from' => $workflow->getState($from_state_id)->label(), + '%to' => $workflow->getState($values['to'])->label(), + ])); + } + } + } + } + + /** + * 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) { + if (!$form_state->isValidationComplete()) { + // Only do something once form validation is complete. + return; + } + /** @var \Drupal\workflows\WorkflowInterface $entity */ + $values = $form_state->getValues(); + $form_state->set('created_transition', FALSE); + $entity->setTransitionLabel($values['id'], $values['label']); + $entity->setTransitionFromStates($values['id'], array_filter($values['from'])); + if (isset($values['type_settings'])) { + $configuration = $entity->getTypePlugin()->getConfiguration(); + $configuration['transitions'][$values['id']] = $values['type_settings'][$entity->getTypePlugin()->getPluginId()]; + $entity->set('type_settings', $configuration); + } + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + /** @var \Drupal\workflows\WorkflowInterface $workflow */ + $workflow = $this->entity; + $workflow->save(); + drupal_set_message($this->t('Saved %label transition.', [ + '%label' => $workflow->getTransition($this->transitionId)->label(), + ])); + $form_state->setRedirectUrl($workflow->toUrl('edit-form')); + } + + /** + * {@inheritdoc} + */ + protected function actions(array $form, FormStateInterface $form_state) { + $actions['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Save'), + '#submit' => ['::submitForm', '::save'], + ]; + + $actions['delete'] = [ + '#type' => 'link', + '#title' => $this->t('Delete'), + // Deleting a transition is editing a workflow. + '#access' => $this->entity->access('edit'), + '#attributes' => [ + 'class' => ['button', 'button--danger'], + ], + '#url' => Url::fromRoute('entity.workflow.delete_transition_form', [ + 'workflow' => $this->entity->id(), + 'workflow_transition' => $this->transitionId + ]) + ]; + + return $actions; + } + +} diff --git a/core/modules/workflows/src/Plugin/WorkflowTypeBase.php b/core/modules/workflows/src/Plugin/WorkflowTypeBase.php new file mode 100644 index 0000000000000000000000000000000000000000..1b910e923b83ef76d850e8dff0f848aaee6f79d0 --- /dev/null +++ b/core/modules/workflows/src/Plugin/WorkflowTypeBase.php @@ -0,0 +1,119 @@ +getPluginDefinition(); + // The label can be an object. + // @see \Drupal\Core\StringTranslation\TranslatableMarkup + return $definition['label']; + } + + /** + * {@inheritdoc} + */ + public function checkWorkflowAccess(WorkflowInterface $entity, $operation, AccountInterface $account) { + return AccessResult::neutral(); + } + + /** + * {@inheritDoc} + */ + public function decorateState(StateInterface $state) { + return $state; + } + + /** + * {@inheritDoc} + */ + public function deleteState($state_id) { + unset($this->configuration['states'][$state_id]); + } + + /** + * {@inheritDoc} + */ + public function decorateTransition(TransitionInterface $transition) { + return $transition; + } + + /** + * {@inheritDoc} + */ + public function deleteTransition($transition_id) { + unset($this->configuration['transitions'][$transition_id]); + } + + /** + * {@inheritdoc} + */ + public function buildStateConfigurationForm(FormStateInterface $form_state, WorkflowInterface $workflow, StateInterface $state = NULL) { + return []; + } + + /** + * {@inheritdoc} + */ + public function buildTransitionConfigurationForm(FormStateInterface $form_state, WorkflowInterface $workflow, TransitionInterface $transition = NULL) { + 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' => [], + 'transitions' => [], + ]; + } + + /** + * {@inheritDoc} + */ + public function calculateDependencies() { + return []; + } + +} diff --git a/core/modules/workflows/src/State.php b/core/modules/workflows/src/State.php new file mode 100644 index 0000000000000000000000000000000000000000..f8b4faea3c0f3be06952d8a883a6969a44110907 --- /dev/null +++ b/core/modules/workflows/src/State.php @@ -0,0 +1,118 @@ +workflow = $workflow; + $this->id = $id; + $this->label = $label; + $this->weight = $weight; + } + + /** + * {@inheritdoc} + */ + public function id() { + return $this->id; + } + + /** + * {@inheritdoc} + */ + public function label() { + return $this->label; + } + + /** + * {@inheritdoc} + */ + public function weight() { + return $this->weight; + } + + /** + * {@inheritdoc} + */ + public function canTransitionTo($to_state_id) { + return $this->workflow->hasTransitionFromStateToState($this->id, $to_state_id); + } + + /** + * {@inheritdoc} + */ + public function getTransitionTo($to_state_id) { + if (!$this->canTransitionTo($to_state_id)) { + throw new \InvalidArgumentException("Can not transition to '$to_state_id' state"); + } + return $this->workflow->getTransitionFromStateToState($this->id(), $to_state_id); + } + + /** + * {@inheritdoc} + */ + public function getTransitions() { + return $this->workflow->getTransitionsForState($this->id); + } + + /** + * Helper method to convert a list of states to labels + * + * @param \Drupal\workflows\StateInterface $state + * + * @return string + * The label of the state. + */ + public static function labelCallback(StateInterface $state) { + return $state->label(); + } + +} diff --git a/core/modules/workflows/src/StateInterface.php b/core/modules/workflows/src/StateInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..6aa16e9d9863652e7a4da8d29750abc6455811cb --- /dev/null +++ b/core/modules/workflows/src/StateInterface.php @@ -0,0 +1,73 @@ +workflow = $workflow; + $this->id = $id; + $this->label = $label; + $this->fromStateIds = $from_state_ids; + $this->toStateId = $to_state_id; + $this->weight = $weight; + } + + /** + * {@inheritdoc} + */ + public function id() { + return $this->id; + } + + /** + * {@inheritdoc} + */ + public function label() { + return $this->label; + } + + /** + * {@inheritdoc} + */ + public function from() { + return $this->workflow->getStates($this->fromStateIds); + } + + /** + * {@inheritdoc} + */ + public function to() { + return $this->workflow->getState($this->toStateId); + } + + /** + * {@inheritdoc} + */ + public function weight() { + return $this->weight; + } + +} diff --git a/core/modules/workflows/src/TransitionInterface.php b/core/modules/workflows/src/TransitionInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..c178b22da1e2c6d6c4c33f979fbace7d3d6ba5f2 --- /dev/null +++ b/core/modules/workflows/src/TransitionInterface.php @@ -0,0 +1,54 @@ +get('plugin.manager.workflows.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) { + if ($operation === 'delete-state') { + // Deleting a state is editing a workflow, but also we should forbid + // access if there is only one state. + /** @var \Drupal\workflows\Entity\Workflow $entity */ + $admin_access = AccessResult::allowedIf(count($entity->getStates()) > 1)->andIf(parent::checkAccess($entity, 'edit', $account))->addCacheableDependency($entity); + } + else { + $admin_access = parent::checkAccess($entity, $operation, $account); + } + /** @var \Drupal\workflows\WorkflowInterface $entity */ + return $entity->getTypePlugin()->checkWorkflowAccess($entity, $operation, $account)->orIf($admin_access); + } + + /** + * {@inheritdoc} + */ + protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { + $workflow_types_count = count($this->workflowTypeManager->getDefinitions()); + $admin_access = parent::checkCreateAccess($account, $context, $entity_bundle); + // Allow access if there is at least one workflow type. Since workflow types + // are provided by modules this is cacheable until extensions change. + return $admin_access->andIf(AccessResult::allowedIf($workflow_types_count > 0))->addCacheTags(['config:core.extension']); + } + +} diff --git a/core/modules/workflows/src/WorkflowInterface.php b/core/modules/workflows/src/WorkflowInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..0efedea287a0bf78ecadb3e7cb69b52d08f477ef --- /dev/null +++ b/core/modules/workflows/src/WorkflowInterface.php @@ -0,0 +1,289 @@ +get('entity_type.manager')->getStorage($entity_type->id()), + $container->get('plugin.manager.workflows.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\workflows\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/workflows/src/WorkflowTypeInterface.php b/core/modules/workflows/src/WorkflowTypeInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..17fddecb98e71b35e488e4a1bc052c8c3e52ac1a --- /dev/null +++ b/core/modules/workflows/src/WorkflowTypeInterface.php @@ -0,0 +1,120 @@ +alterInfo('workflow_type_info'); + $this->setCacheBackend($cache_backend, 'workflow_type_info'); + } + +} diff --git a/core/modules/workflows/tests/modules/workflow_type_test/config/schema/workflow_type_test.schema.yml b/core/modules/workflows/tests/modules/workflow_type_test/config/schema/workflow_type_test.schema.yml new file mode 100644 index 0000000000000000000000000000000000000000..4f12fdd1f459a51e2b77cfd4a874939923441cd6 --- /dev/null +++ b/core/modules/workflows/tests/modules/workflow_type_test/config/schema/workflow_type_test.schema.yml @@ -0,0 +1,33 @@ +workflow.type_settings.workflow_type_test: + type: mapping + label: 'Workflow test type settings' + mapping: + states: + type: sequence + sequence: + type: ignore + +workflow.type_settings.workflow_type_complex_test: + type: mapping + label: 'Workflow complex test type settings' + mapping: + states: + type: sequence + label: 'Additional state configuration' + sequence: + type: mapping + label: 'States' + mapping: + extra: + type: string + label: 'Extra information' + transitions: + type: sequence + label: 'Additional transition configuration' + sequence: + type: mapping + label: 'Transitions' + mapping: + extra: + type: string + label: 'Extra information' diff --git a/core/modules/workflows/tests/modules/workflow_type_test/src/DecoratedState.php b/core/modules/workflows/tests/modules/workflow_type_test/src/DecoratedState.php new file mode 100644 index 0000000000000000000000000000000000000000..793e8993a03098877817fc5d6bde189c1bae2e77 --- /dev/null +++ b/core/modules/workflows/tests/modules/workflow_type_test/src/DecoratedState.php @@ -0,0 +1,90 @@ +state = $state; + $this->extra = $extra; + } + + /** + * Gets the extra information stored on the state. + * + * @return string + */ + public function getExtra() { + return $this->extra; + } + + /** + * {@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 canTransitionTo($to_state_id) { + return $this->state->canTransitionTo($to_state_id); + } + + /** + * {@inheritdoc} + */ + public function getTransitionTo($to_state_id) { + return $this->state->getTransitionTo($to_state_id); + } + + /** + * {@inheritdoc} + */ + public function getTransitions() { + return $this->state->getTransitions(); + } + +} diff --git a/core/modules/workflows/tests/modules/workflow_type_test/src/DecoratedTransition.php b/core/modules/workflows/tests/modules/workflow_type_test/src/DecoratedTransition.php new file mode 100644 index 0000000000000000000000000000000000000000..d7690d519d14767a4946aefb3a8c1c9107cb9bf6 --- /dev/null +++ b/core/modules/workflows/tests/modules/workflow_type_test/src/DecoratedTransition.php @@ -0,0 +1,83 @@ +transition = $transition; + $this->extra = $extra; + } + + /** + * Gets the extra information stored on the transition. + * + * @return string + */ + public function getExtra() { + return $this->extra; + } + + /** + * {@inheritdoc} + */ + public function id() { + return $this->transition->id(); + } + + /** + * {@inheritdoc} + */ + public function label() { + return $this->transition->label(); + } + + /** + * {@inheritdoc} + */ + public function from() { + return $this->transition->from(); + } + + /** + * {@inheritdoc} + */ + public function to() { + return $this->transition->to(); + } + + /** + * {@inheritdoc} + */ + public function weight() { + return $this->transition->weight(); + } + +} diff --git a/core/modules/workflows/tests/modules/workflow_type_test/src/Plugin/WorkflowType/ComplexTestType.php b/core/modules/workflows/tests/modules/workflow_type_test/src/Plugin/WorkflowType/ComplexTestType.php new file mode 100644 index 0000000000000000000000000000000000000000..8440c1c18cde285f20adac351238c119cf7a1eb4 --- /dev/null +++ b/core/modules/workflows/tests/modules/workflow_type_test/src/Plugin/WorkflowType/ComplexTestType.php @@ -0,0 +1,82 @@ +configuration['states'][$state->id()])) { + $state = new DecoratedState($state, $this->configuration['states'][$state->id()]['extra']); + } + else { + $state = new DecoratedState($state); + } + return $state; + } + + /** + * {@inheritDoc} + */ + public function decorateTransition(TransitionInterface $transition) { + if (isset($this->configuration['transitions'][$transition->id()])) { + $transition = new DecoratedTransition($transition, $this->configuration['transitions'][$transition->id()]['extra']); + } + else { + $transition = new DecoratedTransition($transition); + } + return $transition; + } + + /** + * {@inheritdoc} + */ + public function buildStateConfigurationForm(FormStateInterface $form_state, WorkflowInterface $workflow, StateInterface $state = NULL) { + /** @var \Drupal\workflow_type_test\DecoratedState $state */ + $form = []; + $form['extra'] = [ + '#type' => 'textfield', + '#title' => $this->t('Extra'), + '#description' => $this->t('Extra information added to state'), + '#default_value' => isset($state) ? $state->getExtra() : FALSE, + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function buildTransitionConfigurationForm(FormStateInterface $form_state, WorkflowInterface $workflow, TransitionInterface $transition = NULL) { + /** @var \Drupal\workflow_type_test\DecoratedTransition $transition */ + $form = []; + $form['extra'] = [ + '#type' => 'textfield', + '#title' => $this->t('Extra'), + '#description' => $this->t('Extra information added to transition'), + '#default_value' => isset($transition) ? $transition->getExtra() : FALSE, + ]; + return $form; + } + +} diff --git a/core/modules/workflows/tests/modules/workflow_type_test/src/Plugin/WorkflowType/TestType.php b/core/modules/workflows/tests/modules/workflow_type_test/src/Plugin/WorkflowType/TestType.php new file mode 100644 index 0000000000000000000000000000000000000000..78ebe035bcc4caf59f4d3033d32acb4495e9671e --- /dev/null +++ b/core/modules/workflows/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/workflows/tests/src/Functional/WorkflowUiTest.php b/core/modules/workflows/tests/src/Functional/WorkflowUiTest.php new file mode 100644 index 0000000000000000000000000000000000000000..ed5eb6ff2b652727e55b0adf71e8b6d3029b6ce1 --- /dev/null +++ b/core/modules/workflows/tests/src/Functional/WorkflowUiTest.php @@ -0,0 +1,262 @@ +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('draft', 'Draft') + ->addState('published', 'Published') + ->addTransition('publish', 'Publish', ['draft', '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', + 'admin/config/workflow/workflows/manage/test/add_transition', + 'admin/config/workflow/workflows/manage/test/transition/publish', + 'admin/config/workflow/workflows/manage/test/transition/publish/delete', + ]; + + foreach ($paths as $path) { + $this->drupalGet($path); + // No access. + $this->assertSession()->statusCodeEquals(403); + } + $this->drupalLogin($this->createUser(['administer workflows'])); + foreach ($paths as $path) { + $this->drupalGet($path); + // User has access. + $this->assertSession()->statusCodeEquals(200); + } + + // Delete one of the states and ensure the other test cannot be deleted. + $this->drupalGet('admin/config/workflow/workflows/manage/test/state/published/delete'); + $this->submitForm([], 'Delete'); + $this->drupalGet('admin/config/workflow/workflows/manage/test/state/published/delete'); + $this->assertSession()->statusCodeEquals(403); + } + + /** + * Tests the creation of a workflow through the UI. + */ + public function testWorkflowCreation() { + $workflow_storage = $this->container->get('entity_type.manager')->getStorage('workflow'); + /** @var \Drupal\workflows\WorkflowInterface $workflow */ + $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()->addressEquals('admin/config/workflow/workflows/manage/test/add_state'); + $this->drupalGet('/admin/config/workflow/workflows/manage/test'); + $this->assertSession()->pageTextContains('This workflow has no states and will be disabled until there is at least one, add a new state.'); + $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.'); + $workflow = $workflow_storage->loadUnchanged('test'); + $this->assertFalse($workflow->getState('published')->canTransitionTo('published'), 'No default transition from published to published exists.'); + + $this->clickLink('Add a new state'); + // Don't create a draft to draft transition by default. + $this->submitForm(['label' => 'Draft', 'id' => 'draft'], 'Save'); + $this->assertSession()->pageTextContains('Created Draft state.'); + $workflow = $workflow_storage->loadUnchanged('test'); + $this->assertFalse($workflow->getState('draft')->canTransitionTo('draft'), 'Can not transition from draft to draft'); + + $this->clickLink('Add a new transition'); + $this->submitForm(['id' => 'publish', 'label' => 'Publish', 'from[draft]' => 'draft', 'to' => 'published'], 'Save'); + $this->assertSession()->pageTextContains('Created Publish transition.'); + $workflow = $workflow_storage->loadUnchanged('test'); + $this->assertTrue($workflow->getState('draft')->canTransitionTo('published'), 'Can transition from draft to published'); + + $this->clickLink('Add a new transition'); + $this->submitForm(['id' => 'create_new_draft', 'label' => 'Create new draft', 'from[draft]' => 'draft', 'to' => 'draft'], 'Save'); + $this->assertSession()->pageTextContains('Created Create new draft transition.'); + $workflow = $workflow_storage->loadUnchanged('test'); + $this->assertTrue($workflow->getState('draft')->canTransitionTo('draft'), 'Can transition from draft to draft'); + + // The fist state to edit on the page should be published. + $this->clickLink('Edit'); + $this->assertSession()->fieldValueEquals('label', 'Published'); + // Change the label. + $this->submitForm(['label' => 'Live'], 'Save'); + $this->assertSession()->pageTextContains('Saved Live state.'); + + // Allow published to draft. + $this->clickLink('Edit', 3); + $this->submitForm(['from[published]' => 'published'], 'Save'); + $this->assertSession()->pageTextContains('Saved Create new draft transition.'); + $workflow = $workflow_storage->loadUnchanged('test'); + $this->assertTrue($workflow->getState('published')->canTransitionTo('draft'), 'Can transition from published to draft'); + + // Try creating a duplicate transition. + $this->clickLink('Add a new transition'); + $this->submitForm(['id' => 'create_new_draft', 'label' => 'Create new draft', 'from[published]' => 'published', 'to' => 'draft'], 'Save'); + $this->assertSession()->pageTextContains('The machine-readable name is already in use. It must be unique.'); + // Try creating a transition which duplicates the states of another. + $this->submitForm(['id' => 'create_new_draft2', 'label' => 'Create new draft again', 'from[published]' => 'published', 'to' => 'draft'], 'Save'); + $this->assertSession()->pageTextContains('The transition from Live to Draft already exists.'); + + // Create a new transition. + $this->submitForm(['id' => 'save_and_publish', 'label' => 'Save and publish', 'from[published]' => 'published', 'to' => 'published'], 'Save'); + $this->assertSession()->pageTextContains('Created Save and publish transition.'); + // Edit the new transition and try to add an existing transition. + $this->clickLink('Edit', 4); + $this->submitForm(['from[draft]' => 'draft'], 'Save'); + $this->assertSession()->pageTextContains('The transition from Draft to Live already exists.'); + + // Delete the transition. + $workflow = $workflow_storage->loadUnchanged('test'); + $this->assertTrue($workflow->hasTransitionFromStateToState('published', 'published'), 'Can transition from published to published'); + $this->clickLink('Delete'); + $this->assertSession()->pageTextContains('Are you sure you want to delete Save and publish from Test?'); + $this->submitForm([], 'Delete'); + $workflow = $workflow_storage->loadUnchanged('test'); + $this->assertFalse($workflow->hasTransitionFromStateToState('published', 'published'), 'Cannot transition from published to published'); + + // Try creating a duplicate state. + $this->drupalGet('admin/config/workflow/workflows/manage/test'); + $this->clickLink('Add a new state'); + $this->submitForm(['label' => 'Draft', 'id' => 'draft'], 'Save'); + $this->assertSession()->pageTextContains('The machine-readable name is already in use. It must be unique.'); + + // Ensure that weight changes the state ordering. + $workflow = $workflow_storage->loadUnchanged('test'); + $this->assertEquals('published', $workflow->getInitialState()->id()); + $this->drupalGet('admin/config/workflow/workflows/manage/test'); + $this->submitForm(['states[draft][weight]' => '-1'], 'Save'); + $workflow = $workflow_storage->loadUnchanged('test'); + $this->assertEquals('draft', $workflow->getInitialState()->id()); + + // This will take us to the list of workflows, so we need to edit the + // workflow again. + $this->clickLink('Edit'); + + // Ensure that weight changes the transition ordering. + $this->assertEquals(['publish', 'create_new_draft'], array_keys($workflow->getTransitions())); + $this->drupalGet('admin/config/workflow/workflows/manage/test'); + $this->submitForm(['transitions[create_new_draft][weight]' => '-1'], 'Save'); + $workflow = $workflow_storage->loadUnchanged('test'); + $this->assertEquals(['create_new_draft', 'publish'], array_keys($workflow->getTransitions())); + + // This will take us to the list of workflows, so we need to edit the + // workflow again. + $this->clickLink('Edit'); + + // Ensure that a delete link for the published state exists before deleting + // the draft state. + $published_delete_link = Url::fromRoute('entity.workflow.delete_state_form', [ + 'workflow' => $workflow->id(), + 'workflow_state' => 'published' + ])->toString(); + $this->assertSession()->linkByHrefExists($published_delete_link); + + // 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.'); + $workflow = $workflow_storage->loadUnchanged('test'); + $this->assertFalse($workflow->hasState('draft'), 'Draft state deleted'); + $this->assertTrue($workflow->hasState('published'), 'Workflow still has published state'); + + // The last state cannot be deleted so the only delete link on the page will + // be for the workflow. + $this->assertSession()->linkByHrefNotExists($published_delete_link); + $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.'); + $this->assertNull($workflow_storage->loadUnchanged('test'), 'The test workflow has been deleted'); + } + + /** + * Tests that workflow types can add form fields to states and transitions. + */ + public function testWorkflowDecoration() { + // Create a minimal workflow for testing. + $workflow = Workflow::create(['id' => 'test', 'type' => 'workflow_type_complex_test']); + $workflow + ->addState('published', 'Published') + ->addTransition('publish', 'Publish', ['published'], 'published') + ->save(); + + $this->assertEquals('', $workflow->getState('published')->getExtra()); + $this->assertEquals('', $workflow->getTransition('publish')->getExtra()); + + $this->drupalLogin($this->createUser(['administer workflows'])); + + // Add additional state information when editing. + $this->drupalGet('admin/config/workflow/workflows/manage/test/state/published'); + $this->assertSession()->pageTextContains('Extra information added to state'); + $this->submitForm(['type_settings[workflow_type_complex_test][extra]' => 'Extra state information'], 'Save'); + + // Add additional transition information when editing. + $this->drupalGet('admin/config/workflow/workflows/manage/test/transition/publish'); + $this->assertSession()->pageTextContains('Extra information added to transition'); + $this->submitForm(['type_settings[workflow_type_complex_test][extra]' => 'Extra transition information'], 'Save'); + + $workflow_storage = $this->container->get('entity_type.manager')->getStorage('workflow'); + /** @var \Drupal\workflows\WorkflowInterface $workflow */ + $workflow = $workflow_storage->loadUnchanged('test'); + $this->assertEquals('Extra state information', $workflow->getState('published')->getExtra()); + $this->assertEquals('Extra transition information', $workflow->getTransition('publish')->getExtra()); + + // Add additional state information when adding. + $this->drupalGet('admin/config/workflow/workflows/manage/test/add_state'); + $this->assertSession()->pageTextContains('Extra information added to state'); + $this->submitForm(['label' => 'Draft', 'id' => 'draft', 'type_settings[workflow_type_complex_test][extra]' => 'Extra state information on add'], 'Save'); + + // Add additional transition information when adding. + $this->drupalGet('admin/config/workflow/workflows/manage/test/add_transition'); + $this->assertSession()->pageTextContains('Extra information added to transition'); + $this->submitForm(['id' => 'draft_published', 'label' => 'Publish', 'from[draft]' => 'draft', 'to' => 'published', 'type_settings[workflow_type_complex_test][extra]' => 'Extra transition information on add'], 'Save'); + + $workflow = $workflow_storage->loadUnchanged('test'); + $this->assertEquals('Extra state information on add', $workflow->getState('draft')->getExtra()); + $this->assertEquals('Extra transition information on add', $workflow->getTransition('draft_published')->getExtra()); + } + +} diff --git a/core/modules/workflows/tests/src/Kernel/ComplexWorkflowTypeTest.php b/core/modules/workflows/tests/src/Kernel/ComplexWorkflowTypeTest.php new file mode 100644 index 0000000000000000000000000000000000000000..2ff1c2adf7a3e6714898f42fbe96da3cfad7fe7a --- /dev/null +++ b/core/modules/workflows/tests/src/Kernel/ComplexWorkflowTypeTest.php @@ -0,0 +1,55 @@ + 'test', 'type' => 'workflow_type_complex_test'], 'workflow'); + $workflow + ->addState('draft', 'Draft') + ->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft'); + $this->assertInstanceOf(DecoratedState::class, $workflow->getState('draft')); + $this->assertInstanceOf(DecoratedTransition::class, $workflow->getTransition('create_new_draft')); + } + + /** + * @covers ::loadMultipleByType + */ + public function testLoadMultipleByType() { + $workflow1 = new Workflow(['id' => 'test1', 'type' => 'workflow_type_complex_test'], 'workflow'); + $workflow1->save(); + $workflow2 = new Workflow(['id' => 'test2', 'type' => 'workflow_type_complex_test'], 'workflow'); + $workflow2->save(); + $workflow3 = new Workflow(['id' => 'test3', 'type' => 'workflow_type_test'], 'workflow'); + $workflow3->save(); + + $this->assertEquals(['test1', 'test2'], array_keys(Workflow::loadMultipleByType('workflow_type_complex_test'))); + $this->assertEquals(['test3'], array_keys(Workflow::loadMultipleByType('workflow_type_test'))); + $this->assertEquals([], Workflow::loadMultipleByType('a_type_that_does_not_exist')); + } + +} diff --git a/core/modules/workflows/tests/src/Unit/StateTest.php b/core/modules/workflows/tests/src/Unit/StateTest.php new file mode 100644 index 0000000000000000000000000000000000000000..82feca34627b02c28e3f80e969245347b9b47054 --- /dev/null +++ b/core/modules/workflows/tests/src/Unit/StateTest.php @@ -0,0 +1,131 @@ +prophesize(WorkflowTypeInterface::class); + $workflow_type->decorateState(Argument::any())->willReturnArgument(0); + $workflow_type->decorateTransition(Argument::any())->willReturnArgument(0); + $workflow_type->deleteState(Argument::any())->willReturn(NULL); + $workflow_type->deleteTransition(Argument::any())->willReturn(NULL); + $workflow_manager = $this->prophesize(WorkflowTypeManager::class); + $workflow_manager->createInstance('test_type', Argument::any())->willReturn($workflow_type->reveal()); + $container->set('plugin.manager.workflows.type', $workflow_manager->reveal()); + \Drupal::setContainer($container); + } + + /** + * @covers ::__construct + * @covers ::id + * @covers ::label + * @covers ::weight + */ + public function testGetters() { + $state = new State( + $this->prophesize(WorkflowInterface::class)->reveal(), + 'draft', + 'Draft', + 3 + ); + $this->assertEquals('draft', $state->id()); + $this->assertEquals('Draft', $state->label()); + $this->assertEquals(3, $state->weight()); + } + + /** + * @covers ::canTransitionTo + */ + public function testCanTransitionTo() { + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow + ->addState('draft', 'Draft') + ->addState('published', 'Published') + ->addTransition('publish', 'Publish', ['draft'], 'published'); + $state = $workflow->getState('draft'); + $this->assertTrue($state->canTransitionTo('published')); + $this->assertFalse($state->canTransitionTo('some_other_state')); + + $workflow->deleteTransition('publish'); + $this->assertFalse($state->canTransitionTo('published')); + } + + /** + * @covers ::getTransitionTo + */ + public function testGetTransitionTo() { + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow + ->addState('draft', 'Draft') + ->addState('published', 'Published') + ->addTransition('publish', 'Publish', ['draft'], 'published'); + $state = $workflow->getState('draft'); + $transition = $state->getTransitionTo('published'); + $this->assertEquals('Publish', $transition->label()); + } + + /** + * @covers ::getTransitionTo + */ + public function testGetTransitionToException() { + $this->setExpectedException(\InvalidArgumentException::class, "Can not transition to 'published' state"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->addState('draft', 'Draft'); + $state = $workflow->getState('draft'); + $state->getTransitionTo('published'); + } + + /** + * @covers ::getTransitions + */ + public function testGetTransitions() { + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow + ->addState('draft', 'Draft') + ->addState('published', 'Published') + ->addState('archived', 'Archived') + ->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft') + ->addTransition('publish', 'Publish', ['draft'], 'published') + ->addTransition('archive', 'Archive', ['published'], 'archived'); + $state = $workflow->getState('draft'); + $transitions = $state->getTransitions(); + $this->assertCount(2, $transitions); + $this->assertEquals('Create new draft', $transitions['create_new_draft']->label()); + $this->assertEquals('Publish', $transitions['publish']->label()); + } + + /** + * @covers ::labelCallback + */ + public function testLabelCallback() { + $workflow = $this->prophesize(WorkflowInterface::class)->reveal(); + $states = [ + new State($workflow, 'draft', 'Draft'), + new State($workflow, 'published', 'Published'), + ]; + $this->assertEquals(['Draft', 'Published'], array_map([State::class, 'labelCallback'], $states)); + } + +} diff --git a/core/modules/workflows/tests/src/Unit/TransitionTest.php b/core/modules/workflows/tests/src/Unit/TransitionTest.php new file mode 100644 index 0000000000000000000000000000000000000000..3202e8a46644873023dd4c847c87d7955f48e3c4 --- /dev/null +++ b/core/modules/workflows/tests/src/Unit/TransitionTest.php @@ -0,0 +1,71 @@ +prophesize(WorkflowTypeInterface::class); + $workflow_type->decorateState(Argument::any())->willReturnArgument(0); + $workflow_type->decorateTransition(Argument::any())->willReturnArgument(0); + $workflow_manager = $this->prophesize(WorkflowTypeManager::class); + $workflow_manager->createInstance('test_type', Argument::any())->willReturn($workflow_type->reveal()); + $container->set('plugin.manager.workflows.type', $workflow_manager->reveal()); + \Drupal::setContainer($container); + } + + /** + * @covers ::__construct + * @covers ::id + * @covers ::label + */ + public function testGetters() { + $state = new Transition( + $this->prophesize(WorkflowInterface::class)->reveal(), + 'draft_published', + 'Publish', + ['draft'], + 'published' + ); + $this->assertEquals('draft_published', $state->id()); + $this->assertEquals('Publish', $state->label()); + } + + /** + * @covers ::from + * @covers ::to + */ + public function testFromAndTo() { + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow + ->addState('draft', 'Draft') + ->addState('published', 'Published') + ->addTransition('publish', 'Publish', ['draft'], 'published'); + $state = $workflow->getState('draft'); + $transition = $state->getTransitionTo('published'); + $this->assertEquals($state, $transition->from()['draft']); + $this->assertEquals($workflow->getState('published'), $transition->to()); + } + +} diff --git a/core/modules/workflows/tests/src/Unit/WorkflowTest.php b/core/modules/workflows/tests/src/Unit/WorkflowTest.php new file mode 100644 index 0000000000000000000000000000000000000000..49e8671425cd48eb4eefc47dddf6626eec4f42fa --- /dev/null +++ b/core/modules/workflows/tests/src/Unit/WorkflowTest.php @@ -0,0 +1,654 @@ +prophesize(WorkflowTypeInterface::class); + $workflow_type->decorateState(Argument::any())->willReturnArgument(0); + $workflow_type->decorateTransition(Argument::any())->willReturnArgument(0); + $workflow_manager = $this->prophesize(WorkflowTypeManager::class); + $workflow_manager->createInstance('test_type', Argument::any())->willReturn($workflow_type->reveal()); + $container->set('plugin.manager.workflows.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 does not set up a transition to itself. + $this->assertFalse($workflow->hasTransitionFromStateToState('draft', 'draft')); + + // 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 ::addState + */ + public function testAddStateInvalidIdException() { + $this->setExpectedException(\InvalidArgumentException::class, "The state ID 'draft-draft' must contain only lowercase letters, numbers, and underscores"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->addState('draft-draft', 'Draft'); + } + + /** + * @covers ::getStates + */ + public function testGetStates() { + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + + // Getting 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())); + + // The label is also used for sorting if weights are equal. + $workflow->setStateWeight('archived', 0); + $this->assertArrayEquals(['published', 'archived', 'draft'], array_keys($workflow->getStates())); + + // You can limit the states returned by passing in states IDs. + $this->assertArrayEquals(['archived', 'draft'], 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('create_new_draft', 'Create new draft', ['draft'], 'draft') + ->addTransition('publish', 'Publish', ['draft'], 'published'); + + // 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->getTransitionTo('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\workflows\WorkflowTypeInterface::deleteState() is called + // correctly. + $container = new ContainerBuilder(); + $workflow_type = $this->prophesize(WorkflowTypeInterface::class); + $workflow_type->decorateState(Argument::any())->willReturnArgument(0); + $workflow_type->decorateTransition(Argument::any())->willReturnArgument(0); + $workflow_type->deleteState('draft')->shouldBeCalled(); + $workflow_type->deleteTransition('create_new_draft')->shouldBeCalled(); + $workflow_manager = $this->prophesize(WorkflowTypeManager::class); + $workflow_manager->createInstance('test_type', Argument::any())->willReturn($workflow_type->reveal()); + $container->set('plugin.manager.workflows.type', $workflow_manager->reveal()); + \Drupal::setContainer($container); + + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow + ->addState('draft', 'Draft') + ->addState('published', 'Published') + ->addTransition('publish', 'Publish', ['draft', 'published'], 'published') + ->addTransition('create_new_draft', 'Create new draft', ['draft', '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 ::deleteState + */ + public function testDeleteOnlyStateException() { + $this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' can not be deleted from workflow 'test' as it is the only state"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->addState('draft', 'Draft'); + $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 + * @covers ::hasTransition + */ + 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('publish', 'Publish', ['draft'], 'published'); + $this->assertTrue($workflow->getState('draft')->canTransitionTo('published')); + $this->assertEquals(0, $workflow->getTransition('publish')->weight()); + $this->assertTrue($workflow->hasTransition('publish')); + $this->assertFalse($workflow->hasTransition('draft')); + + $workflow->addTransition('save_publish', 'Save', ['published'], 'published'); + $this->assertEquals(1, $workflow->getTransition('save_publish')->weight()); + } + + /** + * @covers ::addTransition + */ + public function testAddTransitionDuplicateException() { + $this->setExpectedException(\InvalidArgumentException::class, "The transition 'publish' already exists in workflow 'test'"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->addState('published', 'Published'); + $workflow->addTransition('publish', 'Publish', ['published'], 'published'); + $workflow->addTransition('publish', 'Publish', ['published'], 'published'); + } + + /** + * @covers ::addTransition + */ + public function testAddTransitionInvalidIdException() { + $this->setExpectedException(\InvalidArgumentException::class, "The transition ID 'publish-publish' must contain only lowercase letters, numbers, and underscores"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->addState('published', 'Published'); + $workflow->addTransition('publish-publish', 'Publish', ['published'], '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('publish', 'Publish', ['draft'], 'published'); + } + + /** + * @covers ::addTransition + */ + public function testAddTransitionDuplicateTransitionStatesException() { + $this->setExpectedException(\InvalidArgumentException::class, "The 'publish' transition already allows 'draft' to 'published' transitions in workflow 'test'"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow + ->addState('draft', 'Draft') + ->addState('published', 'Published'); + $workflow->addTransition('publish', 'Publish', ['draft', 'published'], 'published'); + $workflow->addTransition('draft_to_published', 'Publish a draft', ['draft'], 'published'); + } + + /** + * @covers ::addTransition + */ + public function testAddTransitionConsistentAfterFromCatch() { + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->addState('published', 'Published'); + try { + $workflow->addTransition('publish', 'Publish', ['draft'], 'published'); + } + catch (\InvalidArgumentException $e) { + } + // Ensure that the workflow is not left in an inconsistent state after an + // exception is thrown from Workflow::setTransitionFromStates() whilst + // calling Workflow::addTransition(). + $this->assertFalse($workflow->hasTransition('publish')); + } + + /** + * @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('publish', 'Publish', ['draft'], 'published'); + } + + /** + * @covers ::getTransitions + * @covers ::setTransitionWeight + */ + public function testGetTransitions() { + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + + // Getting transitions works when there are none. + $this->assertArrayEquals([], array_keys($workflow->getTransitions())); + $this->assertArrayEquals([], array_keys($workflow->getTransitions([]))); + + // By default states are ordered in the order added. + $workflow + ->addState('a', 'A') + ->addState('b', 'B') + ->addTransition('a_a', 'A to A', ['a'], 'a') + ->addTransition('a_b', 'A to B', ['a'], 'b'); + + // Ensure we're returning transition objects. + $this->assertInstanceOf(Transition::class, $workflow->getTransitions()['a_a']); + + // Passing in no IDs returns all transitions. + $this->assertArrayEquals(['a_a', 'a_b'], array_keys($workflow->getTransitions())); + + // The order of states is by weight. + $workflow->setTransitionWeight('a_b', -1); + $this->assertArrayEquals(['a_b', 'a_a'], array_keys($workflow->getTransitions())); + + // If all weights are equal it will fallback to labels. + $workflow->setTransitionWeight('a_b', 0); + $this->assertArrayEquals(['a_a', 'a_b'], array_keys($workflow->getTransitions())); + $workflow->setTransitionLabel('a_b', 'A B'); + $this->assertArrayEquals(['a_b', 'a_a'], array_keys($workflow->getTransitions())); + + // You can limit the states returned by passing in states IDs. + $this->assertArrayEquals(['a_a'], array_keys($workflow->getTransitions(['a_a']))); + + // An empty array does not load all states. + $this->assertArrayEquals([], array_keys($workflow->getTransitions([]))); + } + + + /** + * @covers ::getTransition + */ + public function testGetTransition() { + $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('create_new_draft', 'Create new draft', ['draft'], 'draft') + ->addTransition('publish', 'Publish', ['draft'], 'published'); + + // Ensure we're returning state objects and they are set up correctly + $this->assertInstanceOf(Transition::class, $workflow->getTransition('create_new_draft')); + $this->assertEquals('publish', $workflow->getTransition('publish')->id()); + $this->assertEquals('Publish', $workflow->getTransition('publish')->label()); + + $transition = $workflow->getTransition('publish'); + $this->assertEquals($workflow->getState('draft'), $transition->from()['draft']); + $this->assertEquals($workflow->getState('published'), $transition->to()); + } + + /** + * @covers ::getTransition + */ + public function testGetTransitionException() { + $this->setExpectedException(\InvalidArgumentException::class, "The transition 'transition_that_does_not_exist' does not exist in workflow 'test'"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->getTransition('transition_that_does_not_exist'); + } + + /** + * @covers ::getTransitionsForState + */ + public function testGetTransitionsForState() { + $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('create_new_draft', 'Create new draft', ['archived', 'draft'], 'draft') + ->addTransition('publish', 'Publish', ['draft', 'published'], 'published') + ->addTransition('archive', 'Archive', ['published'], 'archived'); + + $this->assertEquals(['create_new_draft', 'publish'], array_keys($workflow->getTransitionsForState('draft'))); + $this->assertEquals(['create_new_draft'], array_keys($workflow->getTransitionsForState('draft', 'to'))); + $this->assertEquals(['publish', 'archive'], array_keys($workflow->getTransitionsForState('published'))); + $this->assertEquals(['publish'], array_keys($workflow->getTransitionsForState('published', 'to'))); + $this->assertEquals(['create_new_draft'], array_keys($workflow->getTransitionsForState('archived', 'from'))); + $this->assertEquals(['archive'], array_keys($workflow->getTransitionsForState('archived', 'to'))); + } + + + /** + * @covers ::getTransitionFromStateToState + * @covers ::hasTransitionFromStateToState + */ + public function testGetTransitionFromStateToState() { + $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('create_new_draft', 'Create new draft', ['archived', 'draft'], 'draft') + ->addTransition('publish', 'Publish', ['draft', 'published'], 'published') + ->addTransition('archive', 'Archive', ['published'], 'archived'); + + $this->assertTrue($workflow->hasTransitionFromStateToState('draft', 'published')); + $this->assertFalse($workflow->hasTransitionFromStateToState('archived', 'archived')); + $transition = $workflow->getTransitionFromStateToState('published', 'archived'); + $this->assertEquals('Archive', $transition->label()); + } + + /** + * @covers ::getTransitionFromStateToState + */ + public function testGetTransitionFromStateToStateException() { + $this->setExpectedException(\InvalidArgumentException::class, "The transition from 'archived' to 'archived' does not exist in workflow 'test'"); + $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('create_new_draft', 'Create new draft', ['archived', 'draft'], 'draft') + ->addTransition('publish', 'Publish', ['draft', 'published'], 'published') + ->addTransition('archive', 'Archive', ['published'], 'archived'); + + $workflow->getTransitionFromStateToState('archived', 'archived'); + } + + /** + * @covers ::setTransitionLabel + */ + public function testSetTransitionLabel() { + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow + ->addState('draft', 'Draft') + ->addState('published', 'Published') + ->addTransition('publish', 'Publish', ['draft'], 'published'); + $this->assertEquals('Publish', $workflow->getTransition('publish')->label()); + $workflow->setTransitionLabel('publish', 'Publish!'); + $this->assertEquals('Publish!', $workflow->getTransition('publish')->label()); + } + + /** + * @covers ::setTransitionLabel + */ + public function testSetTransitionLabelException() { + $this->setExpectedException(\InvalidArgumentException::class, "The transition 'draft-published' 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 ::setTransitionWeight + */ + public function testSetTransitionWeight() { + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow + ->addState('draft', 'Draft') + ->addState('published', 'Published') + ->addTransition('publish', 'Publish', ['draft'], 'published'); + $this->assertEquals(0, $workflow->getTransition('publish')->weight()); + $workflow->setTransitionWeight('publish', 10); + $this->assertEquals(10, $workflow->getTransition('publish')->weight()); + } + + /** + * @covers ::setTransitionWeight + */ + public function testSetTransitionWeightException() { + $this->setExpectedException(\InvalidArgumentException::class, "The transition 'draft-published' does not exist in workflow 'test'"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->addState('published', 'Published'); + $workflow->setTransitionWeight('draft-published', 10); + } + + /** + * @covers ::setTransitionFromStates + */ + public function testSetTransitionFromStates() { + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow + ->addState('draft', 'Draft') + ->addState('published', 'Published') + ->addState('archived', 'Archived') + ->addTransition('test', 'Test', ['draft'], 'draft'); + + $this->assertTrue($workflow->hasTransitionFromStateToState('draft', 'draft')); + $this->assertFalse($workflow->hasTransitionFromStateToState('published', 'draft')); + $this->assertFalse($workflow->hasTransitionFromStateToState('archived', 'draft')); + $workflow->setTransitionFromStates('test', ['draft', 'published', 'archived']); + $this->assertTrue($workflow->hasTransitionFromStateToState('draft', 'draft')); + $this->assertTrue($workflow->hasTransitionFromStateToState('published', 'draft')); + $this->assertTrue($workflow->hasTransitionFromStateToState('archived', 'draft')); + $workflow->setTransitionFromStates('test', ['published', 'archived']); + $this->assertFalse($workflow->hasTransitionFromStateToState('draft', 'draft')); + $this->assertTrue($workflow->hasTransitionFromStateToState('published', 'draft')); + $this->assertTrue($workflow->hasTransitionFromStateToState('archived', 'draft')); + } + + /** + * @covers ::setTransitionFromStates + */ + public function testSetTransitionFromStatesMissingTransition() { + $this->setExpectedException(\InvalidArgumentException::class, "The transition 'test' does not exist in workflow 'test'"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow + ->addState('draft', 'Draft') + ->addState('published', 'Published') + ->addState('archived', 'Archived') + ->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft'); + + $workflow->setTransitionFromStates('test', ['draft', 'published', 'archived']); + } + + /** + * @covers ::setTransitionFromStates + */ + public function testSetTransitionFromStatesMissingState() { + $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') + ->addState('archived', 'Archived') + ->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft'); + + $workflow->setTransitionFromStates('create_new_draft', ['draft', 'published', 'archived']); + } + + /** + * @covers ::deleteTransition + */ + public function testDeleteTransition() { + // Create a container so that the plugin manager and workflow type can be + // mocked and test that + // \Drupal\workflows\WorkflowTypeInterface::deleteState() is called + // correctly. + $container = new ContainerBuilder(); + $workflow_type = $this->prophesize(WorkflowTypeInterface::class); + $workflow_type->decorateState(Argument::any())->willReturnArgument(0); + $workflow_type->decorateTransition(Argument::any())->willReturnArgument(0); + $workflow_type->deleteTransition('publish')->shouldBeCalled(); + $workflow_manager = $this->prophesize(WorkflowTypeManager::class); + $workflow_manager->createInstance('test_type', Argument::any())->willReturn($workflow_type->reveal()); + $container->set('plugin.manager.workflows.type', $workflow_manager->reveal()); + \Drupal::setContainer($container); + + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow + ->addState('draft', 'Draft') + ->addState('published', 'Published') + ->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft') + ->addTransition('publish', 'Publish', ['draft'], 'published'); + $this->assertTrue($workflow->getState('draft')->canTransitionTo('published')); + $workflow->deleteTransition('publish'); + $this->assertFalse($workflow->getState('draft')->canTransitionTo('published')); + $this->assertTrue($workflow->getState('draft')->canTransitionTo('draft')); + } + + /** + * @covers ::deleteTransition + */ + public function testDeleteTransitionException() { + $this->setExpectedException(\InvalidArgumentException::class, "The transition 'draft-published' does not exist in workflow 'test'"); + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $workflow->addState('published', 'Published'); + $workflow->deleteTransition('draft-published'); + } + + /** + * @covers ::status + */ + public function testStatus() { + $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow'); + $this->assertFalse($workflow->status()); + $workflow->addState('published', 'Published'); + $this->assertTrue($workflow->status()); + } + +} diff --git a/core/modules/workflows/workflows.info.yml b/core/modules/workflows/workflows.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..692d086ea23dc6e0cc2a2a22cac15f72e260f144 --- /dev/null +++ b/core/modules/workflows/workflows.info.yml @@ -0,0 +1,7 @@ +name: 'Workflows' +type: module +description: 'Provides UI and API for managing workflows. This module can be used with the Content moderation module to add highly customisable workflows to content.' +version: VERSION +core: 8.x +package: Core (Experimental) +configure: workflows.overview diff --git a/core/modules/workflows/workflows.links.action.yml b/core/modules/workflows/workflows.links.action.yml new file mode 100644 index 0000000000000000000000000000000000000000..e3a80f7778ef5430b08c89d680f917537316cb5a --- /dev/null +++ b/core/modules/workflows/workflows.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/workflows/workflows.links.menu.yml b/core/modules/workflows/workflows.links.menu.yml new file mode 100644 index 0000000000000000000000000000000000000000..a6ac512980a3015c3d81494ca5c9eea6cb5cb74a --- /dev/null +++ b/core/modules/workflows/workflows.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/workflows/workflows.module b/core/modules/workflows/workflows.module new file mode 100644 index 0000000000000000000000000000000000000000..26f72b4b54e3a2a983eb511d9f4bb20a4b7428fb --- /dev/null +++ b/core/modules/workflows/workflows.module @@ -0,0 +1,58 @@ +' . t('About') . ''; + $output .= '

' . t('The Workflows module provides a UI and an API 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 Workflows module.', [':workflow' => 'https://www.drupal.org/documentation/modules/workflows']) . '

'; + return $output; + } +} + +/** + * Implements hook_entity_type_build(). + */ +function workflows_entity_type_build(array &$entity_types) { + /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */ + $entity_types['workflow'] + ->setFormClass('add', WorkflowAddForm::class) + ->setFormClass('edit', WorkflowEditForm::class) + ->setFormClass('delete', WorkflowDeleteForm::class) + ->setFormClass('add-state', WorkflowStateAddForm::class) + ->setFormClass('edit-state', WorkflowStateEditForm::class) + ->setFormClass('delete-state', WorkflowStateDeleteForm::class) + ->setFormClass('add-transition', WorkflowTransitionAddForm::class) + ->setFormClass('edit-transition', WorkflowTransitionEditForm::class) + ->setFormClass('delete-transition', WorkflowTransitionDeleteForm::class) + ->setListBuilderClass(WorkflowListBuilder::class) + ->set('admin_permission', 'administer workflows') + ->setLinkTemplate('add-form', '/admin/config/workflow/workflows/add') + ->setLinkTemplate('edit-form', '/admin/config/workflow/workflows/manage/{workflow}') + ->setLinkTemplate('delete-form', '/admin/config/workflow/workflows/manage/{workflow}/delete') + ->setLinkTemplate('add-state-form', '/admin/config/workflow/workflows/manage/{workflow}/add_state') + ->setLinkTemplate('add-transition-form', '/admin/config/workflow/workflows/manage/{workflow}/add_transition') + ->setLinkTemplate('collection', '/admin/config/workflow/workflows'); +} diff --git a/core/modules/workflows/workflows.permissions.yml b/core/modules/workflows/workflows.permissions.yml new file mode 100644 index 0000000000000000000000000000000000000000..88573b6bb7d95559ca079ac302f246bfd554ee18 --- /dev/null +++ b/core/modules/workflows/workflows.permissions.yml @@ -0,0 +1,4 @@ +'administer workflows': + title: 'Administer workflows' + description: 'Create and edit workflows.' + 'restrict access': TRUE diff --git a/core/modules/workflows/workflows.routing.yml b/core/modules/workflows/workflows.routing.yml new file mode 100644 index 0000000000000000000000000000000000000000..02269cdd6f90106a9049142c6d686b1017b300dd --- /dev/null +++ b/core/modules/workflows/workflows.routing.yml @@ -0,0 +1,47 @@ +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\workflows\Form\WorkflowStateDeleteForm' + _title: 'Delete state' + requirements: + _entity_access: 'workflow.delete-state' + +entity.workflow.add_transition_form: + path: '/admin/config/workflow/workflows/manage/{workflow}/add_transition' + defaults: + _entity_form: 'workflow.add-transition' + _title: 'Add state' + requirements: + _entity_access: 'workflow.edit' + +entity.workflow.edit_transition_form: + path: '/admin/config/workflow/workflows/manage/{workflow}/transition/{workflow_transition}' + defaults: + _entity_form: 'workflow.edit-transition' + _title: 'Edit state' + requirements: + _entity_access: 'workflow.edit' + +entity.workflow.delete_transition_form: + path: '/admin/config/workflow/workflows/manage/{workflow}/transition/{workflow_transition}/delete' + defaults: + _form: '\Drupal\workflows\Form\WorkflowTransitionDeleteForm' + _title: 'Delete state' + requirements: + _entity_access: 'workflow.edit' \ No newline at end of file diff --git a/core/modules/workflows/workflows.services.yml b/core/modules/workflows/workflows.services.yml new file mode 100644 index 0000000000000000000000000000000000000000..772bab71d2a5adb78fd3f361e11e39637c430b07 --- /dev/null +++ b/core/modules/workflows/workflows.services.yml @@ -0,0 +1,6 @@ +services: + plugin.manager.workflows.type: + class: Drupal\workflows\WorkflowTypeManager + parent: default_plugin_manager + tags: + - { name: plugin_manager_cache_clear } \ No newline at end of file