summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNathaniel Catchpole2017-02-10 17:28:48 (GMT)
committerNathaniel Catchpole2017-02-10 17:28:48 (GMT)
commit8f738c844c56cb3b53221f84976425e9dd134dea (patch)
tree67f8df0380bb95702c66ed89e04bbaadf47e06f6
parent172e9e38cc95a7aa1fabd4a2b7122a7b4b5d6764 (diff)
Issue #2844594 by alexpott, scott_euser, timmillwood, Sam152: Default workflow states and transitions
-rw-r--r--core/modules/content_moderation/src/Plugin/WorkflowType/ContentModeration.php44
-rw-r--r--core/modules/content_moderation/src/Tests/ModerationStateNodeTest.php16
-rw-r--r--core/modules/content_moderation/tests/src/Functional/ContentModerationWorkflowTypeTest.php18
-rw-r--r--core/modules/content_moderation/tests/src/Kernel/ContentModerationPermissionsTest.php16
-rw-r--r--core/modules/workflows/src/Annotation/WorkflowType.php9
-rw-r--r--core/modules/workflows/src/Entity/Workflow.php14
-rw-r--r--core/modules/workflows/src/Exception/RequiredStateMissingException.php11
-rw-r--r--core/modules/workflows/src/Form/WorkflowAddForm.php21
-rw-r--r--core/modules/workflows/src/Form/WorkflowEditForm.php13
-rw-r--r--core/modules/workflows/src/Form/WorkflowStateEditForm.php2
-rw-r--r--core/modules/workflows/src/Plugin/WorkflowTypeBase.php14
-rw-r--r--core/modules/workflows/src/WorkflowAccessControlHandler.php14
-rw-r--r--core/modules/workflows/src/WorkflowDeleteAccessCheck.php53
-rw-r--r--core/modules/workflows/src/WorkflowTypeInterface.php27
-rw-r--r--core/modules/workflows/tests/modules/workflow_type_test/config/schema/workflow_type_test.schema.yml9
-rw-r--r--core/modules/workflows/tests/modules/workflow_type_test/src/Plugin/WorkflowType/RequiredStateTestType.php45
-rw-r--r--core/modules/workflows/tests/modules/workflow_type_test/src/Plugin/WorkflowType/TestType.php9
-rw-r--r--core/modules/workflows/tests/src/Functional/WorkflowUiTest.php43
-rw-r--r--core/modules/workflows/tests/src/Kernel/RequiredStatesTest.php121
-rw-r--r--core/modules/workflows/workflows.routing.yml2
-rw-r--r--core/modules/workflows/workflows.services.yml6
21 files changed, 462 insertions, 45 deletions
diff --git a/core/modules/content_moderation/src/Plugin/WorkflowType/ContentModeration.php b/core/modules/content_moderation/src/Plugin/WorkflowType/ContentModeration.php
index a5d0401..67743d4 100644
--- a/core/modules/content_moderation/src/Plugin/WorkflowType/ContentModeration.php
+++ b/core/modules/content_moderation/src/Plugin/WorkflowType/ContentModeration.php
@@ -17,6 +17,10 @@ use Drupal\workflows\WorkflowInterface;
* @WorkflowType(
* id = "content_moderation",
* label = @Translation("Content moderation"),
+ * required_states = {
+ * "draft",
+ * "published",
+ * },
* )
*/
class ContentModeration extends WorkflowTypeBase {
@@ -26,6 +30,20 @@ class ContentModeration extends WorkflowTypeBase {
/**
* {@inheritdoc}
*/
+ public function initializeWorkflow(WorkflowInterface $workflow) {
+ $workflow
+ ->addState('draft', $this->t('Draft'))
+ ->setStateWeight('draft', -5)
+ ->addState('published', $this->t('Published'))
+ ->setStateWeight('published', 0)
+ ->addTransition('create_new_draft', $this->t('Create New Draft'), ['draft', 'published'], 'draft')
+ ->addTransition('publish', $this->t('Publish'), ['draft', 'published'], 'published');
+ return $workflow;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function checkWorkflowAccess(WorkflowInterface $entity, $operation, AccountInterface $account) {
if ($operation === 'view') {
return AccessResult::allowedIfHasPermission($account, 'view content moderation');
@@ -51,12 +69,15 @@ class ContentModeration extends WorkflowTypeBase {
*/
public function buildStateConfigurationForm(FormStateInterface $form_state, WorkflowInterface $workflow, StateInterface $state = NULL) {
/** @var \Drupal\content_moderation\ContentModerationState $state */
+ $is_required_state = isset($state) ? in_array($state->id(), $this->getRequiredStates(), TRUE) : FALSE;
+
$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,
+ '#disabled' => $is_required_state,
];
$form['default_revision'] = [
@@ -64,6 +85,7 @@ class ContentModeration extends WorkflowTypeBase {
'#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,
+ '#disabled' => $is_required_state,
// @todo Add form #state to force "make default" on when "published" is
// on for a state.
// @see https://www.drupal.org/node/2645614
@@ -156,7 +178,16 @@ class ContentModeration extends WorkflowTypeBase {
public function defaultConfiguration() {
// This plugin does not store anything per transition.
return [
- 'states' => [],
+ 'states' => [
+ 'draft' => [
+ 'published' => FALSE,
+ 'default_revision' => FALSE,
+ ],
+ 'published' => [
+ 'published' => TRUE,
+ 'default_revision' => TRUE,
+ ],
+ ],
'entity_types' => [],
];
}
@@ -169,4 +200,15 @@ class ContentModeration extends WorkflowTypeBase {
return [];
}
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfiguration() {
+ $configuration = parent::getConfiguration();
+ // Ensure that states and entity types are ordered consistently.
+ ksort($configuration['states']);
+ ksort($configuration['entity_types']);
+ return $configuration;
+ }
+
}
diff --git a/core/modules/content_moderation/src/Tests/ModerationStateNodeTest.php b/core/modules/content_moderation/src/Tests/ModerationStateNodeTest.php
index a89ec9f..fe36336 100644
--- a/core/modules/content_moderation/src/Tests/ModerationStateNodeTest.php
+++ b/core/modules/content_moderation/src/Tests/ModerationStateNodeTest.php
@@ -4,7 +4,6 @@ namespace Drupal\content_moderation\Tests;
use Drupal\Core\Url;
use Drupal\node\Entity\Node;
-use Drupal\workflows\Entity\Workflow;
/**
* Tests general content moderation workflow for nodes.
@@ -71,21 +70,6 @@ class ModerationStateNodeTest extends ModerationStateTestBase {
$this->fail('Non-moderated test node was not saved correctly.');
}
$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();
-
- $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/tests/src/Functional/ContentModerationWorkflowTypeTest.php b/core/modules/content_moderation/tests/src/Functional/ContentModerationWorkflowTypeTest.php
index b49439b..ff37bd1 100644
--- a/core/modules/content_moderation/tests/src/Functional/ContentModerationWorkflowTypeTest.php
+++ b/core/modules/content_moderation/tests/src/Functional/ContentModerationWorkflowTypeTest.php
@@ -43,13 +43,19 @@ class ContentModerationWorkflowTypeTest extends BrowserTestBase {
'id' => 'test_workflow',
'workflow_type' => 'content_moderation',
], 'Save');
- $this->assertSession()->pageTextContains('Created the Test Workflow Workflow. In order for the workflow to be enabled there needs to be at least one state.');
+
+ // Make sure the test workflow includes the default states and transitions.
+ $this->assertSession()->pageTextContains('Draft');
+ $this->assertSession()->pageTextContains('Published');
+ $this->assertSession()->pageTextContains('Create New Draft');
+ $this->assertSession()->pageTextContains('Publish');
// Ensure after a workflow is created, the bundle information can be
// refreshed.
$entity_bundle_info->clearCachedBundles();
$this->assertNotEmpty($entity_bundle_info->getAllBundleInfo());
+ $this->clickLink('Add a new state');
$this->submitForm([
'label' => 'Test State',
'id' => 'test_state',
@@ -57,6 +63,16 @@ class ContentModerationWorkflowTypeTest extends BrowserTestBase {
'type_settings[content_moderation][default_revision]' => FALSE,
], 'Save');
$this->assertSession()->pageTextContains('Created Test State state.');
+
+ // Ensure that the published settings cannot be changed.
+ $this->drupalGet('admin/config/workflow/workflows/manage/test_workflow/state/published');
+ $this->assertSession()->fieldDisabled('type_settings[content_moderation][published]');
+ $this->assertSession()->fieldDisabled('type_settings[content_moderation][default_revision]');
+
+ // Ensure that the draft settings cannot be changed.
+ $this->drupalGet('admin/config/workflow/workflows/manage/test_workflow/state/draft');
+ $this->assertSession()->fieldDisabled('type_settings[content_moderation][published]');
+ $this->assertSession()->fieldDisabled('type_settings[content_moderation][default_revision]');
}
}
diff --git a/core/modules/content_moderation/tests/src/Kernel/ContentModerationPermissionsTest.php b/core/modules/content_moderation/tests/src/Kernel/ContentModerationPermissionsTest.php
index b20da5b..3a19d22 100644
--- a/core/modules/content_moderation/tests/src/Kernel/ContentModerationPermissionsTest.php
+++ b/core/modules/content_moderation/tests/src/Kernel/ContentModerationPermissionsTest.php
@@ -70,11 +70,13 @@ class ContentModerationPermissionsTest extends KernelTestBase {
],
],
'states' => [
- 'published' => [
- 'label' => 'Published',
- ],
'draft' => [
'label' => 'Draft',
+ 'weight' => -5,
+ ],
+ 'published' => [
+ 'label' => 'Published',
+ 'weight' => 0,
],
],
],
@@ -101,11 +103,13 @@ class ContentModerationPermissionsTest extends KernelTestBase {
],
],
'states' => [
- 'tired' => [
- 'label' => 'Tired',
- ],
'awake' => [
'label' => 'Awake',
+ 'weight' => -5,
+ ],
+ 'tired' => [
+ 'label' => 'Tired',
+ 'weight' => -0,
],
],
],
diff --git a/core/modules/workflows/src/Annotation/WorkflowType.php b/core/modules/workflows/src/Annotation/WorkflowType.php
index 2aa3ff9..6e578ed 100644
--- a/core/modules/workflows/src/Annotation/WorkflowType.php
+++ b/core/modules/workflows/src/Annotation/WorkflowType.php
@@ -41,4 +41,13 @@ class WorkflowType extends Plugin {
*/
public $label = '';
+ /**
+ * States required to exist.
+ *
+ * Normally supplied by WorkflowType::defaultConfiguration().
+ *
+ * @var array
+ */
+ public $required_states = [];
+
}
diff --git a/core/modules/workflows/src/Entity/Workflow.php b/core/modules/workflows/src/Entity/Workflow.php
index 995ad31..330c392 100644
--- a/core/modules/workflows/src/Entity/Workflow.php
+++ b/core/modules/workflows/src/Entity/Workflow.php
@@ -3,8 +3,10 @@
namespace Drupal\workflows\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection;
+use Drupal\workflows\Exception\RequiredStateMissingException;
use Drupal\workflows\State;
use Drupal\workflows\Transition;
use Drupal\workflows\WorkflowInterface;
@@ -115,6 +117,18 @@ class Workflow extends ConfigEntityBase implements WorkflowInterface, EntityWith
/**
* {@inheritdoc}
*/
+ public function preSave(EntityStorageInterface $storage) {
+ $workflow_type = $this->getTypePlugin();
+ $missing_states = array_diff($workflow_type->getRequiredStates(), array_keys($this->getStates()));
+ if (!empty($missing_states)) {
+ throw new RequiredStateMissingException(sprintf("Workflow type '{$workflow_type->label()}' requires states with the ID '%s' in workflow '{$this->id()}'", implode("', '", $missing_states)));
+ }
+ parent::preSave($storage);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function addState($state_id, $label) {
if (isset($this->states[$state_id])) {
throw new \InvalidArgumentException("The state '$state_id' already exists in workflow '{$this->id()}'");
diff --git a/core/modules/workflows/src/Exception/RequiredStateMissingException.php b/core/modules/workflows/src/Exception/RequiredStateMissingException.php
new file mode 100644
index 0000000..7e88bbf
--- /dev/null
+++ b/core/modules/workflows/src/Exception/RequiredStateMissingException.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace Drupal\workflows\Exception;
+
+use Drupal\Core\Config\ConfigException;
+
+/**
+ * Indicates that a workflow does not contain a required state.
+ */
+class RequiredStateMissingException extends ConfigException {
+}
diff --git a/core/modules/workflows/src/Form/WorkflowAddForm.php b/core/modules/workflows/src/Form/WorkflowAddForm.php
index c779b8f..56cbf69 100644
--- a/core/modules/workflows/src/Form/WorkflowAddForm.php
+++ b/core/modules/workflows/src/Form/WorkflowAddForm.php
@@ -84,11 +84,22 @@ class WorkflowAddForm extends EntityForm {
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'));
+ // Initialize the workflow using the selected type plugin.
+ $workflow = $workflow->getTypePlugin()->initializeWorkflow($workflow);
+ $return = $workflow->save();
+ if (empty($workflow->getStates())) {
+ 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'));
+ }
+ else {
+ drupal_set_message($this->t('Created the %label Workflow.', [
+ '%label' => $workflow->label(),
+ ]));
+ $form_state->setRedirectUrl($workflow->toUrl('edit-form'));
+ }
+ return $return;
}
/**
diff --git a/core/modules/workflows/src/Form/WorkflowEditForm.php b/core/modules/workflows/src/Form/WorkflowEditForm.php
index d8f99f9..6b01920 100644
--- a/core/modules/workflows/src/Form/WorkflowEditForm.php
+++ b/core/modules/workflows/src/Form/WorkflowEditForm.php
@@ -78,14 +78,15 @@ class WorkflowEditForm extends EntityForm {
);
}
- $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()])],
+ $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) {
+ if ($this->entity->access('delete-state:' . $state->id())) {
$links['delete'] = [
'title' => t('Delete'),
'url' => Url::fromRoute('entity.workflow.delete_state_form', [
diff --git a/core/modules/workflows/src/Form/WorkflowStateEditForm.php b/core/modules/workflows/src/Form/WorkflowStateEditForm.php
index b326404..75af1d6 100644
--- a/core/modules/workflows/src/Form/WorkflowStateEditForm.php
+++ b/core/modules/workflows/src/Form/WorkflowStateEditForm.php
@@ -151,7 +151,7 @@ class WorkflowStateEditForm extends EntityForm {
$actions['delete'] = [
'#type' => 'link',
'#title' => $this->t('Delete'),
- '#access' => $this->entity->access('delete-state'),
+ '#access' => $this->entity->access('delete-state:' . $this->stateId),
'#attributes' => [
'class' => ['button', 'button--danger'],
],
diff --git a/core/modules/workflows/src/Plugin/WorkflowTypeBase.php b/core/modules/workflows/src/Plugin/WorkflowTypeBase.php
index 1ed9fca..15ced3e 100644
--- a/core/modules/workflows/src/Plugin/WorkflowTypeBase.php
+++ b/core/modules/workflows/src/Plugin/WorkflowTypeBase.php
@@ -34,6 +34,13 @@ abstract class WorkflowTypeBase extends PluginBase implements WorkflowTypeInterf
/**
* {@inheritdoc}
*/
+ public function initializeWorkflow(WorkflowInterface $workflow) {
+ return $workflow;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
public function label() {
$definition = $this->getPluginDefinition();
// The label can be an object.
@@ -108,6 +115,13 @@ abstract class WorkflowTypeBase extends PluginBase implements WorkflowTypeInterf
}
/**
+ * {@inheritdoc}
+ */
+ public function getRequiredStates() {
+ return $this->getPluginDefinition()['required_states'];
+ }
+
+ /**
* {@inheritDoc}
*/
public function defaultConfiguration() {
diff --git a/core/modules/workflows/src/WorkflowAccessControlHandler.php b/core/modules/workflows/src/WorkflowAccessControlHandler.php
index dbeedf5..156f009 100644
--- a/core/modules/workflows/src/WorkflowAccessControlHandler.php
+++ b/core/modules/workflows/src/WorkflowAccessControlHandler.php
@@ -56,17 +56,21 @@ class WorkflowAccessControlHandler extends EntityAccessControlHandler implements
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
- if ($operation === 'delete-state') {
+ /** @var \Drupal\workflows\Entity\Workflow $entity */
+ $workflow_type = $entity->getTypePlugin();
+ if (strpos($operation, 'delete-state') === 0) {
+ list(, $state_id) = explode(':', $operation, 2);
// 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);
+ $admin_access = AccessResult::allowedIf(count($entity->getStates()) > 1)
+ ->andIf(parent::checkAccess($entity, 'edit', $account))
+ ->andIf(AccessResult::allowedIf(!in_array($state_id, $workflow_type->getRequiredStates(), TRUE)))
+ ->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);
+ return $workflow_type->checkWorkflowAccess($entity, $operation, $account)->orIf($admin_access);
}
/**
diff --git a/core/modules/workflows/src/WorkflowDeleteAccessCheck.php b/core/modules/workflows/src/WorkflowDeleteAccessCheck.php
new file mode 100644
index 0000000..df3b7dd
--- /dev/null
+++ b/core/modules/workflows/src/WorkflowDeleteAccessCheck.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace Drupal\workflows;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Provides a access checker for deleting a workflow state.
+ */
+class WorkflowDeleteAccessCheck implements AccessInterface {
+
+ /**
+ * Checks access to deleting a workflow state for a particular route.
+ *
+ * The value of '_workflow_state_delete_access' is ignored. The route must
+ * have the parameters 'workflow' and 'workflow_state'. For example:
+ * @code
+ * pattern: '/foo/{workflow}/bar/{workflow_state}/delete'
+ * requirements:
+ * _workflow_state_delete_access: 'true'
+ * @endcode
+ * @see \Drupal\Core\ParamConverter\EntityConverter
+ *
+ * @param \Symfony\Component\Routing\Route $route
+ * The route to check against.
+ * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+ * The parametrized route
+ * @param \Drupal\Core\Session\AccountInterface $account
+ * The currently logged in account.
+ *
+ * @return \Drupal\Core\Access\AccessResultInterface
+ * The access result.
+ */
+ public function access(Route $route, RouteMatchInterface $route_match, AccountInterface $account) {
+ // If there is valid entity of the given entity type, check its access.
+ $parameters = $route_match->getParameters();
+ if ($parameters->has('workflow') && $parameters->has('workflow_state')) {
+ $entity = $parameters->get('workflow');
+ if ($entity instanceof EntityInterface) {
+ return $entity->access('delete-state:' . $parameters->get('workflow_state'), $account, TRUE);
+ }
+ }
+ // No opinion, so other access checks should decide if access should be
+ // allowed or not.
+ return AccessResult::neutral();
+ }
+
+}
diff --git a/core/modules/workflows/src/WorkflowTypeInterface.php b/core/modules/workflows/src/WorkflowTypeInterface.php
index 17fddec..511cb9d 100644
--- a/core/modules/workflows/src/WorkflowTypeInterface.php
+++ b/core/modules/workflows/src/WorkflowTypeInterface.php
@@ -18,6 +18,21 @@ use Drupal\Core\Session\AccountInterface;
interface WorkflowTypeInterface extends PluginInspectionInterface, DerivativeInspectionInterface, ConfigurablePluginInterface {
/**
+ * Initializes a workflow.
+ *
+ * Used to create required states and default transitions.
+ *
+ * @param \Drupal\workflows\WorkflowInterface $workflow
+ * The workflow to initialize.
+ *
+ * @return \Drupal\workflows\WorkflowInterface $workflow
+ * The initialized workflow.
+ *
+ * @see \Drupal\workflows\Form\WorkflowAddForm::save()
+ */
+ public function initializeWorkflow(WorkflowInterface $workflow);
+
+ /**
* Gets the label for the workflow type.
*
* @return string
@@ -117,4 +132,16 @@ interface WorkflowTypeInterface extends PluginInspectionInterface, DerivativeIns
*/
public function buildTransitionConfigurationForm(FormStateInterface $form_state, WorkflowInterface $workflow, TransitionInterface $transition = NULL);
+ /**
+ * Gets the required states of workflow type.
+ *
+ * This are usually configured in the workflow type annotation.
+ *
+ * @return array[]
+ * The required states.
+ *
+ * @see \Drupal\workflows\Annotation\WorkflowType
+ */
+ public function getRequiredStates();
+
}
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
index 4f12fdd..84e7a41 100644
--- 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
@@ -7,6 +7,15 @@ workflow.type_settings.workflow_type_test:
sequence:
type: ignore
+workflow.type_settings.workflow_type_required_state_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'
diff --git a/core/modules/workflows/tests/modules/workflow_type_test/src/Plugin/WorkflowType/RequiredStateTestType.php b/core/modules/workflows/tests/modules/workflow_type_test/src/Plugin/WorkflowType/RequiredStateTestType.php
new file mode 100644
index 0000000..4933557
--- /dev/null
+++ b/core/modules/workflows/tests/modules/workflow_type_test/src/Plugin/WorkflowType/RequiredStateTestType.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Drupal\workflow_type_test\Plugin\WorkflowType;
+
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\workflows\Plugin\WorkflowTypeBase;
+use Drupal\workflows\WorkflowInterface;
+
+/**
+ * Test workflow type.
+ *
+ * @WorkflowType(
+ * id = "workflow_type_required_state_test",
+ * label = @Translation("Required State Type Test"),
+ * required_states = {
+ * "fresh",
+ * "rotten",
+ * }
+ * )
+ */
+class RequiredStateTestType extends WorkflowTypeBase {
+
+ use StringTranslationTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function initializeWorkflow(WorkflowInterface $workflow) {
+ $workflow
+ ->addState('fresh', $this->t('Fresh'))
+ ->setStateWeight('fresh', -5)
+ ->addState('rotten', $this->t('Rotten'))
+ ->addTransition('rot', $this->t('Rot'), ['fresh'], 'rotten');
+ return $workflow;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ // No configuration is stored for the test type.
+ return [];
+ }
+
+}
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
index d328d63..2ff68a8 100644
--- 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
@@ -22,4 +22,13 @@ class TestType extends WorkflowTypeBase {
return [];
}
+ /**
+ * {@inheritdoc}
+ */
+ public function getRequiredStates() {
+ // Normally this is obtained from the annotation but we get from state to
+ // allow dynamic testing.
+ return \Drupal::state()->get('workflow_type_test.required_states', []);
+ }
+
}
diff --git a/core/modules/workflows/tests/src/Functional/WorkflowUiTest.php b/core/modules/workflows/tests/src/Functional/WorkflowUiTest.php
index 37de109..91c69b7 100644
--- a/core/modules/workflows/tests/src/Functional/WorkflowUiTest.php
+++ b/core/modules/workflows/tests/src/Functional/WorkflowUiTest.php
@@ -66,10 +66,16 @@ class WorkflowUiTest extends BrowserTestBase {
$this->assertSession()->statusCodeEquals(200);
}
+ // Ensure that default states can not be deleted.
+ \Drupal::state()->set('workflow_type_test.required_states', ['published']);
+ $this->drupalGet('admin/config/workflow/workflows/manage/test/state/published/delete');
+ $this->assertSession()->statusCodeEquals(403);
+ \Drupal::state()->set('workflow_type_test.required_states', []);
+
// 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->drupalGet('admin/config/workflow/workflows/manage/test/state/draft/delete');
$this->assertSession()->statusCodeEquals(403);
}
@@ -189,9 +195,28 @@ class WorkflowUiTest extends BrowserTestBase {
// the draft state.
$published_delete_link = Url::fromRoute('entity.workflow.delete_state_form', [
'workflow' => $workflow->id(),
- 'workflow_state' => 'published'
+ 'workflow_state' => 'published',
+ ])->toString();
+ $draft_delete_link = Url::fromRoute('entity.workflow.delete_state_form', [
+ 'workflow' => $workflow->id(),
+ 'workflow_state' => 'draft',
])->toString();
+ $this->assertSession()->elementContains('css', 'tr[data-drupal-selector="edit-states-published"]', 'Delete');
$this->assertSession()->linkByHrefExists($published_delete_link);
+ $this->assertSession()->linkByHrefExists($draft_delete_link);
+
+ // Make the published state a default state and ensure it is no longer
+ // linked.
+ \Drupal::state()->set('workflow_type_test.required_states', ['published']);
+ $this->getSession()->reload();
+ $this->assertSession()->linkByHrefNotExists($published_delete_link);
+ $this->assertSession()->linkByHrefExists($draft_delete_link);
+ $this->assertSession()->elementNotContains('css', 'tr[data-drupal-selector="edit-states-published"]', 'Delete');
+ \Drupal::state()->set('workflow_type_test.required_states', []);
+ $this->getSession()->reload();
+ $this->assertSession()->elementContains('css', 'tr[data-drupal-selector="edit-states-published"]', 'Delete');
+ $this->assertSession()->linkByHrefExists($published_delete_link);
+ $this->assertSession()->linkByHrefExists($draft_delete_link);
// Delete the Draft state.
$this->clickLink('Delete');
@@ -211,6 +236,20 @@ class WorkflowUiTest extends BrowserTestBase {
$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');
+
+ // Ensure that workflow types that implement
+ // \Drupal\workflows\WorkflowTypeInterface::initializeWorkflow() are
+ // initialized correctly.
+ $this->drupalGet('admin/config/workflow/workflows');
+ $this->clickLink('Add workflow');
+ $this->submitForm(['label' => 'Test 2', 'id' => 'test2', 'workflow_type' => 'workflow_type_required_state_test'], 'Save');
+ $this->assertSession()->addressEquals('admin/config/workflow/workflows/manage/test2');
+ $workflow = $workflow_storage->loadUnchanged('test2');
+ $this->assertTrue($workflow->hasState('fresh'), 'The workflow has the "fresh" state');
+ $this->assertTrue($workflow->hasState('rotten'), 'The workflow has the "rotten" state');
+ $this->assertTrue($workflow->hasTransition('rot'), 'The workflow has the "rot" transition');
+ $this->assertSession()->pageTextContains('Fresh');
+ $this->assertSession()->pageTextContains('Rotten');
}
/**
diff --git a/core/modules/workflows/tests/src/Kernel/RequiredStatesTest.php b/core/modules/workflows/tests/src/Kernel/RequiredStatesTest.php
new file mode 100644
index 0000000..18a2c48
--- /dev/null
+++ b/core/modules/workflows/tests/src/Kernel/RequiredStatesTest.php
@@ -0,0 +1,121 @@
+<?php
+
+namespace Drupal\Tests\workflows\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\workflows\Entity\Workflow;
+
+/**
+ * Tests Workflow type's required states and configuration initialization.
+ *
+ * @coversDefaultClass \Drupal\workflows\Plugin\WorkflowTypeBase
+ *
+ * @group workflows
+ */
+class RequiredStatesTest extends KernelTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $modules = ['workflows', 'workflow_type_test'];
+
+ /**
+ * @covers ::getRequiredStates
+ * @covers ::initializeWorkflow
+ * @covers ::__construct
+ */
+ public function testGetRequiredStates() {
+ $workflow = new Workflow([
+ 'id' => 'test',
+ 'type' => 'workflow_type_required_state_test',
+ ], 'workflow');
+ $workflow = $workflow->getTypePlugin()->initializeWorkflow($workflow);
+ $workflow->save();
+ $this->assertEquals(['fresh', 'rotten'], $workflow->getTypePlugin()
+ ->getRequiredStates());
+
+ // Ensure that the workflow has the default configuration.
+ $this->assertTrue($workflow->hasState('rotten'));
+ $this->assertTrue($workflow->hasState('fresh'));
+ $this->assertTrue($workflow->hasTransitionFromStateToState('fresh', 'rotten'));
+ }
+
+ /**
+ * @covers \Drupal\workflows\Entity\Workflow::preSave
+ * @expectedException \Drupal\workflows\Exception\RequiredStateMissingException
+ * @expectedExceptionMessage Required State Type Test' requires states with the ID 'fresh' in workflow 'test'
+ */
+ public function testDeleteRequiredStateAPI() {
+ $workflow = new Workflow([
+ 'id' => 'test',
+ 'type' => 'workflow_type_required_state_test',
+ ], 'workflow');
+ $workflow = $workflow->getTypePlugin()->initializeWorkflow($workflow);
+ $workflow->save();
+ // Ensure that required states can't be deleted.
+ $workflow->deleteState('fresh')->save();
+ }
+
+ /**
+ * @covers \Drupal\workflows\Entity\Workflow::preSave
+ * @expectedException \Drupal\workflows\Exception\RequiredStateMissingException
+ * @expectedExceptionMessage Required State Type Test' requires states with the ID 'fresh', 'rotten' in workflow 'test'
+ */
+ public function testNoStatesRequiredStateAPI() {
+ $workflow = new Workflow([
+ 'id' => 'test',
+ 'type' => 'workflow_type_required_state_test',
+ ], 'workflow');
+ $workflow->save();
+ }
+
+ /**
+ * Ensures that initialized configuration can be changed.
+ */
+ public function testChangeRequiredStateAPI() {
+ $workflow = new Workflow([
+ 'id' => 'test',
+ 'type' => 'workflow_type_required_state_test',
+ ], 'workflow');
+ $workflow = $workflow->getTypePlugin()->initializeWorkflow($workflow);
+ $workflow->save();
+
+ // Ensure states added by default configuration can be changed.
+ $this->assertEquals('Fresh', $workflow->getState('fresh')->label());
+ $workflow
+ ->setStateLabel('fresh', 'Fresher')
+ ->save();
+ $this->assertEquals('Fresher', $workflow->getState('fresh')->label());
+
+ // Ensure transitions can be altered.
+ $workflow
+ ->addState('cooked', 'Cooked')
+ ->setTransitionFromStates('rot', ['fresh', 'cooked'])
+ ->save();
+ $this->assertTrue($workflow->hasTransitionFromStateToState('fresh', 'rotten'));
+ $this->assertTrue($workflow->hasTransitionFromStateToState('cooked', 'rotten'));
+
+ $workflow
+ ->setTransitionFromStates('rot', ['cooked'])
+ ->save();
+ $this->assertFalse($workflow->hasTransitionFromStateToState('fresh', 'rotten'));
+ $this->assertTrue($workflow->hasTransitionFromStateToState('cooked', 'rotten'));
+
+ // Ensure the default configuration does not cause ordering issues.
+ $workflow->addTransition('cook', 'Cook', ['fresh'], 'cooked')->save();
+ $this->assertSame([
+ 'cooked',
+ 'fresh',
+ 'rotten',
+ ], array_keys($workflow->get('states')));
+ $this->assertSame([
+ 'cook',
+ 'rot',
+ ], array_keys($workflow->get('transitions')));
+
+ // Ensure that transitions can be deleted.
+ $workflow->deleteTransition('rot')->save();
+ $this->assertFalse($workflow->hasTransition('rot'));
+ }
+
+}
diff --git a/core/modules/workflows/workflows.routing.yml b/core/modules/workflows/workflows.routing.yml
index 377e572..329ed10 100644
--- a/core/modules/workflows/workflows.routing.yml
+++ b/core/modules/workflows/workflows.routing.yml
@@ -20,7 +20,7 @@ entity.workflow.delete_state_form:
_form: '\Drupal\workflows\Form\WorkflowStateDeleteForm'
_title: 'Delete state'
requirements:
- _entity_access: 'workflow.delete-state'
+ _workflow_state_delete_access: 'true'
entity.workflow.add_transition_form:
path: '/admin/config/workflow/workflows/manage/{workflow}/add_transition'
diff --git a/core/modules/workflows/workflows.services.yml b/core/modules/workflows/workflows.services.yml
index 772bab7..7d32420 100644
--- a/core/modules/workflows/workflows.services.yml
+++ b/core/modules/workflows/workflows.services.yml
@@ -3,4 +3,8 @@ services:
class: Drupal\workflows\WorkflowTypeManager
parent: default_plugin_manager
tags:
- - { name: plugin_manager_cache_clear } \ No newline at end of file
+ - { name: plugin_manager_cache_clear }
+ workflows.access_check.delete_state:
+ class: \Drupal\workflows\WorkflowDeleteAccessCheck
+ tags:
+ - { name: access_check, applies_to: _workflow_state_delete_access }