Update Composer, update everything

This commit is contained in:
Oliver Davies 2018-11-23 12:29:20 +00:00
parent ea3e94409f
commit dda5c284b6
19527 changed files with 1135420 additions and 351004 deletions

View file

@ -13,39 +13,32 @@ workflows.workflow.*:
label: 'Workflow type'
type_settings:
type: workflow.type_settings.[%parent.type]
label: 'Custom settings for workflow type'
states:
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: 'States'
label: 'From state IDs'
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'
type: string
label: 'From state ID'
to:
type: string
label: 'To state ID'
weight:
type: integer
label: 'Weight'

View file

@ -12,14 +12,11 @@ use Drupal\Component\Annotation\Plugin;
* For a working example, see \Drupal\content_moderation\Plugin\Workflow\ContentModerate
*
* @see \Drupal\workflows\WorkflowTypeInterface
* @see \Drupal\workflows\WorkflowManager
* @see \Drupal\workflows\WorkflowTypeManager
* @see workflow_type_info_alter()
* @see plugin_api
*
* @Annotation
*
* @internal
* The workflow system is currently experimental and should only be leveraged
* by experimental modules and development releases of contributed modules.
*/
class WorkflowType extends Plugin {
@ -33,8 +30,6 @@ class WorkflowType extends Plugin {
/**
* The label of the workflow.
*
* Describes how the plugin is used to apply a workflow to something.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
@ -50,4 +45,25 @@ class WorkflowType extends Plugin {
*/
public $required_states = [];
/**
* A list of optional form classes implementing PluginFormInterface.
*
* Forms which will be used for the workflow UI are:
* - 'configure' (\Drupal\workflows\WorkflowTypeInterface::PLUGIN_FORM_KEY)
* - 'state' (\Drupal\workflows\StateInterface::PLUGIN_FORM_KEY)
* - 'transition' (\Drupal\workflows\TransitionInterface::PLUGIN_FORM_KEY)
*
* @see \Drupal\Core\Plugin\PluginWithFormsInterface
* @see \Drupal\Core\Plugin\PluginFormInterface
* @see \Drupal\workflows\Plugin\WorkflowTypeConfigureFormBase
* @see \Drupal\workflows\Plugin\WorkflowTypeStateFormBase
* @see \Drupal\workflows\Plugin\WorkflowTypeTransitionFormBase
* @see \Drupal\workflows\WorkflowTypeInterface::PLUGIN_FORM_KEY
* @see \Drupal\workflows\StateInterface::PLUGIN_FORM_KEY
* @see \Drupal\workflows\TransitionInterface::PLUGIN_FORM_KEY
*
* @var array
*/
public $forms = [];
}

View file

@ -7,8 +7,6 @@ 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;
/**
@ -18,6 +16,12 @@ use Drupal\workflows\WorkflowInterface;
* id = "workflow",
* label = @Translation("Workflow"),
* label_collection = @Translation("Workflows"),
* label_singular = @Translation("workflow"),
* label_plural = @Translation("workflows"),
* label_count = @PluralTranslation(
* singular = "@count workflow",
* plural = "@count workflows",
* ),
* handlers = {
* "access" = "Drupal\workflows\WorkflowAccessControlHandler",
* "list_builder" = "Drupal\workflows\WorkflowListBuilder",
@ -34,7 +38,7 @@ use Drupal\workflows\WorkflowInterface;
* },
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
* }
* },
* },
* config_prefix = "workflow",
* admin_permission = "administer workflows",
@ -49,21 +53,15 @@ use Drupal\workflows\WorkflowInterface;
* "delete-form" = "/admin/config/workflow/workflows/manage/{workflow}/delete",
* "add-state-form" = "/admin/config/workflow/workflows/manage/{workflow}/add_state",
* "add-transition-form" = "/admin/config/workflow/workflows/manage/{workflow}/add_transition",
* "collection" = "/admin/config/workflow/workflows"
* "collection" = "/admin/config/workflow/workflows",
* },
* config_export = {
* "id",
* "label",
* "states",
* "transitions",
* "type",
* "type_settings"
* "type_settings",
* },
* )
*
* @internal
* The workflow system is currently experimental and should only be leveraged
* by experimental modules and development releases of contributed modules.
*/
class Workflow extends ConfigEntityBase implements WorkflowInterface, EntityWithPluginCollectionInterface {
@ -75,44 +73,12 @@ class Workflow extends ConfigEntityBase implements WorkflowInterface, EntityWith
protected $id;
/**
* The Moderation state label.
* The workflow label.
*
* @var string
*/
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.
*
@ -124,6 +90,7 @@ class Workflow extends ConfigEntityBase implements WorkflowInterface, EntityWith
/**
* The configuration for the workflow type plugin.
*
* @var array
*/
protected $type_settings = [];
@ -140,7 +107,7 @@ class Workflow extends ConfigEntityBase implements WorkflowInterface, EntityWith
*/
public function preSave(EntityStorageInterface $storage) {
$workflow_type = $this->getTypePlugin();
$missing_states = array_diff($workflow_type->getRequiredStates(), array_keys($this->getStates()));
$missing_states = array_diff($workflow_type->getRequiredStates(), array_keys($this->getTypePlugin()->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)));
}
@ -150,331 +117,12 @@ class Workflow extends ConfigEntityBase implements WorkflowInterface, EntityWith
/**
* {@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;
}
/**
* {@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 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;
ksort($this->transitions);
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}
* {@inheritdoc}
*/
public function getPluginCollections() {
return ['type_settings' => $this->getPluginCollection()];
@ -509,38 +157,23 @@ class Workflow extends ConfigEntityBase implements WorkflowInterface, EntityWith
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);
return !empty($this->status) && !empty($this->getTypePlugin()->getStates());
}
/**
* {@inheritdoc}
*/
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;
// Give the parent method and the workflow type plugin a chance to react
// to removed dependencies and report if either of these two made a change.
$parent_changed_entity = parent::onDependencyRemoval($dependencies);
$plugin_changed_entity = $this->getTypePlugin()->onDependencyRemoval($dependencies);
return $plugin_changed_entity || $parent_changed_entity;
}
}

View file

@ -11,6 +11,8 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form for adding workflows.
*
* @internal
*/
class WorkflowAddForm extends EntityForm {
@ -53,7 +55,6 @@ class WorkflowAddForm extends EntityForm {
'#title' => $this->t('Label'),
'#maxlength' => 255,
'#default_value' => $workflow->label(),
'#description' => $this->t('Label for the Workflow.'),
'#required' => TRUE,
];
@ -65,9 +66,8 @@ class WorkflowAddForm extends EntityForm {
],
];
$workflow_types = array_map(function ($plugin_definition) {
return $plugin_definition['label'];
}, $this->workflowTypePluginManager->getDefinitions());
$workflow_types = array_column($this->workflowTypePluginManager->getDefinitions(), 'label', 'id');
$form['workflow_type'] = [
'#type' => 'select',
'#title' => $this->t('Workflow type'),
@ -84,17 +84,15 @@ class WorkflowAddForm extends EntityForm {
public function save(array $form, FormStateInterface $form_state) {
/* @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = $this->entity;
// 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.', [
if (empty($workflow->getTypePlugin()->getStates())) {
$this->messenger()->addStatus($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.', [
$this->messenger()->addStatus($this->t('Created the %label Workflow.', [
'%label' => $workflow->label(),
]));
$form_state->setRedirectUrl($workflow->toUrl('edit-form'));

View file

@ -8,9 +8,24 @@ use Drupal\Core\Url;
/**
* Builds the form to delete Workflow entities.
*
* @internal
*/
class WorkflowDeleteForm extends EntityConfirmFormBase {
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
if ($this->entity->getTypePlugin()->workflowHasData($this->entity)) {
$form['#title'] = $this->getQuestion();
$form['description'] = ['#markup' => $this->t('This workflow is in use. You cannot remove this workflow until you have removed all content using it.')];
return $form;
}
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
@ -38,7 +53,7 @@ class WorkflowDeleteForm extends EntityConfirmFormBase {
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->entity->delete();
drupal_set_message($this->t(
$this->messenger()->addStatus($this->t(
'Workflow %label deleted.',
['%label' => $this->entity->label()]
));

View file

@ -2,18 +2,50 @@
namespace Drupal\workflows\Form;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Plugin\PluginFormFactoryInterface;
use Drupal\workflows\Entity\Workflow;
use Drupal\workflows\State;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\workflows\WorkflowTypeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* The form for editing workflows.
*
* @internal
*/
class WorkflowEditForm extends EntityForm {
/**
* The plugin form factory.
*
* @var \Drupal\Core\Plugin\PluginFormFactoryInterface
*/
protected $pluginFormFactory;
/**
* Creates an instance of WorkflowStateEditForm.
*
* @param \Drupal\Core\Plugin\PluginFormFactoryInterface $pluginFormFactory
* The plugin form factory.
*/
public function __construct(PluginFormFactoryInterface $pluginFormFactory) {
$this->pluginFormFactory = $pluginFormFactory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin_form.factory')
);
}
/**
* {@inheritdoc}
*/
@ -22,12 +54,14 @@ class WorkflowEditForm extends EntityForm {
/* @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = $this->entity;
$workflow_type = $workflow->getTypePlugin();
$form['#title'] = $this->t('Edit %label workflow', ['%label' => $workflow->label()]);
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#maxlength' => 255,
'#default_value' => $workflow->label(),
'#description' => $this->t('Label for the Workflow.'),
'#required' => TRUE,
];
@ -43,7 +77,7 @@ class WorkflowEditForm extends EntityForm {
$header = [
'state' => $this->t('State'),
'weight' => $this->t('Weight'),
'operations' => $this->t('Operations')
'operations' => $this->t('Operations'),
];
$form['states_container'] = [
'#type' => 'details',
@ -65,35 +99,33 @@ class WorkflowEditForm extends EntityForm {
],
];
$states = $workflow->getStates();
$states = $workflow->getTypePlugin()->getStates();
// Warn the user if there are no states.
if (empty($states)) {
drupal_set_message(
$this->messenger()->addWarning(
$this->t(
'This workflow has no states and will be disabled until there is at least one, <a href=":add-state">add a new state.</a>',
[':add-state' => $workflow->toUrl('add-state-form')->toString()]
),
'warning'
)
);
}
$state_weight_delta = round(count($states) / 2);
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 ($this->entity->access('delete-state:' . $state->id())) {
$links['delete'] = [
'title' => t('Delete'),
'url' => Url::fromRoute('entity.workflow.delete_state_form', [
'workflow' => $workflow->id(),
'workflow_state' => $state->id()
'workflow_state' => $state->id(),
]),
'attributes' => ['aria-label' => $this->t('Delete @state state', ['@state' => $state->label()])],
];
}
$form['states_container']['states'][$state->id()] = [
@ -106,6 +138,7 @@ class WorkflowEditForm extends EntityForm {
'#title_display' => 'invisible',
'#default_value' => $state->weight(),
'#attributes' => ['class' => ['state-weight']],
'#delta' => $state_weight_delta,
],
'operations' => [
'#type' => 'operations',
@ -122,7 +155,7 @@ class WorkflowEditForm extends EntityForm {
'weight' => $this->t('Weight'),
'from' => $this->t('From'),
'to' => $this->t('To'),
'operations' => $this->t('Operations')
'operations' => $this->t('Operations'),
];
$form['transitions_container'] = [
'#type' => 'details',
@ -142,16 +175,17 @@ class WorkflowEditForm extends EntityForm {
],
],
];
foreach ($workflow->getTransitions() as $transition) {
$transitions = $workflow->getTypePlugin()->getTransitions();
$transition_weight_delta = round(count($transitions) / 2);
foreach ($transitions 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']],
@ -163,6 +197,7 @@ class WorkflowEditForm extends EntityForm {
'#title_display' => 'invisible',
'#default_value' => $transition->weight(),
'#attributes' => ['class' => ['transition-weight']],
'#delta' => $transition_weight_delta,
],
'from' => [
'#theme' => 'item_list',
@ -180,18 +215,52 @@ class WorkflowEditForm extends EntityForm {
'#markup' => $workflow->toLink($this->t('Add a new transition'), 'add-transition-form')->toString(),
];
if ($workflow_type->hasFormClass(WorkflowTypeInterface::PLUGIN_FORM_KEY)) {
$form['type_settings'] = [
'#tree' => TRUE,
];
$subform_state = SubformState::createForSubform($form['type_settings'], $form, $form_state);
$form['type_settings'] += $this->pluginFormFactory
->createInstance($workflow_type, WorkflowTypeInterface::PLUGIN_FORM_KEY)
->buildConfigurationForm($form['type_settings'], $subform_state);
}
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
/* @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = $this->entity;
$workflow_type = $workflow->getTypePlugin();
if ($workflow_type->hasFormClass(WorkflowTypeInterface::PLUGIN_FORM_KEY)) {
$subform_state = SubformState::createForSubform($form['type_settings'], $form, $form_state);
$this->pluginFormFactory
->createInstance($workflow_type, WorkflowTypeInterface::PLUGIN_FORM_KEY)
->validateConfigurationForm($form['type_settings'], $subform_state);
}
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
/* @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = $this->entity;
$workflow_type = $workflow->getTypePlugin();
if ($workflow_type->hasFormClass(WorkflowTypeInterface::PLUGIN_FORM_KEY)) {
$subform_state = SubformState::createForSubform($form['type_settings'], $form, $form_state);
$this->pluginFormFactory
->createInstance($workflow_type, WorkflowTypeInterface::PLUGIN_FORM_KEY)
->submitConfigurationForm($form['type_settings'], $subform_state);
}
$workflow->save();
drupal_set_message($this->t('Saved the %label Workflow.', ['%label' => $workflow->label()]));
$form_state->setRedirectUrl($workflow->toUrl('collection'));
$this->messenger()->addStatus($this->t('Saved the %label Workflow.', ['%label' => $workflow->label()]));
}
/**
@ -205,10 +274,10 @@ class WorkflowEditForm extends EntityForm {
$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']);
$entity->getTypePlugin()->setStateWeight($state_id, $state_values['weight']);
}
foreach ($values['transitions'] as $transition_id => $transition_values) {
$entity->setTransitionWeight($transition_id, $transition_values['weight']);
$entity->getTypePlugin()->setTransitionWeight($transition_id, $transition_values['weight']);
}
}

View file

@ -5,12 +5,44 @@ namespace Drupal\workflows\Form;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Plugin\PluginFormFactoryInterface;
use Drupal\workflows\StateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Class WorkflowStateAddForm.
*
* @internal
*/
class WorkflowStateAddForm extends EntityForm {
/**
* The plugin form factory.
*
* @var \Drupal\Core\Plugin\PluginFormFactoryInterface
*/
protected $pluginFormFactory;
/**
* Creates an instance of WorkflowStateEditForm.
*
* @param \Drupal\Core\Plugin\PluginFormFactoryInterface $pluginFormFactory
* The plugin form factory.
*/
public function __construct(PluginFormFactoryInterface $pluginFormFactory) {
$this->pluginFormFactory = $pluginFormFactory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin_form.factory')
);
}
/**
* {@inheritdoc}
*/
@ -26,12 +58,13 @@ class WorkflowStateAddForm extends EntityForm {
/* @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = $this->getEntity();
$workflow_type = $workflow->getTypePlugin();
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#title' => $this->t('State label'),
'#maxlength' => 255,
'#default_value' => '',
'#description' => $this->t('Label for the state.'),
'#required' => TRUE,
];
@ -42,11 +75,15 @@ class WorkflowStateAddForm extends EntityForm {
],
];
// Add additional form fields from the workflow type plugin.
$form['type_settings'] = [
$workflow->get('type') => $workflow->getTypePlugin()->buildStateConfigurationForm($form_state, $workflow),
'#tree' => TRUE,
];
if ($workflow_type->hasFormClass(StateInterface::PLUGIN_FORM_KEY)) {
$form['type_settings'] = [
'#tree' => TRUE,
];
$subform_state = SubformState::createForSubform($form['type_settings'], $form, $form_state);
$form['type_settings'] += $this->pluginFormFactory
->createInstance($workflow_type, StateInterface::PLUGIN_FORM_KEY)
->buildConfigurationForm($form['type_settings'], $subform_state);
}
return $form;
}
@ -63,7 +100,7 @@ class WorkflowStateAddForm extends EntityForm {
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);
return $original_workflow->getTypePlugin()->hasState($state_id);
}
/**
@ -79,18 +116,29 @@ class WorkflowStateAddForm extends EntityForm {
* 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->getTypePlugin()->addState($values['id'], $values['label']);
}
// 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 validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
/** @var \Drupal\workflows\WorkflowTypeInterface $workflow_type */
$workflow = $this->entity;
$workflow_type = $workflow->getTypePlugin();
if ($workflow_type->hasFormClass(StateInterface::PLUGIN_FORM_KEY)) {
$subform_state = SubformState::createForSubform($form['type_settings'], $form, $form_state);
$this->pluginFormFactory
->createInstance($workflow_type, StateInterface::PLUGIN_FORM_KEY)
->validateConfigurationForm($form['type_settings'], $subform_state);
}
}
@ -100,9 +148,20 @@ class WorkflowStateAddForm extends EntityForm {
public function save(array $form, FormStateInterface $form_state) {
/** @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = $this->entity;
$workflow_type = $workflow->getTypePlugin();
$state = $workflow_type->getState($form_state->getValue('id'));
if ($workflow_type->hasFormClass(StateInterface::PLUGIN_FORM_KEY)) {
$subform_state = SubformState::createForSubform($form['type_settings'], $form, $form_state);
$subform_state->set('state', $state);
$this->pluginFormFactory
->createInstance($workflow_type, StateInterface::PLUGIN_FORM_KEY)
->submitConfigurationForm($form['type_settings'], $subform_state);
}
$workflow->save();
drupal_set_message($this->t('Created %label state.', [
'%label' => $workflow->getState($form_state->getValue('id'))->label(),
$this->messenger()->addStatus($this->t('Created %label state.', [
'%label' => $workflow->getTypePlugin()->getState($form_state->getValue('id'))->label(),
]));
$form_state->setRedirectUrl($workflow->toUrl('edit-form'));
}

View file

@ -9,6 +9,8 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Builds the form to delete states from Workflow entities.
*
* @internal
*/
class WorkflowStateDeleteForm extends ConfirmFormBase {
@ -37,7 +39,7 @@ class WorkflowStateDeleteForm extends ConfirmFormBase {
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to delete %state from %workflow?', ['%state' => $this->workflow->getState($this->stateId)->label(), '%workflow' => $this->workflow->label()]);
return $this->t('Are you sure you want to delete %state from %workflow?', ['%state' => $this->workflow->getTypePlugin()->getState($this->stateId)->label(), '%workflow' => $this->workflow->label()]);
}
/**
@ -70,11 +72,18 @@ class WorkflowStateDeleteForm extends ConfirmFormBase {
* The form structure.
*/
public function buildForm(array $form, FormStateInterface $form_state, WorkflowInterface $workflow = NULL, $workflow_state = NULL) {
if (!$workflow->hasState($workflow_state)) {
if (!$workflow->getTypePlugin()->hasState($workflow_state)) {
throw new NotFoundHttpException();
}
$this->workflow = $workflow;
$this->stateId = $workflow_state;
if ($this->workflow->getTypePlugin()->workflowStateHasData($this->workflow, $this->workflow->getTypePlugin()->getState($this->stateId))) {
$form['#title'] = $this->getQuestion();
$form['description'] = ['#markup' => $this->t('This workflow state is in use. You cannot remove this workflow state until you have removed all content using it.')];
return $form;
}
return parent::buildForm($form, $form_state);
}
@ -83,12 +92,13 @@ class WorkflowStateDeleteForm extends ConfirmFormBase {
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$workflow_label = $this->workflow->getState($this->stateId)->label();
$workflow_label = $this->workflow->getTypePlugin()->getState($this->stateId)->label();
$this->workflow
->deleteState($this->stateId)
->save();
->getTypePlugin()
->deleteState($this->stateId);
$this->workflow->save();
drupal_set_message($this->t(
$this->messenger()->addStatus($this->t(
'State %label deleted.',
['%label' => $workflow_label]
));

View file

@ -5,10 +5,16 @@ namespace Drupal\workflows\Form;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Plugin\PluginFormFactoryInterface;
use Drupal\Core\Url;
use Drupal\workflows\StateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Class WorkflowStateEditForm.
*
* @internal
*/
class WorkflowStateEditForm extends EntityForm {
@ -19,6 +25,32 @@ class WorkflowStateEditForm extends EntityForm {
*/
protected $stateId;
/**
* The plugin form factory.
*
* @var \Drupal\Core\Plugin\PluginFormFactoryInterface
*/
protected $pluginFormFactory;
/**
* Creates an instance of WorkflowStateEditForm.
*
* @param \Drupal\Core\Plugin\PluginFormFactoryInterface $pluginFormFactory
* The plugin form factory.
*/
public function __construct(PluginFormFactoryInterface $pluginFormFactory) {
$this->pluginFormFactory = $pluginFormFactory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin_form.factory')
);
}
/**
* {@inheritdoc}
*/
@ -42,13 +74,14 @@ class WorkflowStateEditForm extends EntityForm {
/* @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = $this->getEntity();
$state = $workflow->getState($this->stateId);
$workflow_type = $workflow->getTypePlugin();
$state = $workflow->getTypePlugin()->getState($this->stateId);
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#title' => $this->t('State label'),
'#maxlength' => 255,
'#default_value' => $state->label(),
'#description' => $this->t('Label for the state.'),
'#required' => TRUE,
];
@ -62,10 +95,16 @@ class WorkflowStateEditForm extends EntityForm {
];
// Add additional form fields from the workflow type plugin.
$form['type_settings'] = [
$workflow->get('type') => $workflow->getTypePlugin()->buildStateConfigurationForm($form_state, $workflow, $state),
'#tree' => TRUE,
];
if ($workflow_type->hasFormClass(StateInterface::PLUGIN_FORM_KEY)) {
$form['type_settings'] = [
'#tree' => TRUE,
];
$subform_state = SubformState::createForSubform($form['type_settings'], $form, $form_state);
$subform_state->set('state', $state);
$form['type_settings'] += $this->pluginFormFactory
->createInstance($workflow_type, StateInterface::PLUGIN_FORM_KEY)
->buildConfigurationForm($form['type_settings'], $subform_state);
}
$header = [
'label' => $this->t('Transition'),
@ -82,14 +121,14 @@ class WorkflowStateEditForm extends EntityForm {
'title' => $this->t('Edit'),
'url' => Url::fromRoute('entity.workflow.edit_transition_form', [
'workflow' => $workflow->id(),
'workflow_transition' => $transition->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()
'workflow_transition' => $transition->id(),
]),
];
$form['transitions'][$transition->id()] = [
@ -122,13 +161,30 @@ class WorkflowStateEditForm extends EntityForm {
* 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->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);
$entity->getTypePlugin()->setStateLabel($values['id'], $values['label']);
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
/** @var \Drupal\workflows\WorkflowTypeInterface $workflow_type */
$workflow = $this->entity;
$workflow_type = $workflow->getTypePlugin();
if ($workflow_type->hasFormClass(StateInterface::PLUGIN_FORM_KEY)) {
$subform_state = SubformState::createForSubform($form['type_settings'], $form, $form_state);
$subform_state->set('state', $workflow_type->getState($this->stateId));
$this->pluginFormFactory
->createInstance($workflow_type, StateInterface::PLUGIN_FORM_KEY)
->validateConfigurationForm($form['type_settings'], $subform_state);
}
}
@ -138,9 +194,19 @@ class WorkflowStateEditForm extends EntityForm {
public function save(array $form, FormStateInterface $form_state) {
/** @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = $this->entity;
$workflow_type = $workflow->getTypePlugin();
if ($workflow_type->hasFormClass(StateInterface::PLUGIN_FORM_KEY)) {
$subform_state = SubformState::createForSubform($form['type_settings'], $form, $form_state);
$subform_state->set('state', $workflow_type->getState($this->stateId));
$this->pluginFormFactory
->createInstance($workflow_type, StateInterface::PLUGIN_FORM_KEY)
->submitConfigurationForm($form['type_settings'], $subform_state);
}
$workflow->save();
drupal_set_message($this->t('Saved %label state.', [
'%label' => $workflow->getState($this->stateId)->label(),
$this->messenger()->addStatus($this->t('Saved %label state.', [
'%label' => $workflow->getTypePlugin()->getState($this->stateId)->label(),
]));
$form_state->setRedirectUrl($workflow->toUrl('edit-form'));
}
@ -164,8 +230,8 @@ class WorkflowStateEditForm extends EntityForm {
],
'#url' => Url::fromRoute('entity.workflow.delete_state_form', [
'workflow' => $this->entity->id(),
'workflow_state' => $this->stateId
])
'workflow_state' => $this->stateId,
]),
];
return $actions;

View file

@ -5,13 +5,45 @@ namespace Drupal\workflows\Form;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Plugin\PluginFormFactoryInterface;
use Drupal\workflows\State;
use Drupal\workflows\TransitionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Class WorkflowTransitionAddForm.
*
* @internal
*/
class WorkflowTransitionAddForm extends EntityForm {
/**
* The plugin form factory.
*
* @var \Drupal\Core\Plugin\PluginFormFactoryInterface
*/
protected $pluginFormFactory;
/**
* Creates an instance of WorkflowStateEditForm.
*
* @param \Drupal\Core\Plugin\PluginFormFactoryInterface $pluginFormFactory
* The plugin form factory.
*/
public function __construct(PluginFormFactoryInterface $pluginFormFactory) {
$this->pluginFormFactory = $pluginFormFactory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin_form.factory')
);
}
/**
* {@inheritdoc}
*/
@ -27,12 +59,13 @@ class WorkflowTransitionAddForm extends EntityForm {
/* @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = $this->getEntity();
$workflow_type = $workflow->getTypePlugin();
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#title' => $this->t('Transition label'),
'#maxlength' => 255,
'#default_value' => '',
'#description' => $this->t('Label for the transition.'),
'#required' => TRUE,
];
@ -45,7 +78,7 @@ class WorkflowTransitionAddForm extends EntityForm {
// @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());
$states = array_map([State::class, 'labelCallback'], $workflow->getTypePlugin()->getStates());
$form['from'] = [
'#type' => 'checkboxes',
'#title' => $this->t('From'),
@ -57,15 +90,19 @@ class WorkflowTransitionAddForm extends EntityForm {
'#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,
];
if ($workflow_type->hasFormClass(TransitionInterface::PLUGIN_FORM_KEY)) {
$form['type_settings'] = [
'#tree' => TRUE,
];
$subform_state = SubformState::createForSubform($form['type_settings'], $form, $form_state);
$form['type_settings'] += $this->pluginFormFactory
->createInstance($workflow_type, TransitionInterface::PLUGIN_FORM_KEY)
->buildConfigurationForm($form['type_settings'], $subform_state);
}
return $form;
}
@ -82,7 +119,7 @@ class WorkflowTransitionAddForm extends EntityForm {
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);
return $original_workflow->getTypePlugin()->hasTransition($transition_id);
}
/**
@ -104,30 +141,33 @@ class WorkflowTransitionAddForm extends EntityForm {
}
/** @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);
}
$entity->getTypePlugin()->addTransition($values['id'], $values['label'], array_filter($values['from']), $values['to']);
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
/** @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = $this->getEntity();
$workflow_type = $workflow->getTypePlugin();
$values = $form_state->getValues();
foreach (array_filter($values['from']) as $from_state_id) {
if ($workflow->hasTransitionFromStateToState($from_state_id, $values['to'])) {
if ($workflow->getTypePlugin()->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(),
'%from' => $workflow->getTypePlugin()->getState($from_state_id)->label(),
'%to' => $workflow->getTypePlugin()->getState($values['to'])->label(),
]));
}
}
if ($workflow_type->hasFormClass(TransitionInterface::PLUGIN_FORM_KEY)) {
$subform_state = SubformState::createForSubform($form['type_settings'], $form, $form_state);
$this->pluginFormFactory
->createInstance($workflow_type, TransitionInterface::PLUGIN_FORM_KEY)
->validateConfigurationForm($form['type_settings'], $subform_state);
}
}
/**
@ -136,8 +176,19 @@ class WorkflowTransitionAddForm extends EntityForm {
public function save(array $form, FormStateInterface $form_state) {
/** @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = $this->entity;
$workflow_type = $workflow->getTypePlugin();
$transition = $workflow_type->getTransition($form_state->getValue('id'));
if ($workflow_type->hasFormClass(TransitionInterface::PLUGIN_FORM_KEY)) {
$subform_state = SubformState::createForSubform($form['type_settings'], $form, $form_state);
$subform_state->set('transition', $transition);
$this->pluginFormFactory
->createInstance($workflow_type, TransitionInterface::PLUGIN_FORM_KEY)
->submitConfigurationForm($form['type_settings'], $subform_state);
}
$workflow->save();
drupal_set_message($this->t('Created %label transition.', [
$this->messenger()->addStatus($this->t('Created %label transition.', [
'%label' => $form_state->getValue('label'),
]));
$form_state->setRedirectUrl($workflow->toUrl('edit-form'));

View file

@ -9,6 +9,8 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Builds the form to delete transitions from Workflow entities.
*
* @internal
*/
class WorkflowTransitionDeleteForm extends ConfirmFormBase {
@ -78,7 +80,7 @@ class WorkflowTransitionDeleteForm extends ConfirmFormBase {
*/
public function buildForm(array $form, FormStateInterface $form_state, WorkflowInterface $workflow = NULL, $workflow_transition = NULL) {
try {
$this->transition = $workflow->getTransition($workflow_transition);
$this->transition = $workflow->getTypePlugin()->getTransition($workflow_transition);
}
catch (\InvalidArgumentException $e) {
throw new NotFoundHttpException();
@ -92,10 +94,11 @@ class WorkflowTransitionDeleteForm extends ConfirmFormBase {
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->workflow
->deleteTransition($this->transition->id())
->save();
->getTypePlugin()
->deleteTransition($this->transition->id());
$this->workflow->save();
drupal_set_message($this->t('%transition transition deleted.', ['%transition' => $this->transition->label()]));
$this->messenger()->addStatus($this->t('%transition transition deleted.', ['%transition' => $this->transition->label()]));
$form_state->setRedirectUrl($this->getCancelUrl());
}

View file

@ -5,11 +5,17 @@ namespace Drupal\workflows\Form;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformState;
use Drupal\Core\Plugin\PluginFormFactoryInterface;
use Drupal\Core\Url;
use Drupal\workflows\State;
use Drupal\workflows\TransitionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Class WorkflowTransitionEditForm.
*
* @internal
*/
class WorkflowTransitionEditForm extends EntityForm {
@ -20,6 +26,32 @@ class WorkflowTransitionEditForm extends EntityForm {
*/
protected $transitionId;
/**
* The plugin form factory.
*
* @var \Drupal\Core\Plugin\PluginFormFactoryInterface
*/
protected $pluginFormFactory;
/**
* Creates an instance of WorkflowStateEditForm.
*
* @param \Drupal\Core\Plugin\PluginFormFactoryInterface $pluginFormFactory
* The plugin form factory.
*/
public function __construct(PluginFormFactoryInterface $pluginFormFactory) {
$this->pluginFormFactory = $pluginFormFactory;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin_form.factory')
);
}
/**
* {@inheritdoc}
*/
@ -43,13 +75,14 @@ class WorkflowTransitionEditForm extends EntityForm {
/* @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = $this->getEntity();
$transition = $workflow->getTransition($this->transitionId);
$workflow_type = $workflow->getTypePlugin();
$transition = $workflow->getTypePlugin()->getTransition($this->transitionId);
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#title' => $this->t('Transition label'),
'#maxlength' => 255,
'#default_value' => $transition->label(),
'#description' => $this->t('Label for the transition.'),
'#required' => TRUE,
];
@ -60,7 +93,7 @@ class WorkflowTransitionEditForm extends EntityForm {
// @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());
$states = array_map([State::class, 'labelCallback'], $workflow->getTypePlugin()->getStates());
$form['from'] = [
'#type' => 'checkboxes',
'#title' => $this->t('From'),
@ -78,10 +111,16 @@ class WorkflowTransitionEditForm extends EntityForm {
];
// Add additional form fields from the workflow type plugin.
$form['type_settings'] = [
$workflow->get('type') => $workflow->getTypePlugin()->buildTransitionConfigurationForm($form_state, $workflow, $transition),
'#tree' => TRUE,
];
if ($workflow_type->hasFormClass(TransitionInterface::PLUGIN_FORM_KEY)) {
$form['type_settings'] = [
'#tree' => TRUE,
];
$subform_state = SubformState::createForSubform($form['type_settings'], $form, $form_state);
$subform_state->set('transition', $transition);
$form['type_settings'] += $this->pluginFormFactory
->createInstance($workflow_type, TransitionInterface::PLUGIN_FORM_KEY)
->buildConfigurationForm($form['type_settings'], $subform_state);
}
return $form;
}
@ -92,18 +131,29 @@ class WorkflowTransitionEditForm extends EntityForm {
public function validateForm(array &$form, FormStateInterface $form_state) {
/** @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = $this->getEntity();
$workflow_type = $workflow->getTypePlugin();
$transition = $workflow_type->getTransition($this->transitionId);
$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']) {
if ($workflow_type->hasTransitionFromStateToState($from_state_id, $values['to'])) {
$existing_transition = $workflow_type->getTransitionFromStateToState($from_state_id, $values['to']);
if ($existing_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(),
'%from' => $workflow->getTypePlugin()->getState($from_state_id)->label(),
'%to' => $workflow->getTypePlugin()->getState($values['to'])->label(),
]));
}
}
}
if ($workflow_type->hasFormClass(TransitionInterface::PLUGIN_FORM_KEY)) {
$subform_state = SubformState::createForSubform($form['type_settings'], $form, $form_state);
$subform_state->set('transition', $transition);
$this->pluginFormFactory
->createInstance($workflow_type, TransitionInterface::PLUGIN_FORM_KEY)
->validateConfigurationForm($form['type_settings'], $subform_state);
}
}
/**
@ -126,13 +176,8 @@ class WorkflowTransitionEditForm extends EntityForm {
/** @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);
}
$entity->getTypePlugin()->setTransitionLabel($values['id'], $values['label']);
$entity->getTypePlugin()->setTransitionFromStates($values['id'], array_filter($values['from']));
}
/**
@ -141,9 +186,20 @@ class WorkflowTransitionEditForm extends EntityForm {
public function save(array $form, FormStateInterface $form_state) {
/** @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = $this->entity;
$workflow_type = $workflow->getTypePlugin();
$transition = $workflow_type->getTransition($this->transitionId);
if ($workflow_type->hasFormClass(TransitionInterface::PLUGIN_FORM_KEY)) {
$subform_state = SubformState::createForSubform($form['type_settings'], $form, $form_state);
$subform_state->set('transition', $transition);
$this->pluginFormFactory
->createInstance($workflow_type, TransitionInterface::PLUGIN_FORM_KEY)
->submitConfigurationForm($form['type_settings'], $subform_state);
}
$workflow->save();
drupal_set_message($this->t('Saved %label transition.', [
'%label' => $workflow->getTransition($this->transitionId)->label(),
$this->messenger()->addStatus($this->t('Saved %label transition.', [
'%label' => $workflow->getTypePlugin()->getTransition($this->transitionId)->label(),
]));
$form_state->setRedirectUrl($workflow->toUrl('edit-form'));
}
@ -168,8 +224,8 @@ class WorkflowTransitionEditForm extends EntityForm {
],
'#url' => Url::fromRoute('entity.workflow.delete_transition_form', [
'workflow' => $this->entity->id(),
'workflow_transition' => $this->transitionId
])
'workflow_transition' => $this->transitionId,
]),
];
return $actions;

View file

@ -3,11 +3,10 @@
namespace Drupal\workflows\Plugin;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Plugin\PluginWithFormsTrait;
use Drupal\workflows\State;
use Drupal\workflows\StateInterface;
use Drupal\workflows\Transition;
use Drupal\workflows\TransitionInterface;
use Drupal\workflows\WorkflowInterface;
use Drupal\workflows\WorkflowTypeInterface;
@ -16,13 +15,16 @@ use Drupal\workflows\WorkflowTypeInterface;
* A base class for Workflow type plugins.
*
* @see \Drupal\workflows\Annotation\WorkflowType
*
* @internal
* The workflow system is currently experimental and should only be leveraged
* by experimental modules and development releases of contributed modules.
*/
abstract class WorkflowTypeBase extends PluginBase implements WorkflowTypeInterface {
use PluginWithFormsTrait;
/**
* A regex for matching a valid state/transition machine name.
*/
const VALID_ID_REGEX = '/[^a-z0-9_]+/';
/**
* {@inheritdoc}
*/
@ -31,13 +33,6 @@ abstract class WorkflowTypeBase extends PluginBase implements WorkflowTypeInterf
$this->setConfiguration($configuration);
}
/**
* {@inheritdoc}
*/
public function initializeWorkflow(WorkflowInterface $workflow) {
return $workflow;
}
/**
* {@inheritdoc}
*/
@ -51,67 +46,29 @@ abstract class WorkflowTypeBase extends PluginBase implements WorkflowTypeInterf
/**
* {@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]);
public function workflowHasData(WorkflowInterface $workflow) {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function buildStateConfigurationForm(FormStateInterface $form_state, WorkflowInterface $workflow, StateInterface $state = NULL) {
return [];
public function workflowStateHasData(WorkflowInterface $workflow, StateInterface $state) {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function buildTransitionConfigurationForm(FormStateInterface $form_state, WorkflowInterface $workflow, TransitionInterface $transition = NULL) {
return [];
}
/**
* {@inheritDoc}
*/
public function getConfiguration() {
return $this->configuration;
}
/**
* {@inheritDoc}
* {@inheritdoc}
*/
public function setConfiguration(array $configuration) {
$this->configuration = NestedArray::mergeDeep(
$this->defaultConfiguration(),
$configuration
);
$this->configuration = $configuration + $this->defaultConfiguration();
}
/**
@ -122,7 +79,7 @@ abstract class WorkflowTypeBase extends PluginBase implements WorkflowTypeInterf
}
/**
* {@inheritDoc}
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
@ -132,7 +89,7 @@ abstract class WorkflowTypeBase extends PluginBase implements WorkflowTypeInterf
}
/**
* {@inheritDoc}
* {@inheritdoc}
*/
public function calculateDependencies() {
return [];
@ -148,9 +105,360 @@ abstract class WorkflowTypeBase extends PluginBase implements WorkflowTypeInterf
/**
* {@inheritdoc}
*/
public function getInitialState(WorkflowInterface $workflow) {
$ordered_states = $workflow->getStates();
public function getInitialState() {
$ordered_states = $this->getStates();
return reset($ordered_states);
}
/**
* {@inheritdoc}
*/
public function addState($state_id, $label) {
if ($this->hasState($state_id)) {
throw new \InvalidArgumentException("The state '$state_id' already exists in workflow.");
}
if (preg_match(static::VALID_ID_REGEX, $state_id)) {
throw new \InvalidArgumentException("The state ID '$state_id' must contain only lowercase letters, numbers, and underscores");
}
$this->configuration['states'][$state_id] = [
'label' => $label,
'weight' => $this->getNextWeight($this->configuration['states']),
];
ksort($this->configuration['states']);
return $this;
}
/**
* {@inheritdoc}
*/
public function hasState($state_id) {
return isset($this->configuration['states'][$state_id]);
}
/**
* {@inheritdoc}
*/
public function getStates($state_ids = NULL) {
if ($state_ids === NULL) {
$state_ids = array_keys($this->configuration['states']);
}
/** @var \Drupal\workflows\StateInterface[] $states */
$states = array_combine($state_ids, array_map([$this, 'getState'], $state_ids));
return static::labelWeightMultisort($states);
}
/**
* {@inheritdoc}
*/
public function getState($state_id) {
if (!isset($this->configuration['states'][$state_id])) {
throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow.");
}
return new State(
$this,
$state_id,
$this->configuration['states'][$state_id]['label'],
$this->configuration['states'][$state_id]['weight']
);
}
/**
* {@inheritdoc}
*/
public function setStateLabel($state_id, $label) {
if (!$this->hasState($state_id)) {
throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow.");
}
$this->configuration['states'][$state_id]['label'] = $label;
return $this;
}
/**
* {@inheritdoc}
*/
public function setStateWeight($state_id, $weight) {
if (!$this->hasState($state_id)) {
throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow.");
}
if (!is_numeric($weight)) {
$label = $this->getState($state_id)->label();
throw new \InvalidArgumentException("The weight '$weight' must be numeric for state '$label'.");
}
$this->configuration['states'][$state_id]['weight'] = $weight;
return $this;
}
/**
* {@inheritdoc}
*/
public function deleteState($state_id) {
if (!$this->hasState($state_id)) {
throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow.");
}
if (count($this->configuration['states']) === 1) {
throw new \InvalidArgumentException("The state '$state_id' can not be deleted from workflow as it is the only state.");
}
foreach ($this->configuration['transitions'] as $transition_id => $transition) {
if ($transition['to'] === $state_id) {
$this->deleteTransition($transition_id);
continue;
}
$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'])) {
// There are no more 'from' entries, remove the transition.
$this->deleteTransition($transition_id);
continue;
}
// We changed the from state, update the transition.
$this->setTransitionFromStates($transition_id, $transition['from']);
}
}
unset($this->configuration['states'][$state_id]);
return $this;
}
/**
* {@inheritdoc}
*/
public function addTransition($transition_id, $label, array $from_state_ids, $to_state_id) {
if ($this->hasTransition($transition_id)) {
throw new \InvalidArgumentException("The transition '$transition_id' already exists in workflow.");
}
if (preg_match(static::VALID_ID_REGEX, $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->configuration['transitions'][$transition_id] = [
'label' => $label,
'from' => [],
'to' => $to_state_id,
// Always add to the end.
'weight' => $this->getNextWeight($this->configuration['transitions']),
];
try {
$this->setTransitionFromStates($transition_id, $from_state_ids);
}
catch (\InvalidArgumentException $e) {
unset($this->configuration['transitions'][$transition_id]);
throw $e;
}
ksort($this->configuration['transitions']);
return $this;
}
/**
* {@inheritdoc}
*/
public function getTransitions(array $transition_ids = NULL) {
if ($transition_ids === NULL) {
$transition_ids = array_keys($this->configuration['transitions']);
}
/** @var \Drupal\workflows\TransitionInterface[] $transitions */
$transitions = array_combine($transition_ids, array_map([$this, 'getTransition'], $transition_ids));
return static::labelWeightMultisort($transitions);
}
/**
* Sort states or transitions by weight, label, and key.
*
* @param \Drupal\workflows\StateInterface[]|\Drupal\workflows\TransitionInterface[] $objects
* An array of state or transition objects to multi-sort, keyed by the
* state or transition ID.
*
* @return \Drupal\workflows\StateInterface[]|\Drupal\workflows\TransitionInterface[]
* An array of sorted transitions or states, keyed by the state or
* transition ID.
*/
protected static function labelWeightMultisort($objects) {
if (count($objects) > 1) {
// Separate weights, labels, and keys into arrays.
$weights = $labels = [];
$keys = array_keys($objects);
foreach ($objects as $id => $object) {
$weights[$id] = $object->weight();
$labels[$id] = $object->label();
}
// Sort weights, labels, and keys in the same order as each other.
array_multisort(
// Use the numerical weight as the primary sort.
$weights, SORT_NUMERIC, SORT_ASC,
// When objects have the same weight, sort them alphabetically by label.
$labels, SORT_NATURAL, SORT_ASC,
// Ensure that the keys (the object IDs) are sorted in the same order as
// the weights.
$keys
);
// Combine keys and weights to make sure the weights are keyed with the
// correct keys.
$weights = array_combine($keys, $weights);
// Return the objects sorted by weight.
return array_replace($weights, $objects);
}
return $objects;
}
/**
* {@inheritdoc}
*/
public function getTransition($transition_id) {
if (!$this->hasTransition($transition_id)) {
throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow.");
}
return new Transition(
$this,
$transition_id,
$this->configuration['transitions'][$transition_id]['label'],
$this->configuration['transitions'][$transition_id]['from'],
$this->configuration['transitions'][$transition_id]['to'],
$this->configuration['transitions'][$transition_id]['weight']
);
}
/**
* {@inheritdoc}
*/
public function hasTransition($transition_id) {
return isset($this->configuration['transitions'][$transition_id]);
}
/**
* {@inheritdoc}
*/
public function getTransitionsForState($state_id, $direction = TransitionInterface::DIRECTION_FROM) {
$transition_ids = array_keys(array_filter($this->configuration['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.");
}
return $this->getTransition($transition_id);
}
/**
* {@inheritdoc}
*/
public function hasTransitionFromStateToState($from_state_id, $to_state_id) {
return $this->getTransitionIdFromStateToState($from_state_id, $to_state_id) !== NULL;
}
/**
* 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->configuration['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 (!$this->hasTransition($transition_id)) {
throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow.");
}
$this->configuration['transitions'][$transition_id]['label'] = $label;
return $this;
}
/**
* {@inheritdoc}
*/
public function setTransitionWeight($transition_id, $weight) {
if (!$this->hasTransition($transition_id)) {
throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow.");
}
if (!is_numeric($weight)) {
$label = $this->getTransition($transition_id)->label();
throw new \InvalidArgumentException("The weight '$weight' must be numeric for transition '$label'.");
}
$this->configuration['transitions'][$transition_id]['weight'] = $weight;
return $this;
}
/**
* {@inheritdoc}
*/
public function setTransitionFromStates($transition_id, array $from_state_ids) {
if (!$this->hasTransition($transition_id)) {
throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow.");
}
// 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.");
}
if ($this->hasTransitionFromStateToState($from_state_id, $this->configuration['transitions'][$transition_id]['to'])) {
$existing_transition_id = $this->getTransitionIdFromStateToState($from_state_id, $this->configuration['transitions'][$transition_id]['to']);
if ($transition_id !== $existing_transition_id) {
throw new \InvalidArgumentException("The '$existing_transition_id' transition already allows '$from_state_id' to '{$this->configuration['transitions'][$transition_id]['to']}' transitions in workflow.");
}
}
}
// 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->configuration['transitions'][$transition_id]['from'] = $from_state_ids;
return $this;
}
/**
* {@inheritdoc}
*/
public function deleteTransition($transition_id) {
if (!$this->hasTransition($transition_id)) {
throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow.");
}
unset($this->configuration['transitions'][$transition_id]);
return $this;
}
/**
* 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);
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace Drupal\workflows\Plugin;
use Drupal\Component\Plugin\PluginAwareInterface;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* A base class for workflow type configuration forms.
*/
abstract class WorkflowTypeConfigureFormBase implements PluginFormInterface, PluginAwareInterface {
use StringTranslationTrait;
/**
* The workflow type.
*
* @var \Drupal\workflows\WorkflowTypeInterface
*/
protected $workflowType;
/**
* {@inheritdoc}
*/
public function setPlugin(PluginInspectionInterface $plugin) {
$this->workflowType = $plugin;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace Drupal\workflows\Plugin;
use Drupal\Component\Plugin\PluginAwareInterface;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* A base class for workflow type state forms.
*/
abstract class WorkflowTypeStateFormBase implements PluginFormInterface, PluginAwareInterface {
use StringTranslationTrait;
/**
* The workflow type.
*
* @var \Drupal\workflows\WorkflowTypeInterface
*/
protected $workflowType;
/**
* {@inheritdoc}
*/
public function setPlugin(PluginInspectionInterface $plugin) {
$this->workflowType = $plugin;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$values = $form_state->getValues();
$state = $form_state->get('state');
$configuration = $this->workflowType->getConfiguration();
$configuration['states'][$state->id()] = $values + $configuration['states'][$state->id()];
$this->workflowType->setConfiguration($configuration);
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace Drupal\workflows\Plugin;
use Drupal\Component\Plugin\PluginAwareInterface;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* A base class for workflow type transition forms.
*/
abstract class WorkflowTypeTransitionFormBase implements PluginFormInterface, PluginAwareInterface {
use StringTranslationTrait;
/**
* The workflow type.
*
* @var \Drupal\workflows\WorkflowTypeInterface
*/
protected $workflowType;
/**
* {@inheritdoc}
*/
public function setPlugin(PluginInspectionInterface $plugin) {
$this->workflowType = $plugin;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$values = $form_state->getValues();
$transition = $form_state->get('transition');
$configuration = $this->workflowType->getConfiguration();
$configuration['transitions'][$transition->id()] = $values + $configuration['transitions'][$transition->id()];
$this->workflowType->setConfiguration($configuration);
}
}

View file

@ -4,10 +4,6 @@ namespace Drupal\workflows;
/**
* A value object representing a workflow state.
*
* @internal
* The workflow system is currently experimental and should only be leveraged
* by experimental modules and development releases of contributed modules.
*/
class State implements StateInterface {
@ -51,7 +47,7 @@ class State implements StateInterface {
* @param int $weight
* The state's weight.
*/
public function __construct(WorkflowInterface $workflow, $id, $label, $weight = 0) {
public function __construct(WorkflowTypeInterface $workflow, $id, $label, $weight = 0) {
$this->workflow = $workflow;
$this->id = $id;
$this->label = $label;
@ -104,7 +100,7 @@ class State implements StateInterface {
}
/**
* Helper method to convert a list of states to labels
* Helper method to convert a State value object to a label.
*
* @param \Drupal\workflows\StateInterface $state
*

View file

@ -5,14 +5,17 @@ namespace Drupal\workflows;
/**
* An interface for state value objects.
*
* @see \Drupal\workflows\WorkflowTypeInterface::decorateState()
*
* @internal
* The workflow system is currently experimental and should only be leveraged
* by experimental modules and development releases of contributed modules.
* The StateInterface should only be used by Workflows and Content Moderation.
* @todo Revisit the need for this in https://www.drupal.org/node/2902309.
*/
interface StateInterface {
/**
* The key of the state plugin form.
*/
const PLUGIN_FORM_KEY = 'state';
/**
* Gets the state's ID.
*

View file

@ -4,10 +4,6 @@ namespace Drupal\workflows;
/**
* A transition value object that describes the transition between states.
*
* @internal
* The workflow system is currently experimental and should only be leveraged
* by experimental modules and development releases of contributed modules.
*/
class Transition implements TransitionInterface {
@ -69,7 +65,7 @@ class Transition implements TransitionInterface {
* @param int $weight
* (optional) The transition's weight. Defaults to 0.
*/
public function __construct(WorkflowInterface $workflow, $id, $label, array $from_state_ids, $to_state_id, $weight = 0) {
public function __construct(WorkflowTypeInterface $workflow, $id, $label, array $from_state_ids, $to_state_id, $weight = 0) {
$this->workflow = $workflow;
$this->id = $id;
$this->label = $label;

View file

@ -6,11 +6,28 @@ namespace Drupal\workflows;
* A transition value object that describes the transition between two states.
*
* @internal
* The workflow system is currently experimental and should only be leveraged
* by experimental modules and development releases of contributed modules.
* The TransitionInterface should only be used by Workflows and Content
* Moderation.
*
* @todo Revisit the need for this in https://www.drupal.org/node/2902309.
*/
interface TransitionInterface {
/**
* The key of the transition plugin form.
*/
const PLUGIN_FORM_KEY = 'transition';
/**
* The transition direction from.
*/
const DIRECTION_FROM = 'from';
/**
* The transition direction to.
*/
const DIRECTION_TO = 'to';
/**
* Gets the transition's ID.
*

View file

@ -12,13 +12,9 @@ use Drupal\Core\Access\AccessResult;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Access controller for the Moderation State entity.
* Access controller for the Workflow entity.
*
* @see \Drupal\workflows\Entity\Workflow.
*
* @internal
* The workflow system is currently experimental and should only be leveraged
* by experimental modules and development releases of contributed modules.
*/
class WorkflowAccessControlHandler extends EntityAccessControlHandler implements EntityHandlerInterface {
@ -62,15 +58,13 @@ class WorkflowAccessControlHandler extends EntityAccessControlHandler implements
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.
$admin_access = AccessResult::allowedIf(count($entity->getStates()) > 1)
return AccessResult::allowedIf(count($entity->getTypePlugin()->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);
}
return $workflow_type->checkWorkflowAccess($entity, $operation, $account)->orIf($admin_access);
return parent::checkAccess($entity, $operation, $account);
}
/**
@ -81,7 +75,9 @@ class WorkflowAccessControlHandler extends EntityAccessControlHandler implements
$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']);
return $admin_access
->andIf(AccessResult::allowedIf($workflow_types_count > 0))
->addCacheTags(['workflow_type_plugins']);
}
}

View file

@ -11,6 +11,10 @@ use Symfony\Component\Routing\Route;
/**
* Provides a access checker for deleting a workflow state.
*
* @internal
* Marked as internal until it's validated this should form part of the public
* API in https://www.drupal.org/node/2897148.
*/
class WorkflowDeleteAccessCheck implements AccessInterface {

View file

@ -6,270 +6,9 @@ use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Provides an interface for defining workflow entities.
*
* @internal
* The workflow system is currently experimental and should only be leveraged
* by experimental modules and development releases of contributed modules.
*/
interface WorkflowInterface extends ConfigEntityInterface {
/**
* Adds a state to the workflow.
*
* @param string $state_id
* The state's ID.
* @param string $label
* The state's label.
*
* @return \Drupal\workflows\WorkflowInterface
* The workflow entity.
*/
public function addState($state_id, $label);
/**
* Determines if the workflow has a state with the provided ID.
*
* @param string $state_id
* The state's ID.
*
* @return bool
* TRUE if the workflow has a state with the provided ID, FALSE if not.
*/
public function hasState($state_id);
/**
* Gets state objects for the provided state IDs.
*
* @param string[] $state_ids
* A list of state IDs to get. If NULL then all states will be returned.
*
* @return \Drupal\workflows\StateInterface[]
* An array of workflow states.
*
* @throws \InvalidArgumentException
* Thrown if $state_ids contains a state ID that does not exist.
*/
public function getStates($state_ids = NULL);
/**
* Gets a workflow state.
*
* @param string $state_id
* The state's ID.
*
* @return \Drupal\workflows\StateInterface
* The workflow state.
*
* @throws \InvalidArgumentException
* Thrown if $state_id does not exist.
*/
public function getState($state_id);
/**
* Sets a state's label.
*
* @param string $state_id
* The state ID to set the label for.
* @param string $label
* The state's label.
*
* @return \Drupal\workflows\WorkflowInterface
* The workflow entity.
*/
public function setStateLabel($state_id, $label);
/**
* Sets a state's weight value.
*
* @param string $state_id
* The state ID to set the weight for.
* @param int $weight
* The state's weight.
*
* @return \Drupal\workflows\WorkflowInterface
* The workflow entity.
*/
public function setStateWeight($state_id, $weight);
/**
* Deletes a state from the workflow.
*
* @param string $state_id
* The state ID to delete.
*
* @return \Drupal\workflows\WorkflowInterface
* The workflow entity.
*
* @throws \InvalidArgumentException
* Thrown if $state_id does not exist.
*/
public function deleteState($state_id);
/**
* Adds a transition to the workflow.
*
* @param string $id
* The transition ID.
* @param string $label
* The transition's label.
* @param array $from_state_ids
* The state IDs to transition from.
* @param string $to_state_id
* The state ID to transition to.
*
* @return \Drupal\workflows\WorkflowInterface
* The workflow entity.
*
* @throws \InvalidArgumentException
* Thrown if either state does not exist.
*/
public function addTransition($id, $label, array $from_state_ids, $to_state_id);
/**
* Gets a transition object for the provided transition ID.
*
* @param string $transition_id
* A transition ID.
*
* @return \Drupal\workflows\TransitionInterface
* The transition.
*
* @throws \InvalidArgumentException
* Thrown if $transition_id does not exist.
*/
public function getTransition($transition_id);
/**
* Determines if a transition exists.
*
* @param string $transition_id
* The transition ID.
*
* @return bool
* TRUE if the transition exists, FALSE if not.
*/
public function hasTransition($transition_id);
/**
* Gets transition objects for the provided transition IDs.
*
* @param string[] $transition_ids
* A list of transition IDs to get. If NULL then all transitions will be
* returned.
*
* @return \Drupal\workflows\TransitionInterface[]
* An array of transition objects.
*
* @throws \InvalidArgumentException
* Thrown if $transition_ids contains a transition ID that does not exist.
*/
public function getTransitions(array $transition_ids = NULL);
/**
* Gets the transition IDs for a state for the provided direction.
*
* @param $state_id
* The state to get transitions for.
* @param string $direction
* (optional) The direction of the transition. Defaults to 'from'. Possible
* values are: 'from' and 'to'.
*
* @return array
* The transition IDs for a state for the provided direction.
*/
public function getTransitionsForState($state_id, $direction = 'from');
/**
* Gets a transition 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 \Drupal\workflows\TransitionInterface
* The transitions.
*
* @throws \InvalidArgumentException
* Thrown if the transition does not exist.
*/
public function getTransitionFromStateToState($from_state_id, $to_state_id);
/**
* Determines if a transition from state to state exists.
*
* @param string $from_state_id
* The state ID to transition from.
* @param string $to_state_id
* The state ID to transition to.
*
* @return bool
* TRUE if the transition exists, FALSE if not.
*/
public function hasTransitionFromStateToState($from_state_id, $to_state_id);
/**
* Sets a transition's label.
*
* @param string $transition_id
* The transition ID.
* @param string $label
* The transition's label.
*
* @return \Drupal\workflows\WorkflowInterface
* The workflow entity.
*
* @throws \InvalidArgumentException
* Thrown if the transition does not exist.
*/
public function setTransitionLabel($transition_id, $label);
/**
* Sets a transition's weight.
*
* @param string $transition_id
* The transition ID.
* @param int $weight
* The transition's weight.
*
* @return \Drupal\workflows\WorkflowInterface
* The workflow entity.
*
* @throws \InvalidArgumentException
* Thrown if the transition does not exist.
*/
public function setTransitionWeight($transition_id, $weight);
/**
* Sets a transition's from states.
*
* @param string $transition_id
* The transition ID.
* @param array $from_state_ids
* The state IDs to transition from.
*
* @return \Drupal\workflows\WorkflowInterface
* The workflow entity.
*
* @throws \InvalidArgumentException
* Thrown if the transition does not exist or the states do not exist.
*/
public function setTransitionFromStates($transition_id, array $from_state_ids);
/**
* Deletes a transition.
*
* @param string $transition_id
* The transition ID.
*
* @return \Drupal\workflows\WorkflowInterface
* The workflow entity.
*
* @throws \InvalidArgumentException
* Thrown if the transition does not exist.
*/
public function deleteTransition($transition_id);
/**
* Gets the workflow type plugin.
*

View file

@ -73,10 +73,10 @@ class WorkflowListBuilder extends ConfigEntityListBuilder {
$row['label'] = $entity->label();
$row['type']['data'] = [
'#markup' => $entity->getTypePlugin()->label()
'#markup' => $entity->getTypePlugin()->label(),
];
$items = array_map([State::class, 'labelCallback'], $entity->getStates());
$items = array_map([State::class, 'labelCallback'], $entity->getTypePlugin()->getStates());
$row['states']['data'] = [
'#theme' => 'item_list',
'#context' => ['list_style' => 'comma-list'],
@ -93,7 +93,7 @@ class WorkflowListBuilder extends ConfigEntityListBuilder {
$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.');
$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 <a href=":content-moderation">Content Moderation</a> module provides a workflow type that enables workflows for content entities.', [':content-moderation' => '/admin/modules#module-content-moderation']);
}
return $build;
}

View file

@ -4,33 +4,17 @@ namespace Drupal\workflows;
use Drupal\Component\Plugin\ConfigurablePluginInterface;
use Drupal\Component\Plugin\DerivativeInspectionInterface;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Plugin\PluginWithFormsInterface;
/**
* An interface for Workflow type plugins.
*
* @internal
* The workflow system is currently experimental and should only be leveraged
* by experimental modules and development releases of contributed modules.
*/
interface WorkflowTypeInterface extends PluginInspectionInterface, DerivativeInspectionInterface, ConfigurablePluginInterface {
interface WorkflowTypeInterface extends PluginWithFormsInterface, 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
* The initialized workflow.
*
* @see \Drupal\workflows\Form\WorkflowAddForm::save()
* The key of the global workflow plugin form.
*/
public function initializeWorkflow(WorkflowInterface $workflow);
const PLUGIN_FORM_KEY = 'configure';
/**
* Gets the label for the workflow type.
@ -41,114 +25,51 @@ interface WorkflowTypeInterface extends PluginInspectionInterface, DerivativeIns
public function label();
/**
* Performs access checks.
* Determines if the workflow is being has data associated with it.
*
* @param \Drupal\workflows\WorkflowInterface $entity
* The workflow entity for which to check access.
* @param string $operation
* The entity operation. Usually one of 'view', 'view label', 'update' or
* 'delete'.
* @param \Drupal\Core\Session\AccountInterface $account
* The user for which to check access.
* @internal
* Marked as internal until it's validated this should form part of the
* public API in https://www.drupal.org/node/2897148.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
* @param \Drupal\workflows\WorkflowInterface $workflow
* The workflow to check.
*
* @return bool
* TRUE if the workflow is being used, FALSE if not.
*/
public function checkWorkflowAccess(WorkflowInterface $entity, $operation, AccountInterface $account);
public function workflowHasData(WorkflowInterface $workflow);
/**
* Decorates states so the WorkflowType can add additional information.
* Determines if the workflow state has data associated with it.
*
* @internal
* Marked as internal until it's validated this should form part of the
* public API in https://www.drupal.org/node/2897148.
*
* @param \Drupal\workflows\WorkflowInterface $workflow
* The workflow to check.
* @param \Drupal\workflows\StateInterface $state
* The state object to decorate.
* The workflow state to check.
*
* @return \Drupal\workflows\StateInterface
* The decorated state object.
* @return bool
* TRUE if the workflow state is being used, FALSE if not.
*/
public function decorateState(StateInterface $state);
/**
* React to the removal of a state from a workflow.
*
* @param string $state_id
* The state ID of the state that is being removed.
*/
public function deleteState($state_id);
/**
* Decorates transitions so the WorkflowType can add additional information.
* @param \Drupal\workflows\TransitionInterface $transition
* The transition object to decorate.
*
* @return \Drupal\workflows\TransitionInterface
* The decorated transition object.
*/
public function decorateTransition(TransitionInterface $transition);
/**
* React to the removal of a transition from a workflow.
*
* @param string $transition_id
* The transition ID of the transition that is being removed.
*/
public function deleteTransition($transition_id);
public function workflowStateHasData(WorkflowInterface $workflow, StateInterface $state);
/**
* Gets the initial state for the workflow.
*
* @param \Drupal\workflows\WorkflowInterface $workflow
* The workflow entity.
*
* @return \Drupal\workflows\StateInterface
* The initial state.
*/
public function getInitialState(WorkflowInterface $workflow);
/**
* Builds a form to be added to the Workflow state edit form.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param \Drupal\workflows\WorkflowInterface $workflow
* The workflow the state is attached to.
* @param \Drupal\workflows\StateInterface|null $state
* The workflow state being edited. If NULL, a new state is being added.
*
* @return array
* Form elements to add to a workflow state form for customisations to the
* workflow.
*
* @see \Drupal\workflows\Form\WorkflowStateAddForm::form()
* @see \Drupal\workflows\Form\WorkflowStateEditForm::form()
*/
public function buildStateConfigurationForm(FormStateInterface $form_state, WorkflowInterface $workflow, StateInterface $state = NULL);
/**
* Builds a form to be added to the Workflow transition edit form.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param \Drupal\workflows\WorkflowInterface $workflow
* The workflow the state is attached to.
* @param \Drupal\workflows\TransitionInterface|null $transition
* The workflow transition being edited. If NULL, a new transition is being
* added.
*
* @return array
* Form elements to add to a workflow transition form for customisations to
* the workflow.
*
* @see \Drupal\workflows\Form\WorkflowTransitionAddForm::form()
* @see \Drupal\workflows\Form\WorkflowTransitionEditForm::form()
*/
public function buildTransitionConfigurationForm(FormStateInterface $form_state, WorkflowInterface $workflow, TransitionInterface $transition = NULL);
public function getInitialState();
/**
* Gets the required states of workflow type.
*
* This are usually configured in the workflow type annotation.
* This is usually specified in the workflow type annotation.
*
* @return array[]
* @return string[]
* The required states.
*
* @see \Drupal\workflows\Annotation\WorkflowType
@ -170,4 +91,260 @@ interface WorkflowTypeInterface extends PluginInspectionInterface, DerivativeIns
*/
public function onDependencyRemoval(array $dependencies);
/**
* Adds a state to the workflow.
*
* @param string $state_id
* The state's ID.
* @param string $label
* The state's label.
*
* @return $this
*
* @throws \InvalidArgumentException
* Thrown if a state already exists or state ID is invalid.
*/
public function addState($state_id, $label);
/**
* Determines if the workflow has a state with the provided ID.
*
* @param string $state_id
* The state's ID.
*
* @return bool
* TRUE if the workflow has a state with the provided ID, FALSE if not.
*/
public function hasState($state_id);
/**
* Gets state objects for the provided state IDs.
*
* @param string[] $state_ids
* A list of state IDs to get. If NULL then all states will be returned.
*
* @return \Drupal\workflows\StateInterface[]
* An array of workflow states, keyed by state IDs.
*
* @throws \InvalidArgumentException
* Thrown if $state_ids contains a state ID that does not exist.
*/
public function getStates($state_ids = NULL);
/**
* Gets a workflow state.
*
* @param string $state_id
* The state's ID.
*
* @return \Drupal\workflows\StateInterface
* The workflow state.
*
* @throws \InvalidArgumentException
* Thrown if $state_id does not exist.
*/
public function getState($state_id);
/**
* Sets a state's label.
*
* @param string $state_id
* The state ID to set the label for.
* @param string $label
* The state's label.
*
* @return $this
*/
public function setStateLabel($state_id, $label);
/**
* Sets a state's weight value.
*
* @param string $state_id
* The state ID to set the weight for.
* @param int $weight
* The state's weight.
*
* @return $this
*/
public function setStateWeight($state_id, $weight);
/**
* Deletes a state from the workflow.
*
* @param string $state_id
* The state ID to delete.
*
* @return \Drupal\workflows\WorkflowTypeInterface
* The workflow type plugin.
*
* @throws \InvalidArgumentException
* Thrown if $state_id does not exist.
*/
public function deleteState($state_id);
/**
* Adds a transition to the workflow.
*
* @param string $id
* The transition ID.
* @param string $label
* The transition's label.
* @param array $from_state_ids
* The state IDs to transition from.
* @param string $to_state_id
* The state ID to transition to.
*
* @return $this
*
* @throws \InvalidArgumentException
* Thrown if either state does not exist.
*/
public function addTransition($id, $label, array $from_state_ids, $to_state_id);
/**
* Gets a transition object for the provided transition ID.
*
* @param string $transition_id
* A transition ID.
*
* @return \Drupal\workflows\TransitionInterface
* The transition.
*
* @throws \InvalidArgumentException
* Thrown if $transition_id does not exist.
*/
public function getTransition($transition_id);
/**
* Determines if a transition exists.
*
* @param string $transition_id
* The transition ID.
*
* @return bool
* TRUE if the transition exists, FALSE if not.
*/
public function hasTransition($transition_id);
/**
* Gets transition objects for the provided transition IDs.
*
* @param string[] $transition_ids
* A list of transition IDs to get. If NULL then all transitions will be
* returned.
*
* @return \Drupal\workflows\TransitionInterface[]
* An array of transition objects.
*
* @throws \InvalidArgumentException
* Thrown if $transition_ids contains a transition ID that does not exist.
*/
public function getTransitions(array $transition_ids = NULL);
/**
* Gets the transition IDs for a state for the provided direction.
*
* @param $state_id
* The state to get transitions for.
* @param string $direction
* (optional) The direction of the transition, defaults to
* TransitionInterface::DIRECTION_FROM. Possible values are:
* TransitionInterface::DIRECTION_FROM or TransitionInterface::DIRECTION_TO.
*
* @return array
* The transition IDs for a state for the provided direction.
*
* @see \Drupal\workflows\TransitionInterface::DIRECTION_FROM
* @see \Drupal\workflows\TransitionInterface::DIRECTION_TO
*/
public function getTransitionsForState($state_id, $direction = TransitionInterface::DIRECTION_FROM);
/**
* Gets a transition 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 \Drupal\workflows\TransitionInterface
* The transitions.
*
* @throws \InvalidArgumentException
* Thrown if the transition does not exist.
*/
public function getTransitionFromStateToState($from_state_id, $to_state_id);
/**
* Determines if a transition from state to state exists.
*
* @param string $from_state_id
* The state ID to transition from.
* @param string $to_state_id
* The state ID to transition to.
*
* @return bool
* TRUE if the transition exists, FALSE if not.
*/
public function hasTransitionFromStateToState($from_state_id, $to_state_id);
/**
* Sets a transition's label.
*
* @param string $transition_id
* The transition ID.
* @param string $label
* The transition's label.
*
* @return $this
*
* @throws \InvalidArgumentException
* Thrown if the transition does not exist.
*/
public function setTransitionLabel($transition_id, $label);
/**
* Sets a transition's weight.
*
* @param string $transition_id
* The transition ID.
* @param int $weight
* The transition's weight.
*
* @return $this
*
* @throws \InvalidArgumentException
* Thrown if the transition does not exist.
*/
public function setTransitionWeight($transition_id, $weight);
/**
* Sets a transition's from states.
*
* @param string $transition_id
* The transition ID.
* @param array $from_state_ids
* The state IDs to transition from.
*
* @return $this
*
* @throws \InvalidArgumentException
* Thrown if the transition does not exist or the states do not exist.
*/
public function setTransitionFromStates($transition_id, array $from_state_ids);
/**
* Deletes a transition.
*
* @param string $transition_id
* The transition ID.
*
* @return $this
*
* @throws \InvalidArgumentException
* Thrown if the transition does not exist.
*/
public function deleteTransition($transition_id);
}

View file

@ -13,10 +13,6 @@ use Drupal\workflows\Annotation\WorkflowType;
* @see \Drupal\workflows\Annotation\WorkflowType
* @see \Drupal\workflows\WorkflowTypeInterface
* @see plugin_api
*
* @internal
* The workflow system is currently experimental and should only be leveraged
* by experimental modules and development releases of contributed modules.
*/
class WorkflowTypeManager extends DefaultPluginManager {
@ -34,7 +30,7 @@ class WorkflowTypeManager extends DefaultPluginManager {
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/WorkflowType', $namespaces, $module_handler, WorkflowTypeInterface::class, WorkflowType::class);
$this->alterInfo('workflow_type_info');
$this->setCacheBackend($cache_backend, 'workflow_type_info');
$this->setCacheBackend($cache_backend, 'workflow_type_info', ['workflow_type_plugins']);
}
}

View file

@ -5,4 +5,4 @@ package: Testing
version: VERSION
core: 8.x
dependencies:
- workflows
- drupal:workflows

View file

@ -6,6 +6,10 @@ workflow.type_settings.workflow_type_test:
type: sequence
sequence:
type: ignore
transitions:
type: sequence
sequence:
type: ignore
workflow.type_settings.workflow_type_required_state_test:
type: mapping
@ -15,28 +19,54 @@ workflow.type_settings.workflow_type_required_state_test:
type: sequence
sequence:
type: ignore
transitions:
type: sequence
sequence:
type: ignore
# @todo, inline this straight into "workflow.type_settings.workflow_type_complex_test"
# after https://www.drupal.org/node/2871746 is resolved.
workflows.state.complex_test_state:
type: workflows.state
mapping:
extra:
type: string
label: 'Extra information'
workflows.state.complex_test_transition:
type: workflows.transition
mapping:
extra:
type: string
label: 'Extra information'
workflow.type_settings.workflow_type_complex_test:
type: mapping
label: 'Workflow complex test type settings'
mapping:
example_setting:
type: string
label: 'Example setting'
states:
type: sequence
label: 'Additional state configuration'
label: 'States'
sequence:
type: mapping
type: workflows.state.complex_test_state
label: 'States'
mapping:
extra:
type: string
label: 'Extra information'
transitions:
type: sequence
label: 'Additional transition configuration'
label: 'Transitions'
sequence:
type: mapping
label: 'Transitions'
mapping:
extra:
type: string
label: 'Extra information'
type: workflows.state.complex_test_transition
workflow.type_settings.predefined_states_workflow_test_type:
type: mapping
label: 'Predefined states workflow test type'
mapping:
transitions:
type: sequence
label: 'Transitions'
sequence:
label: 'Transitions'
type: workflows.transition

View file

@ -1,90 +0,0 @@
<?php
namespace Drupal\workflow_type_test;
use Drupal\workflows\StateInterface;
/**
* A value object representing a workflow state.
*/
class DecoratedState implements StateInterface {
/**
* The vanilla state object from the Workflow module.
*
* @var \Drupal\workflows\StateInterface
*/
protected $state;
/**
* Extra information added to state.
*
* @var string
*/
protected $extra;
/**
* DecoratedState constructor.
*
* @param \Drupal\workflows\StateInterface $state
* The vanilla state object from the Workflow module.
* @param string $extra
* (optional) Extra information stored on the state. Defaults to ''.
*/
public function __construct(StateInterface $state, $extra = '') {
$this->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();
}
}

View file

@ -1,83 +0,0 @@
<?php
namespace Drupal\workflow_type_test;
use Drupal\workflows\TransitionInterface;
/**
* A value object representing a workflow transition.
*/
class DecoratedTransition implements TransitionInterface {
/**
* The vanilla transition object from the Workflow module.
*
* @var \Drupal\workflows\TransitionInterface
*/
protected $transition;
/**
* Extra information added to transition.
*
* @var string
*/
protected $extra;
/**
* DecoratedTransition constructor.
*
* @param \Drupal\workflows\TransitionInterface $transition
* The vanilla transition object from the Workflow module.
* @param string $extra
* (optional) Extra information stored on the transition. Defaults to ''.
*/
public function __construct(TransitionInterface $transition, $extra = '') {
$this->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();
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace Drupal\workflow_type_test\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\workflows\Plugin\WorkflowTypeConfigureFormBase;
/**
* Form to configure the complex test workflow type.
*
* @see \Drupal\workflow_type_test\Plugin\WorkflowType\ComplexTestType
*/
class ComplexTestTypeConfigureForm extends WorkflowTypeConfigureFormBase {
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$type_configuration = $this->workflowType->getConfiguration();
$form['example_setting'] = [
'#type' => 'textfield',
'#title' => $this->t('Example global workflow setting'),
'#description' => $this->t('Extra information added to the workflow'),
'#default_value' => $type_configuration['example_setting'],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$type_configuration = $this->workflowType->getConfiguration();
$type_configuration['example_setting'] = $form_state->getValue('example_setting');
$this->workflowType->setConfiguration($type_configuration);
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace Drupal\workflow_type_test\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\workflows\Plugin\WorkflowTypeStateFormBase;
/**
* Form to configure the complex test workflow states.
*
* @see \Drupal\workflow_type_test\Plugin\WorkflowType\ComplexTestType
*/
class ComplexTestTypeStateForm extends WorkflowTypeStateFormBase {
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$state = $form_state->get('state');
$configuration = $this->workflowType->getConfiguration();
$form['extra'] = [
'#type' => 'textfield',
'#title' => $this->t('Extra'),
'#description' => $this->t('Extra information added to state'),
'#default_value' => $state && isset($configuration['states'][$state->id()]['extra']) ? $configuration['states'][$state->id()]['extra'] : '',
];
return $form;
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace Drupal\workflow_type_test\Form;
use Drupal\Core\Form\FormStateInterface;
use Drupal\workflows\Plugin\WorkflowTypeTransitionFormBase;
/**
* Form to configure the complex test workflow states.
*
* @see \Drupal\workflow_type_test\Plugin\WorkflowType\ComplexTestType
*/
class ComplexTestTypeTransitionForm extends WorkflowTypeTransitionFormBase {
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$transition = $form_state->get('transition');
$configuration = $this->workflowType->getConfiguration();
$form['extra'] = [
'#type' => 'textfield',
'#title' => $this->t('Extra'),
'#description' => $this->t('Extra information added to transition'),
'#default_value' => $transition && isset($configuration['transitions'][$transition->id()]['extra']) ? $configuration['transitions'][$transition->id()]['extra'] : '',
];
return $form;
}
}

View file

@ -2,14 +2,8 @@
namespace Drupal\workflow_type_test\Plugin\WorkflowType;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\workflows\Plugin\WorkflowTypeBase;
use Drupal\workflows\StateInterface;
use Drupal\workflows\TransitionInterface;
use Drupal\workflows\WorkflowInterface;
use Drupal\workflow_type_test\DecoratedState;
use Drupal\workflow_type_test\DecoratedTransition;
/**
* Test workflow type.
@ -17,68 +11,17 @@ use Drupal\workflow_type_test\DecoratedTransition;
* @WorkflowType(
* id = "workflow_type_complex_test",
* label = @Translation("Workflow Type Complex Test"),
* forms = {
* "configure" = "\Drupal\workflow_type_test\Form\ComplexTestTypeConfigureForm",
* "state" = "\Drupal\workflow_type_test\Form\ComplexTestTypeStateForm",
* "transition" = "\Drupal\workflow_type_test\Form\ComplexTestTypeTransitionForm",
* }
* )
*/
class ComplexTestType extends WorkflowTypeBase {
use StringTranslationTrait;
/**
* {@inheritDoc}
*/
public function decorateState(StateInterface $state) {
if (isset($this->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;
}
/**
* {@inheritdoc}
*/
@ -88,4 +31,13 @@ class ComplexTestType extends WorkflowTypeBase {
return TRUE;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return parent::defaultConfiguration() + [
'example_setting' => '',
];
}
}

View file

@ -0,0 +1,98 @@
<?php
namespace Drupal\workflow_type_test\Plugin\WorkflowType;
use Drupal\workflows\Plugin\WorkflowTypeBase;
use Drupal\workflows\State;
/**
* Test workflow type.
*
* @WorkflowType(
* id = "predefined_states_workflow_test_type",
* label = @Translation("Predefined States Workflow Test Type"),
* required_states = {
* "pay_blinds",
* "bet",
* "raise",
* "fold",
* }
* )
*/
class PredefinedStatesWorkflowTestType extends WorkflowTypeBase {
/**
* {@inheritdoc}
*/
public function getStates($state_ids = NULL) {
return array_filter([
'pay_blinds' => new State($this, 'pay_blinds', 'Pay Blinds'),
'bet' => new State($this, 'bet', 'Bet'),
'raise' => new State($this, 'raise', 'Raise'),
'fold' => new State($this, 'fold', 'Fold'),
], function ($state) use ($state_ids) {
return is_array($state_ids) ? in_array($state->id(), $state_ids) : TRUE;
});
}
/**
* {@inheritdoc}
*/
public function getState($state_id) {
$states = $this->getStates();
if (!isset($states[$state_id])) {
throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow.'");
}
return $states[$state_id];
}
/**
* {@inheritdoc}
*/
public function hasState($state_id) {
$states = $this->getStates();
return isset($states[$state_id]);
}
/**
* {@inheritdoc}
*/
public function addState($state_id, $label) {
// States cannot be added on this workflow.
return $this;
}
/**
* {@inheritdoc}
*/
public function setStateLabel($state_id, $label) {
// States cannot be altered on this workflow.
return $this;
}
/**
* {@inheritdoc}
*/
public function setStateWeight($state_id, $weight) {
// States cannot be altered on this workflow.
return $this;
}
/**
* {@inheritdoc}
*/
public function deleteState($state_id) {
// States cannot be deleted on this workflow.
return $this;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'transitions' => [],
];
}
}

View file

@ -4,7 +4,6 @@ namespace Drupal\workflow_type_test\Plugin\WorkflowType;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\workflows\Plugin\WorkflowTypeBase;
use Drupal\workflows\WorkflowInterface;
/**
* Test workflow type.
@ -22,24 +21,32 @@ 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 [];
return [
'states' => [
'fresh' => [
'label' => 'Fresh',
'weight' => 0,
],
'rotten' => [
'label' => 'Rotten',
'weight' => 1,
],
],
'transitions' => [
'rot' => [
'label' => 'Rot',
'to' => 'rotten',
'weight' => 0,
'from' => [
'fresh',
],
],
],
];
}
}

View file

@ -14,14 +14,6 @@ use Drupal\workflows\Plugin\WorkflowTypeBase;
*/
class TestType extends WorkflowTypeBase {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
// No configuration is stored for the test type.
return [];
}
/**
* {@inheritdoc}
*/

View file

@ -5,4 +5,4 @@ package: Testing
version: VERSION
core: 8.x
dependencies:
- workflows
- drupal:workflows

View file

@ -0,0 +1,28 @@
<?php
/**
* @file
* Module file for workflow_type_test.
*/
/**
* Implements hook_workflow_type_info_alter().
*/
function workflow_type_test_workflow_type_info_alter(&$definitions) {
// Allow tests to override the workflow type definitions.
$state = \Drupal::state();
if ($state->get('workflow_type_test.plugin_definitions') !== NULL) {
$definitions = $state->get('workflow_type_test.plugin_definitions');
}
}
/**
* Sets the type plugin definitions override and clear the cache.
*
* @param array $definitions
* Definitions to set.
*/
function workflow_type_test_set_definitions($definitions) {
\Drupal::state()->set('workflow_type_test.plugin_definitions', $definitions);
\Drupal::service('plugin.manager.workflows.type')->clearCachedDefinitions();
}

View file

@ -0,0 +1,30 @@
<?php
namespace Drupal\Tests\workflows\Functional\Hal;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\Tests\workflows\Functional\Rest\WorkflowResourceTestBase;
/**
* @group hal
*/
class WorkflowHalJsonAnonTest extends WorkflowResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['hal'];
/**
* {@inheritdoc}
*/
protected static $format = 'hal_json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/hal+json';
}

View file

@ -0,0 +1,24 @@
<?php
namespace Drupal\Tests\workflows\Functional\Hal;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group hal
*/
class WorkflowHalJsonBasicAuthTest extends WorkflowHalJsonAnonTest {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,19 @@
<?php
namespace Drupal\Tests\workflows\Functional\Hal;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group hal
*/
class WorkflowHalJsonCookieTest extends WorkflowHalJsonAnonTest {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -0,0 +1,24 @@
<?php
namespace Drupal\Tests\workflows\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class WorkflowJsonAnonTest extends WorkflowResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\Tests\workflows\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class WorkflowJsonBasicAuthTest extends WorkflowResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,29 @@
<?php
namespace Drupal\Tests\workflows\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group rest
*/
class WorkflowJsonCookieTest extends WorkflowResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -0,0 +1,107 @@
<?php
namespace Drupal\Tests\workflows\Functional\Rest;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
use Drupal\workflows\Entity\Workflow;
/**
* ResourceTestBase for Workflow entity.
*/
abstract class WorkflowResourceTestBase extends EntityResourceTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'workflows',
'workflow_type_test',
];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'workflow';
/**
* {@inheritdoc}
*/
protected static $patchProtectedFieldNames = [];
/**
* The Workflow entity.
*
* @var \Drupal\workflows\WorkflowInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
$this->grantPermissionsToTestedRole(['administer workflows']);
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
$workflow = Workflow::create([
'id' => 'rest_workflow',
'label' => 'REST Worklow',
'type' => 'workflow_type_complex_test',
]);
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published');
$configuration = $workflow->getTypePlugin()->getConfiguration();
$configuration['example_setting'] = 'foo';
$configuration['states']['draft']['extra'] = 'bar';
$workflow->getTypePlugin()->setConfiguration($configuration);
$workflow->save();
return $workflow;
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
return [
'dependencies' => [
'module' => [
'workflow_type_test',
],
],
'id' => 'rest_workflow',
'label' => 'REST Worklow',
'langcode' => 'en',
'status' => TRUE,
'type' => 'workflow_type_complex_test',
'type_settings' => [
'states' => [
'draft' => [
'extra' => 'bar',
'label' => 'Draft',
'weight' => 0,
],
'published' => [
'label' => 'Published',
'weight' => 1,
],
],
'transitions' => [],
'example_setting' => 'foo',
],
'uuid' => $this->entity->uuid(),
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
// @todo Update in https://www.drupal.org/node/2300677.
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Drupal\Tests\workflows\Functional\Rest;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class WorkflowXmlAnonTest extends WorkflowResourceTestBase {
use AnonResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
}

View file

@ -0,0 +1,36 @@
<?php
namespace Drupal\Tests\workflows\Functional\Rest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class WorkflowXmlBasicAuthTest extends WorkflowResourceTestBase {
use BasicAuthResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,31 @@
<?php
namespace Drupal\Tests\workflows\Functional\Rest;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\XmlEntityNormalizationQuirksTrait;
/**
* @group rest
*/
class WorkflowXmlCookieTest extends WorkflowResourceTestBase {
use CookieResourceTestTrait;
use XmlEntityNormalizationQuirksTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'xml';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'text/xml; charset=UTF-8';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -38,6 +38,7 @@ class WorkflowUiNoTypeTest extends BrowserTestBase {
$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()->linkExists('Content Moderation');
$this->assertSession()->pageTextNotContains('Add workflow');
$this->container->get('module_installer')->install(['workflow_type_test']);
@ -48,7 +49,7 @@ class WorkflowUiNoTypeTest extends BrowserTestBase {
$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.');
$this->assertSession()->pageTextContains('There are no workflows yet.');
}
}

View file

@ -36,10 +36,11 @@ class WorkflowUiTest extends BrowserTestBase {
// Create a minimal workflow for testing.
$workflow = Workflow::create(['id' => 'test', 'type' => 'workflow_type_test']);
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published')
->addTransition('publish', 'Publish', ['draft', 'published'], 'published')
->save();
->addTransition('publish', 'Publish', ['draft', 'published'], 'published');
$workflow->save();
$paths = [
'admin/config/workflow/workflows',
@ -79,6 +80,31 @@ class WorkflowUiTest extends BrowserTestBase {
$this->assertSession()->statusCodeEquals(403);
}
/**
* Test the machine name validation of the state add form.
*/
public function testStateMachineNameValidation() {
Workflow::create([
'id' => 'test_workflow',
'type' => 'workflow_type_test',
])->save();
$this->drupalLogin($this->createUser(['administer workflows']));
$this->drupalPostForm('admin/config/workflow/workflows/manage/test_workflow/add_state', [
'label' => 'Test State',
'id' => 'Invalid ID',
], 'Save');
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->pageTextContains('The machine-readable name must contain only lowercase letters, numbers, and underscores.');
$this->drupalPostForm('admin/config/workflow/workflows/manage/test_workflow/add_transition', [
'label' => 'Test Transition',
'id' => 'Invalid ID',
], 'Save');
$this->assertSession()->pageTextContains('The machine-readable name must contain only lowercase letters, numbers, and underscores.');
}
/**
* Tests the creation of a workflow through the UI.
*/
@ -90,7 +116,7 @@ class WorkflowUiTest extends BrowserTestBase {
$this->assertSession()->linkByHrefExists('admin/config/workflow/workflows');
$this->clickLink('Workflows');
$this->assertSession()->pageTextContains('Workflows');
$this->assertSession()->pageTextContains('There is no Workflow yet.');
$this->assertSession()->pageTextContains('There are no workflows yet.');
$this->clickLink('Add workflow');
$this->submitForm(['label' => 'Test', 'id' => 'test', 'workflow_type' => 'workflow_type_test'], 'Save');
$this->assertSession()->pageTextContains('Created the Test Workflow.');
@ -102,26 +128,28 @@ class WorkflowUiTest extends BrowserTestBase {
$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->assertFalse($workflow->getTypePlugin()->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->assertFalse($workflow->getTypePlugin()->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->assertTrue($workflow->getTypePlugin()->getState('draft')->canTransitionTo('published'), 'Can transition from draft to published');
$this->clickLink('Add a new transition');
$this->assertCount(2, $this->cssSelect('input[name="to"][type="radio"]'));
$this->assertCount(0, $this->cssSelect('input[name="to"][checked="checked"][type="radio"]'));
$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');
$this->assertTrue($workflow->getTypePlugin()->getState('draft')->canTransitionTo('draft'), 'Can transition from draft to draft');
// The fist state to edit on the page should be published.
$this->clickLink('Edit');
@ -135,7 +163,7 @@ class WorkflowUiTest extends BrowserTestBase {
$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');
$this->assertTrue($workflow->getTypePlugin()->getState('published')->canTransitionTo('draft'), 'Can transition from published to draft');
// Try creating a duplicate transition.
$this->clickLink('Add a new transition');
@ -155,12 +183,12 @@ class WorkflowUiTest extends BrowserTestBase {
// Delete the transition.
$workflow = $workflow_storage->loadUnchanged('test');
$this->assertTrue($workflow->hasTransitionFromStateToState('published', 'published'), 'Can transition from published to published');
$this->assertTrue($workflow->getTypePlugin()->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');
$this->assertFalse($workflow->getTypePlugin()->hasTransitionFromStateToState('published', 'published'), 'Cannot transition from published to published');
// Try creating a duplicate state.
$this->drupalGet('admin/config/workflow/workflows/manage/test');
@ -170,26 +198,24 @@ class WorkflowUiTest extends BrowserTestBase {
// Ensure that weight changes the state ordering.
$workflow = $workflow_storage->loadUnchanged('test');
$this->assertEquals('published', $workflow->getTypePlugin()->getInitialState($workflow)->id());
$this->assertEquals('published', $workflow->getTypePlugin()->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->getTypePlugin()->getInitialState($workflow)->id());
$this->assertEquals('draft', $workflow->getTypePlugin()->getInitialState()->id());
// This will take us to the list of workflows, so we need to edit the
// workflow again.
$this->clickLink('Edit');
// Verify that we are still on the workflow edit page.
$this->assertSession()->addressEquals('admin/config/workflow/workflows/manage/test');
// Ensure that weight changes the transition ordering.
$this->assertEquals(['publish', 'create_new_draft'], array_keys($workflow->getTransitions()));
$this->assertEquals(['publish', 'create_new_draft'], array_keys($workflow->getTypePlugin()->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->assertEquals(['create_new_draft', 'publish'], array_keys($workflow->getTypePlugin()->getTransitions()));
// This will take us to the list of workflows, so we need to edit the
// workflow again.
$this->clickLink('Edit');
// Verify that we are still on the workflow edit page.
$this->assertSession()->addressEquals('admin/config/workflow/workflows/manage/test');
// Ensure that a delete link for the published state exists before deleting
// the draft state.
@ -224,8 +250,8 @@ class WorkflowUiTest extends BrowserTestBase {
$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');
$this->assertFalse($workflow->getTypePlugin()->hasState('draft'), 'Draft state deleted');
$this->assertTrue($workflow->getTypePlugin()->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.
@ -234,69 +260,157 @@ class WorkflowUiTest extends BrowserTestBase {
$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->assertSession()->pageTextContains('There are no workflows 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.
// Ensure that workflow types with default configuration 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->assertTrue($workflow->getTypePlugin()->hasState('fresh'), 'The workflow has the "fresh" state');
$this->assertTrue($workflow->getTypePlugin()->hasState('rotten'), 'The workflow has the "rotten" state');
$this->assertTrue($workflow->getTypePlugin()->hasTransition('rot'), 'The workflow has the "rot" transition');
$this->assertSession()->pageTextContains('Fresh');
$this->assertSession()->pageTextContains('Rotten');
}
/**
* Tests that workflow types can add form fields to states and transitions.
* Test the workflow configuration form.
*/
public function testWorkflowDecoration() {
// Create a minimal workflow for testing.
$workflow = Workflow::create(['id' => 'test', 'type' => 'workflow_type_complex_test']);
public function testWorkflowConfigurationForm() {
$workflow = Workflow::create(['id' => 'test', 'type' => 'workflow_type_complex_test', 'label' => 'Test']);
$workflow
->getTypePlugin()
->addState('published', 'Published')
->addTransition('publish', 'Publish', ['published'], 'published')
->save();
$this->assertEquals('', $workflow->getState('published')->getExtra());
$this->assertEquals('', $workflow->getTransition('publish')->getExtra());
->addTransition('publish', 'Publish', ['published'], 'published');
$workflow->save();
$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');
// Add additional information to the workflow via the configuration form.
$this->drupalGet('admin/config/workflow/workflows/manage/test');
$this->assertSession()->pageTextContains('Example global workflow setting');
$this->submitForm(['type_settings[example_setting]' => 'Extra global settings'], '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());
$this->assertEquals('Extra global settings', $workflow->getTypePlugin()->getConfiguration()['example_setting']);
}
// 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');
/**
* Test a workflow, state, and transition can have a numeric ID and label.
*/
public function testNumericIds() {
$this->drupalLogin($this->createUser(['administer workflows']));
$this->drupalGet('admin/config/workflow/workflows');
$this->clickLink('Add workflow');
$this->submitForm(['label' => 123, 'id' => 123, 'workflow_type' => 'workflow_type_complex_test'], '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');
$this->assertSession()->addressEquals('admin/config/workflow/workflows/manage/123/add_state');
$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());
$this->submitForm(['label' => 456, 'id' => 456], 'Save');
$this->assertSession()->pageTextContains('Created 456 state.');
$this->clickLink('Add a new state');
$this->submitForm(['label' => 789, 'id' => 789], 'Save');
$this->assertSession()->pageTextContains('Created 789 state.');
$this->clickLink('Add a new transition');
$this->submitForm(['id' => 101112, 'label' => 101112, 'from[456]' => 456, 'to' => 789], 'Save');
$this->assertSession()->pageTextContains('Created 101112 transition.');
$workflow = $this->container->get('entity_type.manager')->getStorage('workflow')->loadUnchanged(123);
$this->assertEquals(123, $workflow->id());
$this->assertEquals(456, $workflow->getTypePlugin()->getState(456)->id());
$this->assertEquals(101112, $workflow->getTypePlugin()->getTransition(101112)->id());
$this->assertEquals(789, $workflow->getTypePlugin()->getTransition(101112)->to()->id());
}
/**
* Test the sorting of states and transitions by weight and label.
*/
public function testSorting() {
$workflow = Workflow::create(['id' => 'test', 'type' => 'workflow_type_complex_test', 'label' => 'Test']);
$workflow
->getTypePlugin()
->setConfiguration([
'states' => [
'twoa' => [
'label' => 'twoa',
'weight' => 2,
],
'three' => [
'label' => 'three',
'weight' => 3,
],
'twob' => [
'label' => 'twob',
'weight' => 2,
],
'one' => [
'label' => 'one',
'weight' => 1,
],
],
'transitions' => [
'three' => [
'label' => 'three',
'from' => ['three'],
'to' => 'three',
'weight' => 3,
],
'twoa' => [
'label' => 'twoa',
'from' => ['twoa'],
'to' => 'twoa',
'weight' => 2,
],
'one' => [
'label' => 'one',
'from' => ['one'],
'to' => 'one',
'weight' => 1,
],
'twob' => [
'label' => 'twob',
'from' => ['twob'],
'to' => 'twob',
'weight' => 2,
],
],
]);
$workflow->save();
$this->drupalLogin($this->createUser(['administer workflows']));
$this->drupalGet('admin/config/workflow/workflows/manage/test');
$expected_states = ['one', 'twoa', 'twob', 'three'];
$elements = $this->xpath('//details[@id="edit-states-container"]//table/tbody/tr');
foreach ($elements as $key => $element) {
$this->assertEquals($expected_states[$key], $element->find('xpath', 'td')->getText());
}
$expected_transitions = ['one', 'twoa', 'twob', 'three'];
$elements = $this->xpath('//details[@id="edit-transitions-container"]//table/tbody/tr');
foreach ($elements as $key => $element) {
$this->assertEquals($expected_transitions[$key], $element->find('xpath', 'td')->getText());
}
// Ensure that there are enough weights to satisfy the potential number of
// states and transitions.
$this->assertSession()
->selectExists('states[three][weight]')
->selectOption('2');
$this->assertSession()
->selectExists('states[three][weight]')
->selectOption('-2');
$this->assertSession()
->selectExists('transitions[three][weight]')
->selectOption('2');
$this->assertSession()
->selectExists('transitions[three][weight]')
->selectOption('-2');
}
}

View file

@ -4,13 +4,11 @@ namespace Drupal\Tests\workflows\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\workflows\Entity\Workflow;
use Drupal\workflow_type_test\DecoratedState;
use Drupal\workflow_type_test\DecoratedTransition;
/**
* Workflow entity tests that require modules or storage.
*
* @coversDefaultClass \Drupal\workflows\Entity\Workflow
* @coversDefaultClass \Drupal\workflow_type_test\Plugin\WorkflowType\ComplexTestType
*
* @group workflows
*/
@ -22,22 +20,7 @@ class ComplexWorkflowTypeTest extends KernelTestBase {
public static $modules = ['workflows', 'workflow_type_test'];
/**
* Tests a workflow type that decorates transitions and states.
*
* @covers ::getState
* @covers ::getTransition
*/
public function testComplexType() {
$workflow = new Workflow(['id' => '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
* @covers \Drupal\workflows\Entity\Workflow::loadMultipleByType
*/
public function testLoadMultipleByType() {
$workflow1 = new Workflow(['id' => 'test1', 'type' => 'workflow_type_complex_test'], 'workflow');

View file

@ -0,0 +1,52 @@
<?php
namespace Drupal\Tests\workflows\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\workflows\Entity\Workflow;
/**
* Test a predefined workflow based on something other than configuration.
*
* @group workflows
*/
class PredefinedWorkflowTypeTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['workflows', 'workflow_type_test'];
/**
* Test a predefined workflow type.
*/
public function testPredefinedWorkflowType() {
$workflow = Workflow::create([
'id' => 'aces',
'label' => 'Aces Workflow',
'type' => 'predefined_states_workflow_test_type',
'transitions' => [
'bet' => [
'label' => 'Bet',
'from' => [
'pay_blinds',
],
'to' => 'bet',
],
'raise' => [
'label' => 'Raise',
'from' => [
'pay_blinds',
],
'to' => 'raise',
],
],
]);
$workflow->save();
// No states configuration is stored for this workflow.
$configuration = $workflow->getTypePlugin()->getConfiguration();
$this->assertFalse(isset($configuration['states']));
}
}

View file

@ -22,7 +22,6 @@ class RequiredStatesTest extends KernelTestBase {
/**
* @covers ::getRequiredStates
* @covers ::initializeWorkflow
* @covers ::__construct
*/
public function testGetRequiredStates() {
@ -30,15 +29,14 @@ class RequiredStatesTest extends KernelTestBase {
'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'));
$this->assertTrue($workflow->getTypePlugin()->hasState('rotten'));
$this->assertTrue($workflow->getTypePlugin()->hasState('fresh'));
$this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('fresh', 'rotten'));
}
/**
@ -49,11 +47,11 @@ class RequiredStatesTest extends KernelTestBase {
'id' => 'test',
'type' => 'workflow_type_required_state_test',
], 'workflow');
$workflow = $workflow->getTypePlugin()->initializeWorkflow($workflow);
$workflow->save();
// Ensure that required states can't be deleted.
$this->setExpectedException(RequiredStateMissingException::class, "Required State Type Test' requires states with the ID 'fresh' in workflow 'test'");
$workflow->deleteState('fresh')->save();
$workflow->getTypePlugin()->deleteState('fresh');
$workflow->save();
}
/**
@ -63,6 +61,9 @@ class RequiredStatesTest extends KernelTestBase {
$workflow = new Workflow([
'id' => 'test',
'type' => 'workflow_type_required_state_test',
'type_settings' => [
'states' => [],
],
], 'workflow');
$this->setExpectedException(RequiredStateMissingException::class, "Required State Type Test' requires states with the ID 'fresh', 'rotten' in workflow 'test'");
$workflow->save();
@ -76,45 +77,49 @@ class RequiredStatesTest extends KernelTestBase {
'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());
$this->assertEquals('Fresh', $workflow->getTypePlugin()->getState('fresh')->label());
$workflow
->setStateLabel('fresh', 'Fresher')
->save();
$this->assertEquals('Fresher', $workflow->getState('fresh')->label());
->getTypePlugin()
->setStateLabel('fresh', 'Fresher');
$workflow->save();
$this->assertEquals('Fresher', $workflow->getTypePlugin()->getState('fresh')->label());
// Ensure transitions can be altered.
$workflow
->getTypePlugin()
->addState('cooked', 'Cooked')
->setTransitionFromStates('rot', ['fresh', 'cooked'])
->save();
$this->assertTrue($workflow->hasTransitionFromStateToState('fresh', 'rotten'));
$this->assertTrue($workflow->hasTransitionFromStateToState('cooked', 'rotten'));
->setTransitionFromStates('rot', ['fresh', 'cooked']);
$workflow->save();
$this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('fresh', 'rotten'));
$this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('cooked', 'rotten'));
$workflow
->setTransitionFromStates('rot', ['cooked'])
->save();
$this->assertFalse($workflow->hasTransitionFromStateToState('fresh', 'rotten'));
$this->assertTrue($workflow->hasTransitionFromStateToState('cooked', 'rotten'));
->getTypePlugin()
->setTransitionFromStates('rot', ['cooked']);
$workflow->save();
$this->assertFalse($workflow->getTypePlugin()->hasTransitionFromStateToState('fresh', 'rotten'));
$this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('cooked', 'rotten'));
// Ensure the default configuration does not cause ordering issues.
$workflow->addTransition('cook', 'Cook', ['fresh'], 'cooked')->save();
$workflow->getTypePlugin()->addTransition('cook', 'Cook', ['fresh'], 'cooked');
$workflow->save();
$this->assertSame([
'cooked',
'fresh',
'rotten',
], array_keys($workflow->get('states')));
], array_keys($workflow->getTypePlugin()->getConfiguration()['states']));
$this->assertSame([
'cook',
'rot',
], array_keys($workflow->get('transitions')));
], array_keys($workflow->getTypePlugin()->getConfiguration()['transitions']));
// Ensure that transitions can be deleted.
$workflow->deleteTransition('rot')->save();
$this->assertFalse($workflow->hasTransition('rot'));
$workflow->getTypePlugin()->deleteTransition('rot');
$workflow->save();
$this->assertFalse($workflow->getTypePlugin()->hasTransition('rot'));
}
}

View file

@ -0,0 +1,218 @@
<?php
namespace Drupal\Tests\workflows\Kernel;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\Context\CacheContextsManager;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\KernelTests\KernelTestBase;
use Drupal\simpletest\UserCreationTrait;
use Drupal\workflows\Entity\Workflow;
/**
* @coversDefaultClass \Drupal\workflows\WorkflowAccessControlHandler
* @group workflows
*/
class WorkflowAccessControlHandlerTest extends KernelTestBase {
use UserCreationTrait;
/**
* {@inheritdoc}
*/
public static $modules = [
'workflows',
'workflow_type_test',
'system',
'user',
];
/**
* The workflow access control handler.
*
* @var \Drupal\workflows\WorkflowAccessControlHandler
*/
protected $accessControlHandler;
/**
* A test admin user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $adminUser;
/**
* A non-privileged user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $user;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('workflow');
$this->installEntitySchema('user');
$this->installSchema('system', ['sequences']);
$this->accessControlHandler = $this->container->get('entity_type.manager')->getAccessControlHandler('workflow');
// Create and discard user 1, which is special and bypasses all access
// checking.
$this->createUser([]);
$this->user = $this->createUser([]);
$this->adminUser = $this->createUser(['administer workflows']);
}
/**
* @covers ::checkCreateAccess
*/
public function testCheckCreateAccess() {
// A user must have the correct permission to create a workflow.
$this->assertEquals(
AccessResult::neutral()
->addCacheContexts(['user.permissions'])
->setReason("The 'administer workflows' permission is required.")
->addCacheTags(['workflow_type_plugins']),
$this->accessControlHandler->createAccess(NULL, $this->user, [], TRUE)
);
$this->assertEquals(
AccessResult::allowed()
->addCacheContexts(['user.permissions'])
->addCacheTags(['workflow_type_plugins']),
$this->accessControlHandler->createAccess(NULL, $this->adminUser, [], TRUE)
);
// Remove all plugin types and ensure not even the admin user is allowed to
// create a workflow.
workflow_type_test_set_definitions([]);
$this->accessControlHandler->resetCache();
$this->assertEquals(
AccessResult::neutral()
->addCacheContexts(['user.permissions'])
->addCacheTags(['workflow_type_plugins']),
$this->accessControlHandler->createAccess(NULL, $this->adminUser, [], TRUE)
);
}
/**
* @covers ::checkAccess
* @dataProvider checkAccessProvider
*/
public function testCheckAccess($user, $operation, $result, $states_to_create = []) {
$workflow = Workflow::create([
'type' => 'workflow_type_test',
'id' => 'test_workflow',
]);
$workflow->save();
$workflow_type = $workflow->getTypePlugin();
foreach ($states_to_create as $state_id => $is_required) {
$workflow_type->addState($state_id, $this->randomString());
}
\Drupal::state()->set('workflow_type_test.required_states', array_filter($states_to_create));
$this->assertEquals($result, $this->accessControlHandler->access($workflow, $operation, $this->{$user}, TRUE));
}
/**
* Data provider for ::testCheckAccess.
*
* @return array
*/
public function checkAccessProvider() {
$container = new ContainerBuilder();
$cache_contexts_manager = $this->prophesize(CacheContextsManager::class);
$cache_contexts_manager->assertValidTokens()->willReturn(TRUE);
$cache_contexts_manager->reveal();
$container->set('cache_contexts_manager', $cache_contexts_manager);
\Drupal::setContainer($container);
return [
'Admin view' => [
'adminUser',
'view',
AccessResult::allowed()->addCacheContexts(['user.permissions']),
],
'Admin update' => [
'adminUser',
'update',
AccessResult::allowed()->addCacheContexts(['user.permissions']),
],
'Admin delete' => [
'adminUser',
'delete',
AccessResult::allowed()->addCacheContexts(['user.permissions']),
],
'Admin delete only state' => [
'adminUser',
'delete-state:foo',
AccessResult::neutral()->addCacheTags(['config:workflows.workflow.test_workflow']),
['foo' => FALSE],
],
'Admin delete one of two states' => [
'adminUser',
'delete-state:foo',
AccessResult::allowed()
->addCacheTags(['config:workflows.workflow.test_workflow'])
->addCacheContexts(['user.permissions']),
['foo' => FALSE, 'bar' => FALSE],
],
'Admin delete required state when there are >1 states' => [
'adminUser',
'delete-state:foo',
AccessResult::allowed()
->addCacheTags(['config:workflows.workflow.test_workflow'])
->addCacheContexts(['user.permissions']),
['foo' => TRUE, 'bar' => FALSE],
],
'User view' => [
'user',
'view',
AccessResult::neutral()
->addCacheContexts(['user.permissions'])
->setReason("The 'administer workflows' permission is required."),
],
'User update' => [
'user',
'update',
AccessResult::neutral()
->addCacheContexts(['user.permissions'])
->setReason("The 'administer workflows' permission is required."),
],
'User delete' => [
'user',
'delete',
AccessResult::neutral()
->addCacheContexts(['user.permissions'])
->setReason("The 'administer workflows' permission is required."),
],
'User delete only state' => [
'user',
'delete-state:foo',
AccessResult::neutral()->addCacheTags(['config:workflows.workflow.test_workflow']),
['foo' => FALSE],
],
'User delete one of two states' => [
'user',
'delete-state:foo',
AccessResult::neutral()
->addCacheTags(['config:workflows.workflow.test_workflow'])
->addCacheContexts(['user.permissions'])
->setReason("The 'administer workflows' permission is required."),
['foo' => FALSE, 'bar' => FALSE],
],
'User delete required state when there are >1 states' => [
'user',
'delete-state:foo',
AccessResult::neutral()
->addCacheTags(['config:workflows.workflow.test_workflow'])
->addCacheContexts(['user.permissions'])
->setReason("The 'administer workflows' permission is required."),
['foo' => TRUE, 'bar' => FALSE],
],
];
}
}

View file

@ -33,6 +33,7 @@ class WorkflowDependenciesTest extends KernelTestBase {
// \Drupal\workflows\Entity\Workflow::onDependencyRemoval() works as
// expected.
\Drupal::service('module_installer')->uninstall(['node', 'workflow_third_party_settings_test']);
/** @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = \Drupal::entityTypeManager()->getStorage('workflow')->loadUnchanged($workflow->id());
$this->assertSame(['workflow_type_test'], $workflow->getDependencies()['module']);
}

View file

@ -2,14 +2,10 @@
namespace Drupal\Tests\workflows\Unit;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Tests\UnitTestCase;
use Drupal\workflows\Entity\Workflow;
use Drupal\workflow_type_test\Plugin\WorkflowType\TestType;
use Drupal\workflows\State;
use Drupal\workflows\WorkflowInterface;
use Drupal\workflows\WorkflowTypeInterface;
use Drupal\workflows\WorkflowTypeManager;
use Prophecy\Argument;
/**
* @coversDefaultClass \Drupal\workflows\State
@ -18,25 +14,6 @@ use Prophecy\Argument;
*/
class StateTest extends UnitTestCase {
/**
* Sets up the Workflow Type manager so that workflow entities can be used.
*/
protected function setUp() {
parent::setUp();
// 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_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
@ -45,7 +22,7 @@ class StateTest extends UnitTestCase {
*/
public function testGetters() {
$state = new State(
$this->prophesize(WorkflowInterface::class)->reveal(),
$this->prophesize(WorkflowTypeInterface::class)->reveal(),
'draft',
'Draft',
3
@ -59,16 +36,16 @@ class StateTest extends UnitTestCase {
* @covers ::canTransitionTo
*/
public function testCanTransitionTo() {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow
$workflow_type = new TestType([], '', []);
$workflow_type
->addState('draft', 'Draft')
->addState('published', 'Published')
->addTransition('publish', 'Publish', ['draft'], 'published');
$state = $workflow->getState('draft');
$state = $workflow_type->getState('draft');
$this->assertTrue($state->canTransitionTo('published'));
$this->assertFalse($state->canTransitionTo('some_other_state'));
$workflow->deleteTransition('publish');
$workflow_type->deleteTransition('publish');
$this->assertFalse($state->canTransitionTo('published'));
}
@ -76,12 +53,12 @@ class StateTest extends UnitTestCase {
* @covers ::getTransitionTo
*/
public function testGetTransitionTo() {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow
$workflow_type = new TestType([], '', []);
$workflow_type
->addState('draft', 'Draft')
->addState('published', 'Published')
->addTransition('publish', 'Publish', ['draft'], 'published');
$state = $workflow->getState('draft');
$state = $workflow_type->getState('draft');
$transition = $state->getTransitionTo('published');
$this->assertEquals('Publish', $transition->label());
}
@ -91,9 +68,9 @@ class StateTest extends UnitTestCase {
*/
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');
$workflow_type = new TestType([], '', []);
$workflow_type->addState('draft', 'Draft');
$state = $workflow_type->getState('draft');
$state->getTransitionTo('published');
}
@ -101,15 +78,15 @@ class StateTest extends UnitTestCase {
* @covers ::getTransitions
*/
public function testGetTransitions() {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow
$workflow_type = new TestType([], '', []);
$workflow_type
->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');
$state = $workflow_type->getState('draft');
$transitions = $state->getTransitions();
$this->assertCount(2, $transitions);
$this->assertEquals('Create new draft', $transitions['create_new_draft']->label());
@ -120,10 +97,10 @@ class StateTest extends UnitTestCase {
* @covers ::labelCallback
*/
public function testLabelCallback() {
$workflow = $this->prophesize(WorkflowInterface::class)->reveal();
$workflow_type = $this->prophesize(WorkflowTypeInterface::class)->reveal();
$states = [
new State($workflow, 'draft', 'Draft'),
new State($workflow, 'published', 'Published'),
new State($workflow_type, 'draft', 'Draft'),
new State($workflow_type, 'published', 'Published'),
];
$this->assertEquals(['Draft', 'Published'], array_map([State::class, 'labelCallback'], $states));
}

View file

@ -2,14 +2,10 @@
namespace Drupal\Tests\workflows\Unit;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Tests\UnitTestCase;
use Drupal\workflows\Entity\Workflow;
use Drupal\workflow_type_test\Plugin\WorkflowType\TestType;
use Drupal\workflows\Transition;
use Drupal\workflows\WorkflowInterface;
use Drupal\workflows\WorkflowTypeInterface;
use Drupal\workflows\WorkflowTypeManager;
use Prophecy\Argument;
/**
* @coversDefaultClass \Drupal\workflows\Transition
@ -18,23 +14,6 @@ use Prophecy\Argument;
*/
class TransitionTest extends UnitTestCase {
/**
* Sets up the Workflow Type manager so that workflow entities can be used.
*/
protected function setUp() {
parent::setUp();
// 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('test_type', Argument::any())->willReturn($workflow_type->reveal());
$container->set('plugin.manager.workflows.type', $workflow_manager->reveal());
\Drupal::setContainer($container);
}
/**
* @covers ::__construct
* @covers ::id
@ -42,7 +21,7 @@ class TransitionTest extends UnitTestCase {
*/
public function testGetters() {
$state = new Transition(
$this->prophesize(WorkflowInterface::class)->reveal(),
$this->prophesize(WorkflowTypeInterface::class)->reveal(),
'draft_published',
'Publish',
['draft'],
@ -57,7 +36,7 @@ class TransitionTest extends UnitTestCase {
* @covers ::to
*/
public function testFromAndTo() {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow = new TestType([], '', []);
$workflow
->addState('draft', 'Draft')
->addState('published', 'Published')

View file

@ -4,15 +4,15 @@ namespace Drupal\Tests\workflows\Unit;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Tests\UnitTestCase;
use Drupal\workflow_type_test\Plugin\WorkflowType\TestType;
use Drupal\workflows\Entity\Workflow;
use Drupal\workflows\State;
use Drupal\workflows\Transition;
use Drupal\workflows\WorkflowTypeInterface;
use Drupal\workflows\WorkflowTypeManager;
use Prophecy\Argument;
/**
* @coversDefaultClass \Drupal\workflows\Entity\Workflow
* @coversDefaultClass \Drupal\workflows\Plugin\WorkflowTypeBase
*
* @group workflows
*/
@ -26,11 +26,8 @@ class WorkflowTest extends UnitTestCase {
// 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('test_type', Argument::any())->willReturn($workflow_type->reveal());
$workflow_manager->createInstance('test_type', Argument::any())->willReturn(new TestType([], '', []));
$container->set('plugin.manager.workflows.type', $workflow_manager->reveal());
\Drupal::setContainer($container);
}
@ -41,30 +38,30 @@ class WorkflowTest extends UnitTestCase {
*/
public function testAddAndHasState() {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$this->assertFalse($workflow->hasState('draft'));
$this->assertFalse($workflow->getTypePlugin()->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());
$workflow->getTypePlugin()->addState('draft', 'Draft');
$this->assertTrue($workflow->getTypePlugin()->hasState('draft'));
$this->assertFalse($workflow->getTypePlugin()->hasState('published'));
$this->assertEquals(0, $workflow->getTypePlugin()->getState('draft')->weight());
// Adding a state does not set up a transition to itself.
$this->assertFalse($workflow->hasTransitionFromStateToState('draft', 'draft'));
$this->assertFalse($workflow->getTypePlugin()->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());
$workflow->getTypePlugin()->addState('published', 'Published');
$this->assertEquals(1, $workflow->getTypePlugin()->getState('published')->weight());
}
/**
* @covers ::addState
*/
public function testAddStateException() {
$this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' already exists in workflow 'test'");
$this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' already exists in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->addState('draft', 'Draft');
$workflow->addState('draft', 'Draft');
$workflow->getTypePlugin()->addState('draft', 'Draft');
$workflow->getTypePlugin()->addState('draft', 'Draft');
}
/**
@ -73,7 +70,7 @@ class WorkflowTest extends UnitTestCase {
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');
$workflow->getTypePlugin()->addState('draft-draft', 'Draft');
}
/**
@ -83,10 +80,11 @@ class WorkflowTest extends UnitTestCase {
$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([])));
$this->assertArrayEquals([], array_keys($workflow->getTypePlugin()->getStates()));
$this->assertArrayEquals([], array_keys($workflow->getTypePlugin()->getStates([])));
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published')
->addState('archived', 'Archived');
@ -96,36 +94,57 @@ class WorkflowTest extends UnitTestCase {
'archived',
'draft',
'published',
], array_keys($workflow->get('states')));
], array_keys($workflow->getTypePlugin()->getConfiguration()['states']));
// Ensure we're returning state objects.
$this->assertInstanceOf(State::class, $workflow->getStates()['draft']);
$this->assertInstanceOf(State::class, $workflow->getTypePlugin()->getStates()['draft']);
// Passing in no IDs returns all states.
$this->assertArrayEquals(['draft', 'published', 'archived'], array_keys($workflow->getStates()));
$this->assertArrayEquals(['draft', 'published', 'archived'], array_keys($workflow->getTypePlugin()->getStates()));
// The order of states is by weight.
$workflow->setStateWeight('published', -1);
$this->assertArrayEquals(['published', 'draft', 'archived'], array_keys($workflow->getStates()));
$workflow->getTypePlugin()->setStateWeight('published', -1);
$this->assertArrayEquals(['published', 'draft', 'archived'], array_keys($workflow->getTypePlugin()->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()));
$workflow->getTypePlugin()->setStateWeight('archived', 0);
$this->assertArrayEquals(['published', 'archived', 'draft'], array_keys($workflow->getTypePlugin()->getStates()));
// You can limit the states returned by passing in states IDs.
$this->assertArrayEquals(['archived', 'draft'], array_keys($workflow->getStates(['draft', 'archived'])));
$this->assertArrayEquals(['archived', 'draft'], array_keys($workflow->getTypePlugin()->getStates(['draft', 'archived'])));
// An empty array does not load all states.
$this->assertArrayEquals([], array_keys($workflow->getStates([])));
$this->assertArrayEquals([], array_keys($workflow->getTypePlugin()->getStates([])));
}
/**
* Test numeric IDs when added to a workflow.
*/
public function testNumericIdSorting() {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow_type = $workflow->getTypePlugin();
$workflow_type->addState('1', 'One');
$workflow_type->addState('2', 'Two');
$workflow_type->addState('3', 'ZZZ');
$workflow_type->addState('4', 'AAA');
$workflow_type->setStateWeight('1', 1);
$workflow_type->setStateWeight('2', 2);
$workflow_type->setStateWeight('3', 3);
$workflow_type->setStateWeight('4', 3);
// Ensure numeric states are correctly sorted by weight first, label second.
$this->assertEquals([1, 2, 4, 3], array_keys($workflow_type->getStates()));
}
/**
* @covers ::getStates
*/
public function testGetStatesException() {
$this->setExpectedException(\InvalidArgumentException::class, "The state 'state_that_does_not_exist' does not exist in workflow 'test'");
$this->setExpectedException(\InvalidArgumentException::class, "The state 'state_that_does_not_exist' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getStates(['state_that_does_not_exist']);
$workflow->getTypePlugin()->getStates(['state_that_does_not_exist']);
}
/**
@ -135,6 +154,7 @@ class WorkflowTest extends UnitTestCase {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
// By default states are ordered in the order added.
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published')
->addState('archived', 'Archived')
@ -142,27 +162,27 @@ class WorkflowTest extends UnitTestCase {
->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());
$this->assertInstanceOf(State::class, $workflow->getTypePlugin()->getState('draft'));
$this->assertEquals('archived', $workflow->getTypePlugin()->getState('archived')->id());
$this->assertEquals('Archived', $workflow->getTypePlugin()->getState('archived')->label());
$draft = $workflow->getState('draft');
$draft = $workflow->getTypePlugin()->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());
$this->assertEquals(1, $workflow->getTypePlugin()->getState('published')->weight());
$this->assertEquals(2, $workflow->getTypePlugin()->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'");
$this->setExpectedException(\InvalidArgumentException::class, "The state 'state_that_does_not_exist' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getState('state_that_does_not_exist');
$workflow->getTypePlugin()->getState('state_that_does_not_exist');
}
/**
@ -170,19 +190,19 @@ class WorkflowTest extends UnitTestCase {
*/
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());
$workflow->getTypePlugin()->addState('draft', 'Draft');
$this->assertEquals('Draft', $workflow->getTypePlugin()->getState('draft')->label());
$workflow->getTypePlugin()->setStateLabel('draft', 'Unpublished');
$this->assertEquals('Unpublished', $workflow->getTypePlugin()->getState('draft')->label());
}
/**
* @covers ::setStateLabel
*/
public function testSetStateLabelException() {
$this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' does not exist in workflow 'test'");
$this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->setStateLabel('draft', 'Draft');
$workflow->getTypePlugin()->setStateLabel('draft', 'Draft');
}
/**
@ -190,71 +210,70 @@ class WorkflowTest extends UnitTestCase {
*/
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());
$workflow->getTypePlugin()->addState('draft', 'Draft');
$this->assertEquals(0, $workflow->getTypePlugin()->getState('draft')->weight());
$workflow->getTypePlugin()->setStateWeight('draft', -10);
$this->assertEquals(-10, $workflow->getTypePlugin()->getState('draft')->weight());
}
/**
* @covers ::setStateWeight
*/
public function testSetStateWeightException() {
$this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' does not exist in workflow 'test'");
$this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->setStateWeight('draft', 10);
$workflow->getTypePlugin()->setStateWeight('draft', 10);
}
/**
* @covers ::setStateWeight
*/
public function testSetStateWeightNonNumericException() {
$this->setExpectedException(\InvalidArgumentException::class, "The weight 'foo' must be numeric for state 'Published'.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTypePlugin()->addState('published', 'Published');
$workflow->getTypePlugin()->setStateWeight('published', 'foo');
}
/**
* @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
$workflow_type = new TestType([], '', []);
$workflow_type
->addState('draft', 'Draft')
->addState('published', 'Published')
->addState('archived', 'Archived')
->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());
->addTransition('create_new_draft', 'Create new draft', ['draft', 'published'], 'draft')
->addTransition('archive', 'Archive', ['draft', 'published'], 'archived');
$this->assertCount(3, $workflow_type->getStates());
$this->assertCount(3, $workflow_type->getState('published')->getTransitions());
$workflow_type->deleteState('draft');
$this->assertFalse($workflow_type->hasState('draft'));
$this->assertCount(2, $workflow_type->getStates());
$this->assertCount(2, $workflow_type->getState('published')->getTransitions());
$workflow_type->deleteState('published');
$this->assertCount(0, $workflow_type->getTransitions());
}
/**
* @covers ::deleteState
*/
public function testDeleteStateException() {
$this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' does not exist in workflow 'test'");
$this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->deleteState('draft');
$workflow->getTypePlugin()->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");
$this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' can not be deleted from workflow as it is the only state");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->addState('draft', 'Draft');
$workflow->deleteState('draft');
$workflow->getTypePlugin()->addState('draft', 'Draft');
$workflow->getTypePlugin()->deleteState('draft');
}
/**
@ -266,29 +285,30 @@ class WorkflowTest extends UnitTestCase {
// By default states are ordered in the order added.
$workflow
->getTypePlugin()
->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'));
$this->assertFalse($workflow->getTypePlugin()->getState('draft')->canTransitionTo('published'));
$workflow->getTypePlugin()->addTransition('publish', 'Publish', ['draft'], 'published');
$this->assertTrue($workflow->getTypePlugin()->getState('draft')->canTransitionTo('published'));
$this->assertEquals(0, $workflow->getTypePlugin()->getTransition('publish')->weight());
$this->assertTrue($workflow->getTypePlugin()->hasTransition('publish'));
$this->assertFalse($workflow->getTypePlugin()->hasTransition('draft'));
$workflow->addTransition('save_publish', 'Save', ['published'], 'published');
$this->assertEquals(1, $workflow->getTransition('save_publish')->weight());
$workflow->getTypePlugin()->addTransition('save_publish', 'Save', ['published'], 'published');
$this->assertEquals(1, $workflow->getTypePlugin()->getTransition('save_publish')->weight());
}
/**
* @covers ::addTransition
*/
public function testAddTransitionDuplicateException() {
$this->setExpectedException(\InvalidArgumentException::class, "The transition 'publish' already exists in workflow 'test'");
$this->setExpectedException(\InvalidArgumentException::class, "The transition 'publish' already exists in workflow.");
$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');
$workflow->getTypePlugin()->addState('published', 'Published');
$workflow->getTypePlugin()->addTransition('publish', 'Publish', ['published'], 'published');
$workflow->getTypePlugin()->addTransition('publish', 'Publish', ['published'], 'published');
}
/**
@ -297,31 +317,32 @@ class WorkflowTest extends UnitTestCase {
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');
$workflow->getTypePlugin()->addState('published', 'Published');
$workflow->getTypePlugin()->addTransition('publish-publish', 'Publish', ['published'], 'published');
}
/**
* @covers ::addTransition
*/
public function testAddTransitionMissingFromException() {
$this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' does not exist in workflow 'test'");
$this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->addState('published', 'Published');
$workflow->addTransition('publish', 'Publish', ['draft'], 'published');
$workflow->getTypePlugin()->addState('published', 'Published');
$workflow->getTypePlugin()->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'");
$this->setExpectedException(\InvalidArgumentException::class, "The 'publish' transition already allows 'draft' to 'published' transitions in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published');
$workflow->addTransition('publish', 'Publish', ['draft', 'published'], 'published');
$workflow->addTransition('draft_to_published', 'Publish a draft', ['draft'], 'published');
$workflow->getTypePlugin()->addTransition('publish', 'Publish', ['draft', 'published'], 'published');
$workflow->getTypePlugin()->addTransition('draft_to_published', 'Publish a draft', ['draft'], 'published');
}
/**
@ -329,26 +350,26 @@ class WorkflowTest extends UnitTestCase {
*/
public function testAddTransitionConsistentAfterFromCatch() {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->addState('published', 'Published');
$workflow->getTypePlugin()->addState('published', 'Published');
try {
$workflow->addTransition('publish', 'Publish', ['draft'], 'published');
$workflow->getTypePlugin()->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'));
$this->assertFalse($workflow->getTypePlugin()->hasTransition('publish'));
}
/**
* @covers ::addTransition
*/
public function testAddTransitionMissingToException() {
$this->setExpectedException(\InvalidArgumentException::class, "The state 'published' does not exist in workflow 'test'");
$this->setExpectedException(\InvalidArgumentException::class, "The state 'published' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->addState('draft', 'Draft');
$workflow->addTransition('publish', 'Publish', ['draft'], 'published');
$workflow->getTypePlugin()->addState('draft', 'Draft');
$workflow->getTypePlugin()->addTransition('publish', 'Publish', ['draft'], 'published');
}
/**
@ -359,43 +380,43 @@ class WorkflowTest extends UnitTestCase {
$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([])));
$this->assertArrayEquals([], array_keys($workflow->getTypePlugin()->getTransitions()));
$this->assertArrayEquals([], array_keys($workflow->getTypePlugin()->getTransitions([])));
// By default states are ordered in the order added.
$workflow
->getTypePlugin()
->addState('a', 'A')
->addState('b', 'B')
->addTransition('a_b', 'A to B', ['a'], 'b')
->addTransition('a_a', 'A to A', ['a'], 'a');
// Transitions are stored in alphabetical key order in configuration.
$this->assertArrayEquals(['a_a', 'a_b'], array_keys($workflow->get('transitions')));
$this->assertArrayEquals(['a_a', 'a_b'], array_keys($workflow->getTypePlugin()->getConfiguration()['transitions']));
// Ensure we're returning transition objects.
$this->assertInstanceOf(Transition::class, $workflow->getTransitions()['a_a']);
$this->assertInstanceOf(Transition::class, $workflow->getTypePlugin()->getTransitions()['a_a']);
// Passing in no IDs returns all transitions.
$this->assertArrayEquals(['a_b', 'a_a'], array_keys($workflow->getTransitions()));
$this->assertArrayEquals(['a_b', 'a_a'], array_keys($workflow->getTypePlugin()->getTransitions()));
// The order of states is by weight.
$workflow->setTransitionWeight('a_a', -1);
$this->assertArrayEquals(['a_a', 'a_b'], array_keys($workflow->getTransitions()));
$workflow->getTypePlugin()->setTransitionWeight('a_a', -1);
$this->assertArrayEquals(['a_a', 'a_b'], array_keys($workflow->getTypePlugin()->getTransitions()));
// If all weights are equal it will fallback to labels.
$workflow->setTransitionWeight('a_a', 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()));
$workflow->getTypePlugin()->setTransitionWeight('a_a', 0);
$this->assertArrayEquals(['a_a', 'a_b'], array_keys($workflow->getTypePlugin()->getTransitions()));
$workflow->getTypePlugin()->setTransitionLabel('a_b', 'A B');
$this->assertArrayEquals(['a_b', 'a_a'], array_keys($workflow->getTypePlugin()->getTransitions()));
// You can limit the states returned by passing in states IDs.
$this->assertArrayEquals(['a_a'], array_keys($workflow->getTransitions(['a_a'])));
$this->assertArrayEquals(['a_a'], array_keys($workflow->getTypePlugin()->getTransitions(['a_a'])));
// An empty array does not load all states.
$this->assertArrayEquals([], array_keys($workflow->getTransitions([])));
$this->assertArrayEquals([], array_keys($workflow->getTypePlugin()->getTransitions([])));
}
/**
* @covers ::getTransition
*/
@ -403,6 +424,7 @@ class WorkflowTest extends UnitTestCase {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
// By default states are ordered in the order added.
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published')
->addState('archived', 'Archived')
@ -410,22 +432,22 @@ class WorkflowTest extends UnitTestCase {
->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());
$this->assertInstanceOf(Transition::class, $workflow->getTypePlugin()->getTransition('create_new_draft'));
$this->assertEquals('publish', $workflow->getTypePlugin()->getTransition('publish')->id());
$this->assertEquals('Publish', $workflow->getTypePlugin()->getTransition('publish')->label());
$transition = $workflow->getTransition('publish');
$this->assertEquals($workflow->getState('draft'), $transition->from()['draft']);
$this->assertEquals($workflow->getState('published'), $transition->to());
$transition = $workflow->getTypePlugin()->getTransition('publish');
$this->assertEquals($workflow->getTypePlugin()->getState('draft'), $transition->from()['draft']);
$this->assertEquals($workflow->getTypePlugin()->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'");
$this->setExpectedException(\InvalidArgumentException::class, "The transition 'transition_that_does_not_exist' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTransition('transition_that_does_not_exist');
$workflow->getTypePlugin()->getTransition('transition_that_does_not_exist');
}
/**
@ -435,6 +457,7 @@ class WorkflowTest extends UnitTestCase {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
// By default states are ordered in the order added.
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published')
->addState('archived', 'Archived')
@ -442,15 +465,14 @@ class WorkflowTest extends UnitTestCase {
->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')));
$this->assertEquals(['create_new_draft', 'publish'], array_keys($workflow->getTypePlugin()->getTransitionsForState('draft')));
$this->assertEquals(['create_new_draft'], array_keys($workflow->getTypePlugin()->getTransitionsForState('draft', 'to')));
$this->assertEquals(['publish', 'archive'], array_keys($workflow->getTypePlugin()->getTransitionsForState('published')));
$this->assertEquals(['publish'], array_keys($workflow->getTypePlugin()->getTransitionsForState('published', 'to')));
$this->assertEquals(['create_new_draft'], array_keys($workflow->getTypePlugin()->getTransitionsForState('archived', 'from')));
$this->assertEquals(['archive'], array_keys($workflow->getTypePlugin()->getTransitionsForState('archived', 'to')));
}
/**
* @covers ::getTransitionFromStateToState
* @covers ::hasTransitionFromStateToState
@ -459,6 +481,7 @@ class WorkflowTest extends UnitTestCase {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
// By default states are ordered in the order added.
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published')
->addState('archived', 'Archived')
@ -466,9 +489,9 @@ class WorkflowTest extends UnitTestCase {
->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->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('draft', 'published'));
$this->assertFalse($workflow->getTypePlugin()->hasTransitionFromStateToState('archived', 'archived'));
$transition = $workflow->getTypePlugin()->getTransitionFromStateToState('published', 'archived');
$this->assertEquals('Archive', $transition->label());
}
@ -476,10 +499,11 @@ class WorkflowTest extends UnitTestCase {
* @covers ::getTransitionFromStateToState
*/
public function testGetTransitionFromStateToStateException() {
$this->setExpectedException(\InvalidArgumentException::class, "The transition from 'archived' to 'archived' does not exist in workflow 'test'");
$this->setExpectedException(\InvalidArgumentException::class, "The transition from 'archived' to 'archived' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
// By default states are ordered in the order added.
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published')
->addState('archived', 'Archived')
@ -487,7 +511,7 @@ class WorkflowTest extends UnitTestCase {
->addTransition('publish', 'Publish', ['draft', 'published'], 'published')
->addTransition('archive', 'Archive', ['published'], 'archived');
$workflow->getTransitionFromStateToState('archived', 'archived');
$workflow->getTypePlugin()->getTransitionFromStateToState('archived', 'archived');
}
/**
@ -496,22 +520,23 @@ class WorkflowTest extends UnitTestCase {
public function testSetTransitionLabel() {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow
->getTypePlugin()
->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());
$this->assertEquals('Publish', $workflow->getTypePlugin()->getTransition('publish')->label());
$workflow->getTypePlugin()->setTransitionLabel('publish', 'Publish!');
$this->assertEquals('Publish!', $workflow->getTypePlugin()->getTransition('publish')->label());
}
/**
* @covers ::setTransitionLabel
*/
public function testSetTransitionLabelException() {
$this->setExpectedException(\InvalidArgumentException::class, "The transition 'draft-published' does not exist in workflow 'test'");
$this->setExpectedException(\InvalidArgumentException::class, "The transition 'draft-published' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->addState('published', 'Published');
$workflow->setTransitionLabel('draft-published', 'Publish');
$workflow->getTypePlugin()->addState('published', 'Published');
$workflow->getTypePlugin()->setTransitionLabel('draft-published', 'Publish');
}
/**
@ -520,22 +545,34 @@ class WorkflowTest extends UnitTestCase {
public function testSetTransitionWeight() {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow
->getTypePlugin()
->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());
$this->assertEquals(0, $workflow->getTypePlugin()->getTransition('publish')->weight());
$workflow->getTypePlugin()->setTransitionWeight('publish', 10);
$this->assertEquals(10, $workflow->getTypePlugin()->getTransition('publish')->weight());
}
/**
* @covers ::setTransitionWeight
*/
public function testSetTransitionWeightException() {
$this->setExpectedException(\InvalidArgumentException::class, "The transition 'draft-published' does not exist in workflow 'test'");
$this->setExpectedException(\InvalidArgumentException::class, "The transition 'draft-published' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->addState('published', 'Published');
$workflow->setTransitionWeight('draft-published', 10);
$workflow->getTypePlugin()->addState('published', 'Published');
$workflow->getTypePlugin()->setTransitionWeight('draft-published', 10);
}
/**
* @covers ::setTransitionWeight
*/
public function testSetTransitionWeightNonNumericException() {
$this->setExpectedException(\InvalidArgumentException::class, "The weight 'foo' must be numeric for transition 'Publish'.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->getTypePlugin()->addState('published', 'Published');
$workflow->getTypePlugin()->addTransition('publish', 'Publish', [], 'published');
$workflow->getTypePlugin()->setTransitionWeight('publish', 'foo');
}
/**
@ -544,116 +581,106 @@ class WorkflowTest extends UnitTestCase {
public function testSetTransitionFromStates() {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow
->getTypePlugin()
->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'));
$this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('draft', 'draft'));
$this->assertFalse($workflow->getTypePlugin()->hasTransitionFromStateToState('published', 'draft'));
$this->assertFalse($workflow->getTypePlugin()->hasTransitionFromStateToState('archived', 'draft'));
$workflow->getTypePlugin()->setTransitionFromStates('test', ['draft', 'published', 'archived']);
$this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('draft', 'draft'));
$this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('published', 'draft'));
$this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('archived', 'draft'));
$workflow->getTypePlugin()->setTransitionFromStates('test', ['published', 'archived']);
$this->assertFalse($workflow->getTypePlugin()->hasTransitionFromStateToState('draft', 'draft'));
$this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('published', 'draft'));
$this->assertTrue($workflow->getTypePlugin()->hasTransitionFromStateToState('archived', 'draft'));
}
/**
* @covers ::setTransitionFromStates
*/
public function testSetTransitionFromStatesMissingTransition() {
$this->setExpectedException(\InvalidArgumentException::class, "The transition 'test' does not exist in workflow 'test'");
$this->setExpectedException(\InvalidArgumentException::class, "The transition 'test' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('published', 'Published')
->addState('archived', 'Archived')
->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft');
$workflow->setTransitionFromStates('test', ['draft', 'published', 'archived']);
$workflow->getTypePlugin()->setTransitionFromStates('test', ['draft', 'published', 'archived']);
}
/**
* @covers ::setTransitionFromStates
*/
public function testSetTransitionFromStatesMissingState() {
$this->setExpectedException(\InvalidArgumentException::class, "The state 'published' does not exist in workflow 'test'");
$this->setExpectedException(\InvalidArgumentException::class, "The state 'published' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('archived', 'Archived')
->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft');
$workflow->setTransitionFromStates('create_new_draft', ['draft', 'published', 'archived']);
$workflow->getTypePlugin()->setTransitionFromStates('create_new_draft', ['draft', 'published', 'archived']);
}
/**
* @covers ::setTransitionFromStates
*/
public function testSetTransitionFromStatesAlreadyExists() {
$this->setExpectedException(\InvalidArgumentException::class, "The 'create_new_draft' transition already allows 'draft' to 'draft' transitions in workflow 'test'");
$this->setExpectedException(\InvalidArgumentException::class, "The 'create_new_draft' transition already allows 'draft' to 'draft' transitions in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow
->getTypePlugin()
->addState('draft', 'Draft')
->addState('archived', 'Archived')
->addState('needs_review', 'Needs Review')
->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft')
->addTransition('needs_review', 'Needs review', ['needs_review'], 'draft');
$workflow->setTransitionFromStates('needs_review', ['draft']);
$workflow->getTypePlugin()->setTransitionFromStates('needs_review', ['draft']);
}
/**
* @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
$workflow_type = new TestType([], '', []);
$workflow_type
->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'));
$this->assertTrue($workflow_type->getState('draft')->canTransitionTo('published'));
$workflow_type->deleteTransition('publish');
$this->assertFalse($workflow_type->getState('draft')->canTransitionTo('published'));
$this->assertTrue($workflow_type->getState('draft')->canTransitionTo('draft'));
}
/**
* @covers ::deleteTransition
*/
public function testDeleteTransitionException() {
$this->setExpectedException(\InvalidArgumentException::class, "The transition 'draft-published' does not exist in workflow 'test'");
$this->setExpectedException(\InvalidArgumentException::class, "The transition 'draft-published' does not exist in workflow.");
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$workflow->addState('published', 'Published');
$workflow->deleteTransition('draft-published');
$workflow->getTypePlugin()->addState('published', 'Published');
$workflow->getTypePlugin()->deleteTransition('draft-published');
}
/**
* @covers ::status
* @covers \Drupal\workflows\Entity\Workflow::status
*/
public function testStatus() {
$workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
$this->assertFalse($workflow->status());
$workflow->addState('published', 'Published');
$workflow->getTypePlugin()->addState('published', 'Published');
$this->assertTrue($workflow->status());
}

View file

@ -0,0 +1,17 @@
<?php
/**
* @file
* API documentation for Workflows module.
*/
/**
* @defgroup workflow_type_plugins Workflow Type Plugins
* @{
* Any module harnessing Workflows module must define a Workflow Type Plugin.
* This allows the module to tailor the workflow to its specific need. For
* example, Content Moderation module uses its the Workflow Type Plugin to link
* workflows to entities. On their own, workflows are a stand-alone concept. It
* takes a module such as Content Moderation to give the workflow context.
* @}
*/

View file

@ -1,7 +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.'
description: 'Provides UI and API for managing workflows. This module can be used with the Content moderation module to add highly customizable workflows to content.'
version: VERSION
core: 8.x
package: Core (Experimental)
package: Core
configure: entity.workflow.collection

View file

@ -0,0 +1,8 @@
# Workflows extension relation types.
# See https://tools.ietf.org/html/rfc5988#section-4.2.
add-state-form:
uri: https://drupal.org/link-relations/add-state-form
description: A form where a state can be created.
add-transition-form:
uri: https://drupal.org/link-relations/add-transition-form
description: A form where a transition can be created.

View file

@ -1,7 +1,5 @@
# Workflow menu items definition
entity.workflow.collection:
title: 'Workflows'
route_name: entity.workflow.collection
description: 'Configure workflows.'
parent: system.admin_config_workflow

View file

@ -2,7 +2,7 @@
/**
* @file
* Provides hook implementations for the Workflow UI module.
* Provides hook implementations for the Workflows module.
*/
use Drupal\Core\Routing\RouteMatchInterface;
@ -12,11 +12,16 @@ use Drupal\Core\Routing\RouteMatchInterface;
*/
function workflows_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
// Main module help for the Workflow UI module.
case 'help.page.workflows':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . 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 <a href=":workflow">online documentation for the Workflows module</a>.', [':workflow' => 'https://www.drupal.org/documentation/modules/workflows']) . '</p>';
$output .= '<h4>' . t('Workflow') . '</h4>';
$output .= '<p>' . t('A collection of states and transitions between those states.') . '</p>';
$output .= '<h4>' . t('State') . '</h4>';
$output .= '<p>' . t('A particular condition that something is in at a specific time. The usage of the state is determined by a module that harnesses the Workflows module. For example, Content Moderation allows a state to be used for moderation of content by assigning a given state to a content item.') . '</p>';
$output .= '<h4>' . t('Transition') . '</h4>';
$output .= '<p>' . t('The process of changing from one state to another. A transition can occur from multiple states, but only to one state.') . '</p>';
return $output;
}
}