clean install

This commit is contained in:
2024-12-10 15:08:16 +01:00
commit e14eb2d8fd
31193 changed files with 3555714 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
<?php
namespace Drupal\workflows\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Workflow type annotation object.
*
* Plugin Namespace: Plugin\WorkflowType
*
* For a working example, see \Drupal\content_moderation\Plugin\Workflow\ContentModerate
*
* @see \Drupal\workflows\WorkflowTypeInterface
* @see \Drupal\workflows\WorkflowTypeManager
* @see workflow_type_info_alter()
* @see plugin_api
*
* @Annotation
*/
class WorkflowType extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The label of the workflow.
*
* @var \Drupal\Core\Annotation\Translation
*
* @ingroup plugin_translatable
*/
public $label = '';
/**
* States required to exist.
*
* Normally supplied by WorkflowType::defaultConfiguration().
*
* @var array
*/
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

@@ -0,0 +1,71 @@
<?php
namespace Drupal\workflows\Attribute;
use Drupal\Component\Plugin\Attribute\Plugin;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Defines a Workflow type attribute object.
*
* Plugin Namespace: Plugin\WorkflowType
*
* For a working example, see \Drupal\content_moderation\Plugin\Workflow\ContentModerate
*
* @see \Drupal\workflows\WorkflowTypeInterface
* @see \Drupal\workflows\WorkflowTypeManager
* @see workflow_type_info_alter()
* @see plugin_api
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class WorkflowType extends Plugin {
/**
* States required to exist.
*
* Normally supplied by WorkflowType::defaultConfiguration().
*/
public array $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
*/
public array $forms = [];
/**
* Constructs an Action attribute.
*
* @param string $id
* The plugin ID.
* @param \Drupal\Core\StringTranslation\TranslatableMarkup|null $label
* The label of the action.
* @param string[] $forms
* A list of optional form classes implementing PluginFormInterface.
* @param string[] $required_states
* States required to exist.
*/
public function __construct(
public readonly string $id,
public readonly ?TranslatableMarkup $label = NULL,
array $forms = [],
array $required_states = [],
) {
$this->forms = $forms;
$this->required_states = $required_states;
}
}

View File

@@ -0,0 +1,179 @@
<?php
namespace Drupal\workflows\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection;
use Drupal\workflows\Exception\RequiredStateMissingException;
use Drupal\workflows\WorkflowInterface;
/**
* Defines the workflow entity.
*
* @ConfigEntityType(
* 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",
* "form" = {
* "add" = "Drupal\workflows\Form\WorkflowAddForm",
* "edit" = "Drupal\workflows\Form\WorkflowEditForm",
* "delete" = "Drupal\workflows\Form\WorkflowDeleteForm",
* "add-state" = "Drupal\workflows\Form\WorkflowStateAddForm",
* "edit-state" = "Drupal\workflows\Form\WorkflowStateEditForm",
* "delete-state" = "Drupal\workflows\Form\WorkflowStateDeleteForm",
* "add-transition" = "Drupal\workflows\Form\WorkflowTransitionAddForm",
* "edit-transition" = "Drupal\workflows\Form\WorkflowTransitionEditForm",
* "delete-transition" = "Drupal\workflows\Form\WorkflowTransitionDeleteForm",
* },
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
* },
* },
* config_prefix = "workflow",
* admin_permission = "administer workflows",
* entity_keys = {
* "id" = "id",
* "label" = "label",
* "uuid" = "uuid",
* },
* links = {
* "add-form" = "/admin/config/workflow/workflows/add",
* "edit-form" = "/admin/config/workflow/workflows/manage/{workflow}",
* "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",
* },
* config_export = {
* "id",
* "label",
* "type",
* "type_settings",
* },
* )
*/
class Workflow extends ConfigEntityBase implements WorkflowInterface, EntityWithPluginCollectionInterface {
/**
* The Workflow ID.
*
* @var string
*/
protected $id;
/**
* The workflow label.
*
* @var string
*/
protected $label;
/**
* The workflow type plugin ID.
*
* @see \Drupal\workflows\WorkflowTypeManager
*
* @var string
*/
protected $type;
/**
* The configuration for the workflow type plugin.
*
* @var array
*/
protected $type_settings = [];
/**
* The workflow type plugin collection.
*
* @var \Drupal\Component\Plugin\LazyPluginCollection
*/
protected $pluginCollection;
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage) {
$workflow_type = $this->getTypePlugin();
$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)));
}
parent::preSave($storage);
}
/**
* {@inheritdoc}
*/
public function getTypePlugin() {
return $this->getPluginCollection()->get($this->type);
}
/**
* {@inheritdoc}
*/
public function getPluginCollections() {
return ['type_settings' => $this->getPluginCollection()];
}
/**
* Encapsulates the creation of the workflow's plugin collection.
*
* @return \Drupal\Core\Plugin\DefaultSingleLazyPluginCollection
* The workflow's plugin collection.
*/
protected function getPluginCollection() {
if (!$this->pluginCollection && $this->type) {
$this->pluginCollection = new DefaultSingleLazyPluginCollection(\Drupal::service('plugin.manager.workflows.type'), $this->type, $this->type_settings);
}
return $this->pluginCollection;
}
/**
* Loads all workflows of the provided type.
*
* @param string $type
* The workflow type to load all workflows for.
*
* @return static[]
* An array of workflow objects of the provided workflow type, indexed by
* their IDs.
*
* @see \Drupal\workflows\Annotation\WorkflowType
*/
public static function loadMultipleByType($type) {
return self::loadMultiple(\Drupal::entityQuery('workflow')->condition('type', $type)->execute());
}
/**
* {@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->getTypePlugin()->getStates());
}
/**
* {@inheritdoc}
*/
public function onDependencyRemoval(array $dependencies) {
// 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

@@ -0,0 +1,11 @@
<?php
namespace Drupal\workflows\Exception;
use Drupal\Core\Config\ConfigException;
/**
* Indicates that a workflow does not contain a required state.
*/
class RequiredStateMissingException extends ConfigException {
}

View File

@@ -0,0 +1,116 @@
<?php
namespace Drupal\workflows\Form;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\workflows\Entity\Workflow;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Form for adding workflows.
*
* @internal
*/
class WorkflowAddForm extends EntityForm {
/**
* The workflow type plugin manager.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $workflowTypePluginManager;
/**
* WorkflowAddForm constructor.
*
* @param \Drupal\Component\Plugin\PluginManagerInterface $workflow_type_plugin_manager
* The workflow type plugin manager.
*/
public function __construct(PluginManagerInterface $workflow_type_plugin_manager) {
$this->workflowTypePluginManager = $workflow_type_plugin_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('plugin.manager.workflows.type')
);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
/** @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = $this->entity;
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#maxlength' => 255,
'#default_value' => $workflow->label(),
'#required' => TRUE,
];
$form['id'] = [
'#type' => 'machine_name',
'#default_value' => $workflow->id(),
'#machine_name' => [
'exists' => [Workflow::class, 'load'],
],
];
$workflow_types = array_column($this->workflowTypePluginManager->getDefinitions(), 'label', 'id');
$form['workflow_type'] = [
'#type' => 'select',
'#title' => $this->t('Workflow type'),
'#required' => TRUE,
'#options' => $workflow_types,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
/** @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = $this->entity;
$return = $workflow->save();
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 {
$this->messenger()->addStatus($this->t('Created the %label Workflow.', [
'%label' => $workflow->label(),
]));
$form_state->setRedirectUrl($workflow->toUrl('edit-form'));
}
return $return;
}
/**
* {@inheritdoc}
*/
protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
// This form can only set the workflow's ID, label and the weights for each
// state.
/** @var \Drupal\workflows\WorkflowInterface $entity */
$values = $form_state->getValues();
$entity->set('label', $values['label']);
$entity->set('id', $values['id']);
$entity->set('type', $values['workflow_type']);
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace Drupal\workflows\Form;
use Drupal\Core\Entity\EntityConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
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}
*/
public function getQuestion() {
return $this->t('Are you sure you want to delete %name?', ['%name' => $this->entity->label()]);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return new Url('entity.workflow.collection');
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Delete');
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->entity->delete();
$this->messenger()->addStatus($this->t(
'Workflow %label deleted.',
['%label' => $this->entity->label()]
));
$form_state->setRedirectUrl($this->getCancelUrl());
}
}

View File

@@ -0,0 +1,283 @@
<?php
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}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
/** @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(),
'#required' => TRUE,
];
$form['id'] = [
'#type' => 'machine_name',
'#default_value' => $workflow->id(),
'#machine_name' => [
'exists' => [Workflow::class, 'load'],
],
'#disabled' => TRUE,
];
$header = [
'state' => $this->t('State'),
'weight' => $this->t('Weight'),
'operations' => $this->t('Operations'),
];
$form['states_container'] = [
'#type' => 'details',
'#title' => $this->t('States'),
'#open' => TRUE,
];
$form['states_container']['states'] = [
'#type' => 'table',
'#header' => $header,
'#title' => $this->t('States'),
'#empty' => $this->t('There are no states yet.'),
'#tabledrag' => [
[
'action' => 'order',
'relationship' => 'sibling',
'group' => 'state-weight',
],
],
];
$states = $workflow->getTypePlugin()->getStates();
// Warn the user if there are no states.
if (empty($states)) {
$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()]
)
);
}
$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()]),
],
];
if ($this->entity->access('delete-state:' . $state->id())) {
$links['delete'] = [
'title' => $this->t('Delete'),
'url' => Url::fromRoute('entity.workflow.delete_state_form', [
'workflow' => $workflow->id(),
'workflow_state' => $state->id(),
]),
];
}
$form['states_container']['states'][$state->id()] = [
'#attributes' => ['class' => ['draggable']],
'state' => ['#markup' => $state->label()],
'#weight' => $state->weight(),
'weight' => [
'#type' => 'weight',
'#title' => $this->t('Weight for @title', ['@title' => $state->label()]),
'#title_display' => 'invisible',
'#default_value' => $state->weight(),
'#attributes' => ['class' => ['state-weight']],
'#delta' => $state_weight_delta,
],
'operations' => [
'#type' => 'operations',
'#links' => $links,
],
];
}
$form['states_container']['state_add'] = [
'#markup' => $workflow->toLink($this->t('Add a new state'), 'add-state-form')->toString(),
];
$header = [
'label' => $this->t('Label'),
'weight' => $this->t('Weight'),
'from' => $this->t('From'),
'to' => $this->t('To'),
'operations' => $this->t('Operations'),
];
$form['transitions_container'] = [
'#type' => 'details',
'#title' => $this->t('Transitions'),
'#open' => TRUE,
];
$form['transitions_container']['transitions'] = [
'#type' => 'table',
'#header' => $header,
'#title' => $this->t('Transitions'),
'#empty' => $this->t('There are no transitions yet.'),
'#tabledrag' => [
[
'action' => 'order',
'relationship' => 'sibling',
'group' => 'transition-weight',
],
],
];
$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()]),
];
$links['delete'] = [
'title' => $this->t('Delete'),
'url' => Url::fromRoute('entity.workflow.delete_transition_form', ['workflow' => $workflow->id(), 'workflow_transition' => $transition->id()]),
];
$form['transitions_container']['transitions'][$transition->id()] = [
'#attributes' => ['class' => ['draggable']],
'label' => ['#markup' => $transition->label()],
'#weight' => $transition->weight(),
'weight' => [
'#type' => 'weight',
'#title' => $this->t('Weight for @title', ['@title' => $transition->label()]),
'#title_display' => 'invisible',
'#default_value' => $transition->weight(),
'#attributes' => ['class' => ['transition-weight']],
'#delta' => $transition_weight_delta,
],
'from' => [
'#theme' => 'item_list',
'#items' => array_map([State::class, 'labelCallback'], $transition->from()),
'#context' => ['list_style' => 'comma-list'],
],
'to' => ['#markup' => $transition->to()->label()],
'operations' => [
'#type' => 'operations',
'#links' => $links,
],
];
}
$form['transitions_container']['transition_add'] = [
'#markup' => $workflow->toLink($this->t('Add a new transition'), 'add-transition-form')->toString(),
];
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();
$this->messenger()->addStatus($this->t('Saved the %label Workflow.', ['%label' => $workflow->label()]));
}
/**
* {@inheritdoc}
*/
protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
// This form can only set the workflow's ID, label and the weights for each
// state.
/** @var \Drupal\workflows\WorkflowInterface $entity */
$values = $form_state->getValues();
$entity->set('label', $values['label']);
$entity->set('id', $values['id']);
foreach ($values['states'] as $state_id => $state_values) {
$entity->getTypePlugin()->setStateWeight($state_id, $state_values['weight']);
}
foreach ($values['transitions'] as $transition_id => $transition_values) {
$entity->getTypePlugin()->setTransitionWeight($transition_id, $transition_values['weight']);
}
}
}

View File

@@ -0,0 +1,181 @@
<?php
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;
/**
* Entity form variant for adding workflow states.
*
* @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}
*/
public function getFormId() {
return 'workflow_state_add_form';
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
/** @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = $this->getEntity();
$workflow_type = $workflow->getTypePlugin();
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('State label'),
'#maxlength' => 255,
'#default_value' => '',
'#required' => TRUE,
];
$form['id'] = [
'#type' => 'machine_name',
'#machine_name' => [
'exists' => [$this, 'exists'],
],
];
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;
}
/**
* Determines if the workflow state already exists.
*
* @param string $state_id
* The workflow state ID.
*
* @return bool
* TRUE if the workflow state exists, FALSE otherwise.
*/
public function exists($state_id) {
/** @var \Drupal\workflows\WorkflowInterface $original_workflow */
$original_workflow = \Drupal::entityTypeManager()->getStorage('workflow')->loadUnchanged($this->getEntity()->id());
return $original_workflow->getTypePlugin()->hasState($state_id);
}
/**
* Copies top-level form values to entity properties.
*
* This form can only change values for a state, which is part of workflow.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity the current form should operate upon.
* @param array $form
* A nested array of form elements comprising the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
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']);
}
/**
* {@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);
}
}
/**
* {@inheritdoc}
*/
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();
$this->messenger()->addStatus($this->t('Created %label state.', [
'%label' => $workflow->getTypePlugin()->getState($form_state->getValue('id'))->label(),
]));
$form_state->setRedirectUrl($workflow->toUrl('edit-form'));
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save'),
'#submit' => ['::submitForm', '::save'],
];
return $actions;
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Drupal\workflows\Form;
use Drupal\workflows\WorkflowInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Builds the form to delete states from Workflow entities.
*
* @internal
*/
class WorkflowStateDeleteForm extends ConfirmFormBase {
/**
* The workflow entity the state being deleted belongs to.
*
* @var \Drupal\workflows\WorkflowInterface
*/
protected $workflow;
/**
* The state being deleted.
*
* @var string
*/
protected $stateId;
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'workflow_state_delete_form';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
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()]);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return $this->workflow->toUrl();
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Delete');
}
/**
* Form constructor.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param \Drupal\workflows\WorkflowInterface $workflow
* The workflow entity being edited.
* @param string|null $workflow_state
* The workflow state being deleted.
*
* @return array
* The form structure.
*/
public function buildForm(array $form, FormStateInterface $form_state, ?WorkflowInterface $workflow = NULL, $workflow_state = NULL) {
if (!$workflow->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);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$workflow_label = $this->workflow->getTypePlugin()->getState($this->stateId)->label();
$this->workflow
->getTypePlugin()
->deleteState($this->stateId);
$this->workflow->save();
$this->messenger()->addStatus($this->t(
'State %label deleted.',
['%label' => $workflow_label]
));
$form_state->setRedirectUrl($this->getCancelUrl());
}
}

View File

@@ -0,0 +1,240 @@
<?php
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;
/**
* Entity form variant for editing workflow states.
*
* @internal
*/
class WorkflowStateEditForm extends EntityForm {
/**
* The ID of the state that is being edited.
*
* @var string
*/
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}
*/
public function getFormId() {
return 'workflow_state_edit_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $workflow_state = NULL) {
$this->stateId = $workflow_state;
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
/** @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = $this->getEntity();
$workflow_type = $workflow->getTypePlugin();
$state = $workflow->getTypePlugin()->getState($this->stateId);
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('State label'),
'#maxlength' => 255,
'#default_value' => $state->label(),
'#required' => TRUE,
];
$form['id'] = [
'#type' => 'machine_name',
'#default_value' => $this->stateId,
'#machine_name' => [
'exists' => [$this, 'exists'],
],
'#disabled' => TRUE,
];
// Add additional form fields from the workflow type plugin.
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'),
'state' => $this->t('To'),
'operations' => $this->t('Operations'),
];
$form['transitions'] = [
'#type' => 'table',
'#header' => $header,
'#empty' => $this->t('There are no transitions to or from this state yet.'),
];
foreach ($state->getTransitions() as $transition) {
$links['edit'] = [
'title' => $this->t('Edit'),
'url' => Url::fromRoute('entity.workflow.edit_transition_form', [
'workflow' => $workflow->id(),
'workflow_transition' => $transition->id(),
]),
];
$links['delete'] = [
'title' => $this->t('Delete'),
'url' => Url::fromRoute('entity.workflow.delete_transition_form', [
'workflow' => $workflow->id(),
'workflow_transition' => $transition->id(),
]),
];
$form['transitions'][$transition->id()] = [
'label' => [
'#markup' => $transition->label(),
],
'state' => [
'#markup' => $transition->to()->label(),
],
'operations' => [
'#type' => 'operations',
'#links' => $links,
],
];
}
return $form;
}
/**
* Copies top-level form values to entity properties.
*
* This form can only change values for a state, which is part of workflow.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity the current form should operate upon.
* @param array $form
* A nested array of form elements comprising the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
if (!$form_state->isValidationComplete()) {
// Only do something once form validation is complete.
return;
}
/** @var \Drupal\workflows\WorkflowInterface $entity */
$values = $form_state->getValues();
$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);
}
}
/**
* {@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(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();
$this->messenger()->addStatus($this->t('Saved %label state.', [
'%label' => $workflow->getTypePlugin()->getState($this->stateId)->label(),
]));
$form_state->setRedirectUrl($workflow->toUrl('edit-form'));
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save'),
'#submit' => ['::submitForm', '::save'],
];
$actions['delete'] = [
'#type' => 'link',
'#title' => $this->t('Delete'),
'#access' => $this->entity->access('delete-state:' . $this->stateId),
'#attributes' => [
'class' => ['button', 'button--danger'],
],
'#url' => Url::fromRoute('entity.workflow.delete_state_form', [
'workflow' => $this->entity->id(),
'workflow_state' => $this->stateId,
]),
];
return $actions;
}
}

View File

@@ -0,0 +1,209 @@
<?php
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;
/**
* Entity form variant for adding workflow transitions.
*
* @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}
*/
public function getFormId() {
return 'workflow_transition_add_form';
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
/** @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = $this->getEntity();
$workflow_type = $workflow->getTypePlugin();
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Transition label'),
'#maxlength' => 255,
'#default_value' => '',
'#required' => TRUE,
];
$form['id'] = [
'#type' => 'machine_name',
'#machine_name' => [
'exists' => [$this, 'exists'],
],
];
// @todo https://www.drupal.org/node/2830584 Add some ajax to ensure that
// only valid transitions are selectable.
$states = array_map([State::class, 'labelCallback'], $workflow->getTypePlugin()->getStates());
$form['from'] = [
'#type' => 'checkboxes',
'#title' => $this->t('From'),
'#required' => TRUE,
'#default_value' => [],
'#options' => $states,
];
$form['to'] = [
'#type' => 'radios',
'#title' => $this->t('To'),
'#required' => TRUE,
'#options' => $states,
];
// Add additional form fields from the workflow type plugin.
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;
}
/**
* Determines if the workflow transition already exists.
*
* @param string $transition_id
* The workflow transition ID.
*
* @return bool
* TRUE if the workflow transition exists, FALSE otherwise.
*/
public function exists($transition_id) {
/** @var \Drupal\workflows\WorkflowInterface $original_workflow */
$original_workflow = \Drupal::entityTypeManager()->getStorage('workflow')->loadUnchanged($this->getEntity()->id());
return $original_workflow->getTypePlugin()->hasTransition($transition_id);
}
/**
* Copies top-level form values to entity properties.
*
* This form can only change values for a state, which is part of workflow.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity the current form should operate upon.
* @param array $form
* A nested array of form elements comprising the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
if (!$form_state->isValidationComplete()) {
// Only do something once form validation is complete.
return;
}
/** @var \Drupal\workflows\WorkflowInterface $entity */
$values = $form_state->getValues();
$entity->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->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->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);
}
}
/**
* {@inheritdoc}
*/
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();
$this->messenger()->addStatus($this->t('Created %label transition.', [
'%label' => $form_state->getValue('label'),
]));
$form_state->setRedirectUrl($workflow->toUrl('edit-form'));
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save'),
'#submit' => ['::submitForm', '::save'],
];
return $actions;
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Drupal\workflows\Form;
use Drupal\workflows\WorkflowInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Builds the form to delete transitions from Workflow entities.
*
* @internal
*/
class WorkflowTransitionDeleteForm extends ConfirmFormBase {
/**
* The workflow entity the transition being deleted belongs to.
*
* @var \Drupal\workflows\WorkflowInterface
*/
protected $workflow;
/**
* The workflow transition being deleted.
*
* @var \Drupal\workflows\TransitionInterface
*/
protected $transition;
/**
* The transition being deleted.
*
* @var string
*/
protected $transitionId;
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'workflow_transition_delete_form';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Are you sure you want to delete %transition from %workflow?', ['%transition' => $this->transition->label(), '%workflow' => $this->workflow->label()]);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return $this->workflow->toUrl();
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return $this->t('Delete');
}
/**
* Form constructor.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
* @param \Drupal\workflows\WorkflowInterface $workflow
* The workflow entity being edited.
* @param string|null $workflow_transition
* The workflow transition being deleted.
*
* @return array
* The form structure.
*/
public function buildForm(array $form, FormStateInterface $form_state, ?WorkflowInterface $workflow = NULL, $workflow_transition = NULL) {
try {
$this->transition = $workflow->getTypePlugin()->getTransition($workflow_transition);
}
catch (\InvalidArgumentException $e) {
throw new NotFoundHttpException();
}
$this->workflow = $workflow;
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->workflow
->getTypePlugin()
->deleteTransition($this->transition->id());
$this->workflow->save();
$this->messenger()->addStatus($this->t('%transition transition deleted.', ['%transition' => $this->transition->label()]));
$form_state->setRedirectUrl($this->getCancelUrl());
}
}

View File

@@ -0,0 +1,234 @@
<?php
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;
/**
* Entity form variant for editing workflow transitions.
*
* @internal
*/
class WorkflowTransitionEditForm extends EntityForm {
/**
* The ID of the transition that is being edited.
*
* @var string
*/
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}
*/
public function getFormId() {
return 'workflow_transition_edit_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $workflow_transition = NULL) {
$this->transitionId = $workflow_transition;
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
/** @var \Drupal\workflows\WorkflowInterface $workflow */
$workflow = $this->getEntity();
$workflow_type = $workflow->getTypePlugin();
$transition = $workflow->getTypePlugin()->getTransition($this->transitionId);
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Transition label'),
'#maxlength' => 255,
'#default_value' => $transition->label(),
'#required' => TRUE,
];
$form['id'] = [
'#type' => 'value',
'#value' => $this->transitionId,
];
// @todo https://www.drupal.org/node/2830584 Add some ajax to ensure that
// only valid transitions are selectable.
$states = array_map([State::class, 'labelCallback'], $workflow->getTypePlugin()->getStates());
$form['from'] = [
'#type' => 'checkboxes',
'#title' => $this->t('From'),
'#required' => TRUE,
'#default_value' => array_keys($transition->from()),
'#options' => $states,
];
$form['to'] = [
'#type' => 'radios',
'#title' => $this->t('To'),
'#required' => TRUE,
'#default_value' => $transition->to()->id(),
'#options' => $states,
'#disabled' => TRUE,
];
// Add additional form fields from the workflow type plugin.
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;
}
/**
* {@inheritdoc}
*/
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_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->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);
}
}
/**
* Copies top-level form values to entity properties.
*
* This form can only change values for a state, which is part of workflow.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity the current form should operate upon.
* @param array $form
* A nested array of form elements comprising the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
if (!$form_state->isValidationComplete()) {
// Only do something once form validation is complete.
return;
}
/** @var \Drupal\workflows\WorkflowInterface $entity */
$values = $form_state->getValues();
$form_state->set('created_transition', FALSE);
$entity->getTypePlugin()->setTransitionLabel($values['id'], $values['label']);
$entity->getTypePlugin()->setTransitionFromStates($values['id'], array_filter($values['from']));
}
/**
* {@inheritdoc}
*/
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();
$this->messenger()->addStatus($this->t('Saved %label transition.', [
'%label' => $workflow->getTypePlugin()->getTransition($this->transitionId)->label(),
]));
$form_state->setRedirectUrl($workflow->toUrl('edit-form'));
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state) {
$actions['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save'),
'#submit' => ['::submitForm', '::save'],
];
$actions['delete'] = [
'#type' => 'link',
'#title' => $this->t('Delete'),
// Deleting a transition is editing a workflow.
'#access' => $this->entity->access('edit'),
'#attributes' => [
'class' => ['button', 'button--danger'],
],
'#url' => Url::fromRoute('entity.workflow.delete_transition_form', [
'workflow' => $this->entity->id(),
'workflow_transition' => $this->transitionId,
]),
];
return $actions;
}
}

View File

@@ -0,0 +1,464 @@
<?php
namespace Drupal\workflows\Plugin;
use Drupal\Component\Plugin\PluginBase;
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;
/**
* A base class for Workflow type plugins.
*
* @see \Drupal\workflows\Annotation\WorkflowType
*/
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}
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->setConfiguration($configuration);
}
/**
* {@inheritdoc}
*/
public function label() {
$definition = $this->getPluginDefinition();
// The label can be an object.
// @see \Drupal\Core\StringTranslation\TranslatableMarkup
return $definition['label'];
}
/**
* {@inheritdoc}
*/
public function workflowHasData(WorkflowInterface $workflow) {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function workflowStateHasData(WorkflowInterface $workflow, StateInterface $state) {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getConfiguration() {
return $this->configuration;
}
/**
* {@inheritdoc}
*/
public function setConfiguration(array $configuration) {
$this->configuration = $configuration + $this->defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function getRequiredStates() {
return $this->getPluginDefinition()['required_states'];
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'states' => [],
'transitions' => [],
];
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
return [];
}
/**
* {@inheritdoc}
*/
public function onDependencyRemoval(array $dependencies) {
return FALSE;
}
/**
* {@inheritdoc}
*/
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

@@ -0,0 +1,115 @@
<?php
namespace Drupal\workflows;
/**
* A value object representing a workflow state.
*/
class State implements StateInterface {
/**
* The workflow the state is attached to.
*
* @var \Drupal\workflows\WorkflowTypeInterface
*/
protected $workflow;
/**
* The state's ID.
*
* @var string
*/
protected $id;
/**
* The state's label.
*
* @var string
*/
protected $label;
/**
* The state's weight.
*
* @var int
*/
protected $weight;
/**
* State constructor.
*
* @param \Drupal\workflows\WorkflowTypeInterface $workflow
* The workflow the state is attached to.
* @param string $id
* The state's ID.
* @param string $label
* The state's label.
* @param int $weight
* The state's weight.
*/
public function __construct(WorkflowTypeInterface $workflow, $id, $label, $weight = 0) {
$this->workflow = $workflow;
$this->id = $id;
$this->label = $label;
$this->weight = $weight;
}
/**
* {@inheritdoc}
*/
public function id() {
return $this->id;
}
/**
* {@inheritdoc}
*/
public function label() {
return $this->label;
}
/**
* {@inheritdoc}
*/
public function weight() {
return $this->weight;
}
/**
* {@inheritdoc}
*/
public function canTransitionTo($to_state_id) {
return $this->workflow->hasTransitionFromStateToState($this->id, $to_state_id);
}
/**
* {@inheritdoc}
*/
public function getTransitionTo($to_state_id) {
if (!$this->canTransitionTo($to_state_id)) {
throw new \InvalidArgumentException("Can not transition to '$to_state_id' state");
}
return $this->workflow->getTransitionFromStateToState($this->id(), $to_state_id);
}
/**
* {@inheritdoc}
*/
public function getTransitions() {
return $this->workflow->getTransitionsForState($this->id);
}
/**
* Helper method to convert a State value object to a label.
*
* @param \Drupal\workflows\StateInterface $state
* The state.
*
* @return string
* The label of the state.
*/
public static function labelCallback(StateInterface $state) {
return $state->label();
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Drupal\workflows;
/**
* An interface for state value objects.
*
* @internal
* 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.
*
* @return string
* The state's ID.
*/
public function id();
/**
* Gets the state's label.
*
* @return string
* The state's label.
*/
public function label();
/**
* Gets the state's weight.
*
* @return int
* The state's weight.
*/
public function weight();
/**
* Determines if the state can transition to the provided state ID.
*
* @param $to_state_id
* The state to transition to.
*
* @return bool
* TRUE if the state can transition to the provided state ID. FALSE, if not.
*/
public function canTransitionTo($to_state_id);
/**
* Gets the Transition object for the provided state ID.
*
* @param $to_state_id
* The state to transition to.
*
* @return \Drupal\workflows\TransitionInterface
* The Transition object for the provided state ID.
*
* @throws \InvalidArgumentException()
* Exception thrown when the provided state ID can not be transitioned to.
*/
public function getTransitionTo($to_state_id);
/**
* Gets all the possible transition objects for the state.
*
* @return \Drupal\workflows\TransitionInterface[]
* All the possible transition objects for the state.
*/
public function getTransitions();
}

View File

@@ -0,0 +1,112 @@
<?php
namespace Drupal\workflows;
/**
* A transition value object that describes the transition between states.
*/
class Transition implements TransitionInterface {
/**
* The workflow that this transition is attached to.
*
* @var \Drupal\workflows\WorkflowTypeInterface
*/
protected $workflow;
/**
* The transition's ID.
*
* @var string
*/
protected $id;
/**
* The transition's label.
*
* @var string
*/
protected $label;
/**
* The transition's from state IDs.
*
* @var string[]
*/
protected $fromStateIds;
/**
* The transition's to state ID.
*
* @var string
*/
protected $toStateId;
/**
* The transition's weight.
*
* @var int
*/
protected $weight;
/**
* Transition constructor.
*
* @param \Drupal\workflows\WorkflowTypeInterface $workflow
* The workflow the state is attached to.
* @param string $id
* The transition's ID.
* @param string $label
* The transition's label.
* @param array $from_state_ids
* A list of from state IDs.
* @param string $to_state_id
* The to state ID.
* @param int $weight
* (optional) The transition's weight. Defaults to 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;
$this->fromStateIds = $from_state_ids;
$this->toStateId = $to_state_id;
$this->weight = $weight;
}
/**
* {@inheritdoc}
*/
public function id() {
return $this->id;
}
/**
* {@inheritdoc}
*/
public function label() {
return $this->label;
}
/**
* {@inheritdoc}
*/
public function from() {
return $this->workflow->getStates($this->fromStateIds);
}
/**
* {@inheritdoc}
*/
public function to() {
return $this->workflow->getState($this->toStateId);
}
/**
* {@inheritdoc}
*/
public function weight() {
return $this->weight;
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Drupal\workflows;
/**
* A transition value object that describes the transition between two states.
*
* @internal
* 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.
*
* @return string
* The transition's ID.
*/
public function id();
/**
* Gets the transition's label.
*
* @return string
* The transition's label.
*/
public function label();
/**
* Gets the transition's from states.
*
* @return \Drupal\workflows\StateInterface[]
* The transition's from states.
*/
public function from();
/**
* Gets the transition's to state.
*
* @return \Drupal\workflows\StateInterface
* The transition's to state.
*/
public function to();
/**
* Gets the transition's weight.
*
* @return string
* The transition's weight.
*/
public function weight();
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Drupal\workflows;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessResult;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Access controller for the Workflow entity.
*
* @see \Drupal\workflows\Entity\Workflow.
*/
class WorkflowAccessControlHandler extends EntityAccessControlHandler implements EntityHandlerInterface {
/**
* The workflow type plugin manager.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $workflowTypeManager;
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('plugin.manager.workflows.type')
);
}
/**
* Constructs the workflow access control handler instance.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Component\Plugin\PluginManagerInterface $workflow_type_manager
* The workflow type plugin manager.
*/
public function __construct(EntityTypeInterface $entity_type, PluginManagerInterface $workflow_type_manager) {
parent::__construct($entity_type);
$this->workflowTypeManager = $workflow_type_manager;
}
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
/** @var \Drupal\workflows\Entity\Workflow $entity */
$workflow_type = $entity->getTypePlugin();
if (str_starts_with($operation, 'delete-state')) {
[, $state_id] = explode(':', $operation, 2);
// Deleting a state is editing a workflow, but also we should forbid
// access if there is only one state.
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);
}
return parent::checkAccess($entity, $operation, $account);
}
/**
* {@inheritdoc}
*/
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
$workflow_types_count = count($this->workflowTypeManager->getDefinitions());
$admin_access = parent::checkCreateAccess($account, $context, $entity_bundle);
// Allow access if there is at least one workflow type. Since workflow types
// are provided by modules this is cacheable until extensions change.
return $admin_access
->andIf(AccessResult::allowedIf($workflow_types_count > 0));
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Drupal\workflows;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Provides an interface for defining workflow entities.
*/
interface WorkflowInterface extends ConfigEntityInterface {
/**
* Gets the workflow type plugin.
*
* @return \Drupal\workflows\WorkflowTypeInterface
* The workflow type plugin.
*/
public function getTypePlugin();
}

View File

@@ -0,0 +1,102 @@
<?php
namespace Drupal\workflows;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a listing of Workflow entities.
*/
class WorkflowListBuilder extends ConfigEntityListBuilder {
/**
* The workflow type plugin manager.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $workflowTypeManager;
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity_type.manager')->getStorage($entity_type->id()),
$container->get('plugin.manager.workflows.type')
);
}
/**
* Constructs a new WorkflowListBuilder object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
* The entity storage class.
* @param \Drupal\Component\Plugin\PluginManagerInterface $workflow_type_manager
* The workflow type plugin manager.
*/
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, PluginManagerInterface $workflow_type_manager) {
parent::__construct($entity_type, $storage);
$this->workflowTypeManager = $workflow_type_manager;
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'workflow_admin_overview_form';
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['label'] = $this->t('Workflow');
$header['type'] = $this->t('Type');
$header['states'] = $this->t('States');
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
/** @var \Drupal\workflows\WorkflowInterface $entity */
$row['label'] = $entity->label();
$row['type']['data'] = [
'#markup' => $entity->getTypePlugin()->label(),
];
$items = array_map([State::class, 'labelCallback'], $entity->getTypePlugin()->getStates());
$row['states']['data'] = [
'#theme' => 'item_list',
'#context' => ['list_style' => 'comma-list'],
'#items' => $items,
];
return $row + parent::buildRow($entity);
}
/**
* {@inheritdoc}
*/
public function render() {
$build = parent::render();
$workflow_types_count = count($this->workflowTypeManager->getDefinitions());
if ($workflow_types_count === 0) {
$build['table']['#empty'] = $this->t('There are no workflow types available. In order to create workflows you need to install a module that provides a workflow type. For example, the <a href=":content-moderation">Content Moderation</a> module provides a workflow type that enables workflows for content entities.', [':content-moderation' => Url::fromRoute('system.modules_list', [], ['fragment' => 'module-content-moderation'])->toString()]);
}
return $build;
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Drupal\workflows;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Provides an access check for state and transition operations.
*/
class WorkflowStateTransitionOperationsAccessCheck implements AccessInterface {
/**
* Checks access for operations of workflow states and transitions.
*
* The value of '_workflow_access' is used to check to kind of access that
* should be applied to a route in the context of a workflow and a state or
* transition. States and transitions can individually have access control
* applied to them for 'add', 'update' and 'delete'. By default workflows will
* use the admin permission 'administer workflows' for all of these
* operations, except for delete-state which checks there is at least one
* state, a state does not have data and it's not a required state.
*
* For the update and delete operations, a workflow and a state or transition
* is required in the route for the access check to be applied. For the "add"
* operation, only a workflow is required. The '_workflow_access' requirement
* translates into access checks on the workflow entity type in the formats:
* - @code"$operation-state:$state_id"@endcode
* - @code"$operation-transition:$transition_id"@endcode
*
* For example the following route definition with the path
* "/test-workflow/foo-state/delete" the 'delete-state:foo-state' operation
* will be checked:
* @code
* pattern: '/{workflow}/{workflow_state}/delete'
* requirements:
* _workflow_access: 'delete-state'
* @endcode
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The parametrized route
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
*
* @return \Drupal\Core\Access\AccessResultInterface
* An access result.
*
* @throws \Exception
* Throws an exception when a route is defined with an invalid operation.
*/
public function access(RouteMatchInterface $route_match, AccountInterface $account) {
$workflow_operation = $this->getOperation($route_match);
if (!preg_match('/^(?<operation>add|update|delete)-(?<type>state|transition)$/', $workflow_operation, $matches)) {
throw new \Exception("Invalid _workflow_access operation '$workflow_operation' specified for route '{$route_match->getRouteName()}'.");
}
$parameters = $route_match->getParameters();
$workflow = $parameters->get('workflow');
if ($workflow && $matches['operation'] === 'add') {
return $workflow->access($workflow_operation, $account, TRUE);
}
if ($workflow && $type = $parameters->get(sprintf('workflow_%s', $matches['type']))) {
return $workflow->access(sprintf('%s:%s', $workflow_operation, $type), $account, TRUE);
}
return AccessResult::neutral();
}
/**
* Get the operation that will be used for the access check.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The parametrized route
*
* @return string
* The access operation.
*/
protected function getOperation(RouteMatchInterface $route_match) {
return $route_match->getRouteObject()->getRequirement('_workflow_access');
}
}

View File

@@ -0,0 +1,352 @@
<?php
namespace Drupal\workflows;
use Drupal\Component\Plugin\ConfigurableInterface;
use Drupal\Component\Plugin\DependentPluginInterface;
use Drupal\Component\Plugin\DerivativeInspectionInterface;
use Drupal\Core\Plugin\PluginWithFormsInterface;
/**
* An interface for Workflow type plugins.
*/
interface WorkflowTypeInterface extends PluginWithFormsInterface, DerivativeInspectionInterface, ConfigurableInterface, DependentPluginInterface {
/**
* The key of the global workflow plugin form.
*/
const PLUGIN_FORM_KEY = 'configure';
/**
* Gets the label for the workflow type.
*
* @return string
* The workflow type label.
*/
public function label();
/**
* Determines if the workflow is being 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.
*
* @return bool
* TRUE if the workflow is being used, FALSE if not.
*/
public function workflowHasData(WorkflowInterface $workflow);
/**
* 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 workflow state to check.
*
* @return bool
* TRUE if the workflow state is being used, FALSE if not.
*/
public function workflowStateHasData(WorkflowInterface $workflow, StateInterface $state);
/**
* Gets the initial state for the workflow.
*
* @return \Drupal\workflows\StateInterface
* The initial state.
*/
public function getInitialState();
/**
* Gets the required states of workflow type.
*
* This is usually specified in the workflow type annotation.
*
* @return string[]
* The required states.
*
* @see \Drupal\workflows\Annotation\WorkflowType
*/
public function getRequiredStates();
/**
* Informs the plugin that a dependency of the workflow will be deleted.
*
* @param array $dependencies
* An array of dependencies that will be deleted keyed by dependency type.
*
* @return bool
* TRUE if the workflow settings have been changed, FALSE if not.
*
* @see \Drupal\Core\Config\ConfigEntityInterface::onDependencyRemoval()
*
* @todo https://www.drupal.org/node/2579743 make part of a generic interface.
*/
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 $this
* 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 transitions 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 \Drupal\workflows\TransitionInterface[]
* An array of the transition objects for the state in the given direction,
* keyed by transition ID.
*
* @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

@@ -0,0 +1,36 @@
<?php
namespace Drupal\workflows;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\workflows\Attribute\WorkflowType;
/**
* Provides a Workflow type plugin manager.
*
* @see \Drupal\workflows\Annotation\WorkflowType
* @see \Drupal\workflows\WorkflowTypeInterface
* @see plugin_api
*/
class WorkflowTypeManager extends DefaultPluginManager {
/**
* Constructs a new class instance.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/WorkflowType', $namespaces, $module_handler, WorkflowTypeInterface::class, WorkflowType::class, 'Drupal\workflows\Annotation\WorkflowType');
$this->alterInfo('workflow_type_info');
$this->setCacheBackend($cache_backend, 'workflow_type_info');
}
}