Unverified Commit 6772e2df authored by larowlan's avatar larowlan
Browse files

Issue #2849827 by Sam152, timmillwood, larowlan: Move workflow "settings"...

Issue #2849827 by Sam152, timmillwood, larowlan: Move workflow "settings" setters and getters to WorkflowTypeInterface
parent e562b4e1
......@@ -5,59 +5,55 @@ dependencies:
- 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: 'Restore to Draft'
from:
- archived
to: draft
weight: 3
archived_published:
label: Restore
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:
label: Archived
weight: 5
published: false
default_revision: true
draft:
label: Draft
published: false
default_revision: false
weight: -5
published:
label: Published
published: true
default_revision: true
weight: 0
transitions:
create_new_draft:
label: 'Create New Draft'
to: draft
weight: 0
from:
- draft
- published
publish:
label: Publish
to: published
weight: 1
from:
- draft
- published
archive:
label: Archive
from:
- published
to: archived
weight: 2
archived_draft:
label: 'Restore to Draft'
from:
- archived
to: draft
weight: 3
archived_published:
label: Restore
from:
- archived
to: published
weight: 4
entity_types: { }
......@@ -6,22 +6,30 @@ views.filter.latest_revision:
type: string
label: 'Value'
content_moderation.state:
type: workflows.state
mapping:
published:
type: boolean
label: 'Is published'
default_revision:
type: boolean
label: 'Is default revision'
workflow.type_settings.content_moderation:
type: mapping
mapping:
states:
type: sequence
label: 'Additional state configuration for content moderation'
label: 'Content moderation states'
sequence:
type: mapping
type: content_moderation.state
label: 'States'
mapping:
published:
type: boolean
label: 'Is published'
default_revision:
type: boolean
label: 'Is default revision'
transitions:
type: sequence
sequence:
type: workflows.transition
label: 'Transitions'
entity_types:
type: sequence
label: 'Entity types'
......
......@@ -91,20 +91,6 @@ public static function create(ContainerInterface $container, array $configuratio
);
}
/**
* {@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}
*/
......@@ -119,7 +105,7 @@ public function checkWorkflowAccess(WorkflowInterface $entity, $operation, Accou
* {@inheritdoc}
*/
public function decorateState(StateInterface $state) {
if (isset($this->configuration['states'][$state->id()])) {
if (isset($this->configuration['states'][$state->id()]['published']) && isset($this->configuration['states'][$state->id()]['default_revision'])) {
$state = new ContentModerationState($state, $this->configuration['states'][$state->id()]['published'], $this->configuration['states'][$state->id()]['default_revision']);
}
else {
......@@ -243,16 +229,39 @@ public function addEntityTypeAndBundle($entity_type_id, $bundle_id) {
* {@inheritdoc}
*/
public function defaultConfiguration() {
// This plugin does not store anything per transition.
return [
'states' => [
'draft' => [
'label' => 'Draft',
'published' => FALSE,
'default_revision' => FALSE,
'weight' => 0,
],
'published' => [
'label' => 'Published',
'published' => TRUE,
'default_revision' => TRUE,
'weight' => 1,
],
],
'transitions' => [
'create_new_draft' => [
'label' => 'Create New Draft',
'to' => 'draft',
'weight' => 0,
'from' => [
'draft',
'published',
],
],
'publish' => [
'label' => 'Publish',
'to' => 'published',
'weight' => 1,
'from' => [
'draft',
'published',
],
],
],
'entity_types' => [],
......
......@@ -50,6 +50,9 @@ public function testNewWorkflow() {
$this->assertSession()->pageTextContains('Create New Draft');
$this->assertSession()->pageTextContains('Publish');
$this->assertSession()->linkByHrefNotExists('/admin/config/workflow/workflows/manage/test_workflow/state/draft/delete');
$this->assertSession()->linkByHrefNotExists('/admin/config/workflow/workflows/manage/test_workflow/state/published/delete');
// Ensure after a workflow is created, the bundle information can be
// refreshed.
$entity_bundle_info->clearCachedBundles();
......@@ -63,6 +66,15 @@ public function testNewWorkflow() {
'type_settings[content_moderation][default_revision]' => FALSE,
], 'Save');
$this->assertSession()->pageTextContains('Created Test State state.');
$this->assertSession()->linkByHrefExists('/admin/config/workflow/workflows/manage/test_workflow/state/test_state/delete');
// Check there is a link to delete a default transition.
$this->assertSession()->linkByHrefExists('/admin/config/workflow/workflows/manage/test_workflow/transition/publish/delete');
// Delete the transition.
$this->drupalGet('/admin/config/workflow/workflows/manage/test_workflow/transition/publish/delete');
$this->submitForm([], 'Delete');
// The link to delete the transition should now be gone.
$this->assertSession()->linkByHrefNotExists('/admin/config/workflow/workflows/manage/test_workflow/transition/publish/delete');
// Ensure that the published settings cannot be changed.
$this->drupalGet('admin/config/workflow/workflows/manage/test_workflow/state/published');
......
......@@ -55,37 +55,13 @@ public function permissionsTestCases() {
'id' => 'simple_workflow',
'label' => 'Simple Workflow',
'type' => 'content_moderation',
'transitions' => [
'publish' => [
'label' => 'Publish',
'from' => ['draft'],
'to' => 'published',
'weight' => 0,
],
'unpublish' => [
'label' => 'Unpublish',
'from' => ['published'],
'to' => 'draft',
'weight' => 0,
],
],
'states' => [
'draft' => [
'label' => 'Draft',
'weight' => -5,
],
'published' => [
'label' => 'Published',
'weight' => 0,
],
],
],
[
'use simple_workflow transition publish' => [
'title' => 'Use <em class="placeholder">Publish</em> transition from <em class="placeholder">Simple Workflow</em> workflow.',
],
'use simple_workflow transition unpublish' => [
'title' => 'Use <em class="placeholder">Unpublish</em> transition from <em class="placeholder">Simple Workflow</em> workflow.',
'use simple_workflow transition create_new_draft' => [
'title' => 'Use <em class="placeholder">Create New Draft</em> transition from <em class="placeholder">Simple Workflow</em> workflow.',
],
],
],
......
......@@ -37,9 +37,6 @@ class ContentModerationWorkflowTypeApiTest extends KernelTestBase {
protected function setUp() {
parent::setUp();
$this->workflow = Workflow::create(['id' => 'test', 'type' => 'content_moderation']);
$this->workflow
->addState('draft', 'Draft')
->addState('published', 'Published');
}
/**
......
......@@ -8,8 +8,8 @@
use Drupal\Core\Session\AccountInterface;
use Drupal\content_moderation\StateTransitionValidation;
use Drupal\Tests\UnitTestCase;
use Drupal\workflow_type_test\Plugin\WorkflowType\TestType;
use Drupal\workflows\Entity\Workflow;
use Drupal\workflows\WorkflowTypeInterface;
use Drupal\workflows\WorkflowTypeManager;
use Prophecy\Argument;
......@@ -62,14 +62,8 @@ 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->setConfiguration(Argument::any())->will(function ($arguments) {
$this->getConfiguration()->willReturn($arguments[0]);
});
$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());
$workflow_manager->createInstance('content_moderation', Argument::any())->willReturn(new TestType([], '', []));
$container->set('plugin.manager.workflows.type', $workflow_manager->reveal());
\Drupal::setContainer($container);
......
......@@ -14,38 +14,32 @@ workflows.workflow.*:
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:
workflows.state:
type: mapping
mapping:
label:
type: label
label: 'Label'
weight:
type: integer
label: 'Weight'
workflows.transition:
type: mapping
mapping:
label:
type: label
label: 'Transition label'
from:
type: sequence
label: 'Transitions'
label: 'From state IDs'
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'
type: string
label: 'From state ID'
to:
type: string
label: 'To state ID'
weight:
type: integer
label: 'Weight'
......@@ -7,8 +7,6 @@
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;
/**
......@@ -54,8 +52,6 @@
* config_export = {
* "id",
* "label",
* "states",
* "transitions",
* "type",
* "type_settings"
* },
......@@ -81,38 +77,6 @@ class Workflow extends ConfigEntityBase implements WorkflowInterface, EntityWith
*/
protected $label;
/**
* The states of the workflow.
*
* The array key is the machine name for the state. The structure of each
* array item is:
* @code
* label: {translatable label}
* weight: {integer value}
* @endcode
*
* @var array
*/
protected $states = [];
/**
* The permitted transitions of the workflow.
*
* The array key is the machine name for the transition. The machine name is
* generated from the machine names of the states. The structure of each array
* item is:
* @code
* from:
* - {state machine name}
* - {state machine name}
* to: {state machine name}
* label: {translatable label}
* @endcode
*
* @var array
*/
protected $transitions = [];
/**
* The workflow type plugin ID.
*
......@@ -150,397 +114,204 @@ public function preSave(EntityStorageInterface $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()}'");
}
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),
];
ksort($this->states);
return $this;
public function getTypePlugin() {
return $this->getPluginCollection()->get($this->type);
}
/**
* {@inheritdoc}
*/
public function hasState($state_id) {
return isset($this->states[$state_id]);
public function getPluginCollections() {
return ['type_settings' => $this->getPluginCollection()];
}
/**
* {@inheritdoc}
* Encapsulates the creation of the workflow's plugin collection.
*
* @return \Drupal\Core\Plugin\DefaultSingleLazyPluginCollection
* The workflow's plugin collection.
*/
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);
protected function getPluginCollection() {
if (!$this->pluginCollection && $this->type) {
$this->pluginCollection = new DefaultSingleLazyPluginCollection(\Drupal::service('plugin.manager.workflows.type'), $this->type, $this->type_settings);
}
return $states;
return $this->pluginCollection;
}
/**
* {@inheritdoc}
* 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 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);
public static function loadMultipleByType($type) {
return self::loadMultiple(\Drupal::entityQuery('workflow')->condition('type', $type)->execute());
}
/**
* {@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;
public function status() {
// In order for a workflow to be usable it must have at least one state.
return !empty($this->status) && !empty($this->getTypePlugin()->getStates());
}
/**
* {@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;
public function onDependencyRemoval(array $dependencies) {
$changed = $this->getTypePlugin()->onDependencyRemoval($dependencies);
// Ensure the parent method is called in order to process dependencies that
// affect third party settings.
return parent::onDependencyRemoval($dependencies) || $changed;
}
/**
* {@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);
public function addState($state_id, $label) {
$this->getTypePlugin()->addState($state_id, $label);
return $this;
}