summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNathaniel Catchpole2016-12-13 12:34:20 (GMT)
committerNathaniel Catchpole2016-12-13 12:34:20 (GMT)
commit0f139055718b608edfac1422e89667c7a7bf172a (patch)
tree6fa01b63b568cb50845320cc4721bc94e6b9ffab
parente223ebe5f7c227f82c0c28f5538804501a746522 (diff)
Issue #2779647 by alexpott, Sam152, catch, scookie, yoroy, pericxc, timmillwood, tacituseu, jhedstrom, xjm, bojanz, tstoeckler: Add a workflow component, ui module, and implement it in content moderation
-rw-r--r--core/composer.json3
-rw-r--r--core/modules/content_moderation/config/install/content_moderation.state.archived.yml8
-rw-r--r--core/modules/content_moderation/config/install/content_moderation.state.draft.yml8
-rw-r--r--core/modules/content_moderation/config/install/content_moderation.state.published.yml8
-rw-r--r--core/modules/content_moderation/config/install/content_moderation.state_transition.archived_draft.yml11
-rw-r--r--core/modules/content_moderation/config/install/content_moderation.state_transition.archived_published.yml11
-rw-r--r--core/modules/content_moderation/config/install/content_moderation.state_transition.draft_draft.yml10
-rw-r--r--core/modules/content_moderation/config/install/content_moderation.state_transition.draft_published.yml11
-rw-r--r--core/modules/content_moderation/config/install/content_moderation.state_transition.published_archived.yml11
-rw-r--r--core/modules/content_moderation/config/install/content_moderation.state_transition.published_draft.yml11
-rw-r--r--core/modules/content_moderation/config/install/content_moderation.state_transition.published_published.yml10
-rw-r--r--core/modules/content_moderation/config/install/workflows.workflow.editorial.yml63
-rw-r--r--core/modules/content_moderation/config/schema/content_moderation.schema.yml94
-rw-r--r--core/modules/content_moderation/content_moderation.info.yml2
-rw-r--r--core/modules/content_moderation/content_moderation.links.action.yml11
-rw-r--r--core/modules/content_moderation/content_moderation.links.menu.yml21
-rw-r--r--core/modules/content_moderation/content_moderation.links.task.yml2
-rw-r--r--core/modules/content_moderation/content_moderation.module54
-rw-r--r--core/modules/content_moderation/content_moderation.permissions.yml17
-rw-r--r--core/modules/content_moderation/content_moderation.routing.yml7
-rw-r--r--core/modules/content_moderation/content_moderation.services.yml4
-rw-r--r--core/modules/content_moderation/src/ContentModerationState.php114
-rw-r--r--core/modules/content_moderation/src/Entity/ContentModerationState.php13
-rw-r--r--core/modules/content_moderation/src/Entity/Handler/NodeModerationHandler.php31
-rw-r--r--core/modules/content_moderation/src/Entity/ModerationState.php102
-rw-r--r--core/modules/content_moderation/src/Entity/ModerationStateTransition.php114
-rw-r--r--core/modules/content_moderation/src/EntityOperations.php54
-rw-r--r--core/modules/content_moderation/src/EntityTypeInfo.php40
-rw-r--r--core/modules/content_moderation/src/Form/BundleModerationConfigurationForm.php189
-rw-r--r--core/modules/content_moderation/src/Form/EntityModerationForm.php41
-rw-r--r--core/modules/content_moderation/src/Form/ModerationStateForm.php82
-rw-r--r--core/modules/content_moderation/src/Form/ModerationStateTransitionDeleteForm.php49
-rw-r--r--core/modules/content_moderation/src/Form/ModerationStateTransitionForm.php151
-rw-r--r--core/modules/content_moderation/src/ModerationInformation.php37
-rw-r--r--core/modules/content_moderation/src/ModerationInformationInterface.php11
-rw-r--r--core/modules/content_moderation/src/ModerationStateAccessControlHandler.php31
-rw-r--r--core/modules/content_moderation/src/ModerationStateInterface.php28
-rw-r--r--core/modules/content_moderation/src/ModerationStateListBuilder.php40
-rw-r--r--core/modules/content_moderation/src/ModerationStateTransitionInterface.php36
-rw-r--r--core/modules/content_moderation/src/ModerationStateTransitionListBuilder.php173
-rw-r--r--core/modules/content_moderation/src/Permissions.php27
-rw-r--r--core/modules/content_moderation/src/Plugin/Field/FieldFormatter/ContentModerationStateFormatter.php48
-rw-r--r--core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php58
-rw-r--r--core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php28
-rw-r--r--core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraintValidator.php25
-rw-r--r--core/modules/content_moderation/src/Plugin/WorkflowType/ContentModeration.php166
-rw-r--r--core/modules/content_moderation/src/Routing/EntityTypeModerationRouteProvider.php2
-rw-r--r--core/modules/content_moderation/src/StateTransitionValidation.php221
-rw-r--r--core/modules/content_moderation/src/StateTransitionValidationInterface.php47
-rw-r--r--core/modules/content_moderation/src/Tests/ModerationFormTest.php5
-rw-r--r--core/modules/content_moderation/src/Tests/ModerationLocaleTest.php26
-rw-r--r--core/modules/content_moderation/src/Tests/ModerationStateBlockTest.php7
-rw-r--r--core/modules/content_moderation/src/Tests/ModerationStateNodeTest.php67
-rw-r--r--core/modules/content_moderation/src/Tests/ModerationStateNodeTypeTest.php16
-rw-r--r--core/modules/content_moderation/src/Tests/ModerationStateStatesTest.php75
-rw-r--r--core/modules/content_moderation/src/Tests/ModerationStateTestBase.php68
-rw-r--r--core/modules/content_moderation/src/Tests/ModerationStateTransitionsTest.php91
-rw-r--r--core/modules/content_moderation/src/Tests/NodeAccessTest.php26
-rw-r--r--core/modules/content_moderation/src/ViewsData.php2
-rw-r--r--core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.latest.yml4
-rw-r--r--core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_base_table_test.yml11
-rw-r--r--core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_latest_revision.yml4
-rw-r--r--core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_revision_test.yml2
-rw-r--r--core/modules/content_moderation/tests/src/Functional/LatestRevisionViewsFilterTest.php20
-rw-r--r--core/modules/content_moderation/tests/src/Functional/ModerationStateAccessTest.php27
-rw-r--r--core/modules/content_moderation/tests/src/Kernel/ContentModerationSchemaTest.php89
-rw-r--r--core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php94
-rw-r--r--core/modules/content_moderation/tests/src/Kernel/EntityOperationsTest.php42
-rw-r--r--core/modules/content_moderation/tests/src/Kernel/EntityRevisionConverterTest.php10
-rw-r--r--core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php40
-rw-r--r--core/modules/content_moderation/tests/src/Kernel/ModerationStateEntityTest.php69
-rw-r--r--core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php13
-rw-r--r--core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php18
-rw-r--r--core/modules/content_moderation/tests/src/Unit/ModerationInformationTest.php83
-rw-r--r--core/modules/content_moderation/tests/src/Unit/StateTransitionValidationTest.php292
-rw-r--r--core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml16
-rw-r--r--core/modules/workflows/config/schema/workflows.schema.yml51
-rw-r--r--core/modules/workflows/src/Annotation/WorkflowType.php44
-rw-r--r--core/modules/workflows/src/Entity/Workflow.php506
-rw-r--r--core/modules/workflows/src/Form/WorkflowAddForm.php107
-rw-r--r--core/modules/workflows/src/Form/WorkflowDeleteForm.php (renamed from core/modules/content_moderation/src/Form/ModerationStateDeleteForm.php)12
-rw-r--r--core/modules/workflows/src/Form/WorkflowEditForm.php214
-rw-r--r--core/modules/workflows/src/Form/WorkflowStateAddForm.php115
-rw-r--r--core/modules/workflows/src/Form/WorkflowStateDeleteForm.php99
-rw-r--r--core/modules/workflows/src/Form/WorkflowStateEditForm.php167
-rw-r--r--core/modules/workflows/src/Form/WorkflowTransitionAddForm.php151
-rw-r--r--core/modules/workflows/src/Form/WorkflowTransitionDeleteForm.php102
-rw-r--r--core/modules/workflows/src/Form/WorkflowTransitionEditForm.php171
-rw-r--r--core/modules/workflows/src/Plugin/WorkflowTypeBase.php119
-rw-r--r--core/modules/workflows/src/State.php118
-rw-r--r--core/modules/workflows/src/StateInterface.php73
-rw-r--r--core/modules/workflows/src/Transition.php116
-rw-r--r--core/modules/workflows/src/TransitionInterface.php54
-rw-r--r--core/modules/workflows/src/WorkflowAccessControlHandler.php83
-rw-r--r--core/modules/workflows/src/WorkflowInterface.php289
-rw-r--r--core/modules/workflows/src/WorkflowListBuilder.php101
-rw-r--r--core/modules/workflows/src/WorkflowTypeInterface.php120
-rw-r--r--core/modules/workflows/src/WorkflowTypeManager.php40
-rw-r--r--core/modules/workflows/tests/modules/workflow_type_test/config/schema/workflow_type_test.schema.yml33
-rw-r--r--core/modules/workflows/tests/modules/workflow_type_test/src/DecoratedState.php90
-rw-r--r--core/modules/workflows/tests/modules/workflow_type_test/src/DecoratedTransition.php83
-rw-r--r--core/modules/workflows/tests/modules/workflow_type_test/src/Plugin/WorkflowType/ComplexTestType.php82
-rw-r--r--core/modules/workflows/tests/modules/workflow_type_test/src/Plugin/WorkflowType/TestType.php16
-rw-r--r--core/modules/workflows/tests/modules/workflow_type_test/workflow_type_test.info.yml8
-rw-r--r--core/modules/workflows/tests/src/Functional/WorkflowUiNoTypeTest.php54
-rw-r--r--core/modules/workflows/tests/src/Functional/WorkflowUiTest.php262
-rw-r--r--core/modules/workflows/tests/src/Kernel/ComplexWorkflowTypeTest.php55
-rw-r--r--core/modules/workflows/tests/src/Unit/StateTest.php131
-rw-r--r--core/modules/workflows/tests/src/Unit/TransitionTest.php71
-rw-r--r--core/modules/workflows/tests/src/Unit/WorkflowTest.php654
-rw-r--r--core/modules/workflows/workflows.info.yml7
-rw-r--r--core/modules/workflows/workflows.links.action.yml5
-rw-r--r--core/modules/workflows/workflows.links.menu.yml7
-rw-r--r--core/modules/workflows/workflows.module58
-rw-r--r--core/modules/workflows/workflows.permissions.yml4
-rw-r--r--core/modules/workflows/workflows.routing.yml47
-rw-r--r--core/modules/workflows/workflows.services.yml6
117 files changed, 5599 insertions, 2478 deletions
diff --git a/core/composer.json b/core/composer.json
index 99b5cd0..71f07d0 100644
--- a/core/composer.json
+++ b/core/composer.json
@@ -142,7 +142,8 @@
"drupal/update": "self.version",
"drupal/user": "self.version",
"drupal/views": "self.version",
- "drupal/views_ui": "self.version"
+ "drupal/views_ui": "self.version",
+ "drupal/workflows": "self.version"
},
"minimum-stability": "dev",
"prefer-stable": true,
diff --git a/core/modules/content_moderation/config/install/content_moderation.state.archived.yml b/core/modules/content_moderation/config/install/content_moderation.state.archived.yml
deleted file mode 100644
index 0279481..0000000
--- a/core/modules/content_moderation/config/install/content_moderation.state.archived.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-langcode: en
-status: true
-dependencies: { }
-id: archived
-label: Archived
-published: false
-default_revision: true
-weight: -8
diff --git a/core/modules/content_moderation/config/install/content_moderation.state.draft.yml b/core/modules/content_moderation/config/install/content_moderation.state.draft.yml
deleted file mode 100644
index c7eb64c..0000000
--- a/core/modules/content_moderation/config/install/content_moderation.state.draft.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-langcode: en
-status: true
-dependencies: { }
-id: draft
-label: Draft
-published: false
-default_revision: false
-weight: -10
diff --git a/core/modules/content_moderation/config/install/content_moderation.state.published.yml b/core/modules/content_moderation/config/install/content_moderation.state.published.yml
deleted file mode 100644
index 8467e86..0000000
--- a/core/modules/content_moderation/config/install/content_moderation.state.published.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-langcode: en
-status: true
-dependencies: { }
-id: published
-label: Published
-published: true
-default_revision: true
-weight: -9
diff --git a/core/modules/content_moderation/config/install/content_moderation.state_transition.archived_draft.yml b/core/modules/content_moderation/config/install/content_moderation.state_transition.archived_draft.yml
deleted file mode 100644
index 8fbf9c3..0000000
--- a/core/modules/content_moderation/config/install/content_moderation.state_transition.archived_draft.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-langcode: en
-status: true
-dependencies:
- config:
- - content_moderation.state.archived
- - content_moderation.state.draft
-id: archived_draft
-label: 'Un-archive to Draft'
-stateFrom: archived
-stateTo: draft
-weight: -5
diff --git a/core/modules/content_moderation/config/install/content_moderation.state_transition.archived_published.yml b/core/modules/content_moderation/config/install/content_moderation.state_transition.archived_published.yml
deleted file mode 100644
index 4be7600..0000000
--- a/core/modules/content_moderation/config/install/content_moderation.state_transition.archived_published.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-langcode: en
-status: true
-dependencies:
- config:
- - content_moderation.state.archived
- - content_moderation.state.published
-id: archived_published
-label: 'Un-archive'
-stateFrom: archived
-stateTo: published
-weight: -4
diff --git a/core/modules/content_moderation/config/install/content_moderation.state_transition.draft_draft.yml b/core/modules/content_moderation/config/install/content_moderation.state_transition.draft_draft.yml
deleted file mode 100644
index 0ba0f34..0000000
--- a/core/modules/content_moderation/config/install/content_moderation.state_transition.draft_draft.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-langcode: en
-status: true
-dependencies:
- config:
- - content_moderation.state.draft
-id: draft_draft
-label: 'Create New Draft'
-stateFrom: draft
-stateTo: draft
-weight: -10
diff --git a/core/modules/content_moderation/config/install/content_moderation.state_transition.draft_published.yml b/core/modules/content_moderation/config/install/content_moderation.state_transition.draft_published.yml
deleted file mode 100644
index cf95d3d..0000000
--- a/core/modules/content_moderation/config/install/content_moderation.state_transition.draft_published.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-langcode: en
-status: true
-dependencies:
- config:
- - content_moderation.state.draft
- - content_moderation.state.published
-id: draft_published
-label: 'Publish'
-stateFrom: draft
-stateTo: published
-weight: -9
diff --git a/core/modules/content_moderation/config/install/content_moderation.state_transition.published_archived.yml b/core/modules/content_moderation/config/install/content_moderation.state_transition.published_archived.yml
deleted file mode 100644
index f3a866a..0000000
--- a/core/modules/content_moderation/config/install/content_moderation.state_transition.published_archived.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-langcode: en
-status: true
-dependencies:
- config:
- - content_moderation.state.archived
- - content_moderation.state.published
-id: published_archived
-label: 'Archive'
-stateFrom: published
-stateTo: archived
-weight: -6
diff --git a/core/modules/content_moderation/config/install/content_moderation.state_transition.published_draft.yml b/core/modules/content_moderation/config/install/content_moderation.state_transition.published_draft.yml
deleted file mode 100644
index bd25a31..0000000
--- a/core/modules/content_moderation/config/install/content_moderation.state_transition.published_draft.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-langcode: en
-status: true
-dependencies:
- config:
- - content_moderation.state.draft
- - content_moderation.state.published
-id: published_draft
-label: 'Create New Draft'
-stateFrom: published
-stateTo: draft
-weight: -8
diff --git a/core/modules/content_moderation/config/install/content_moderation.state_transition.published_published.yml b/core/modules/content_moderation/config/install/content_moderation.state_transition.published_published.yml
deleted file mode 100644
index 3c09a85..0000000
--- a/core/modules/content_moderation/config/install/content_moderation.state_transition.published_published.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-langcode: en
-status: true
-dependencies:
- config:
- - content_moderation.state.published
-id: published_published
-label: 'Publish'
-stateFrom: published
-stateTo: published
-weight: -7
diff --git a/core/modules/content_moderation/config/install/workflows.workflow.editorial.yml b/core/modules/content_moderation/config/install/workflows.workflow.editorial.yml
new file mode 100644
index 0000000..d0243b1
--- /dev/null
+++ b/core/modules/content_moderation/config/install/workflows.workflow.editorial.yml
@@ -0,0 +1,63 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - content_moderation
+id: editorial
+label: 'Editorial workflow'
+states:
+ archived:
+ label: Archived
+ weight: 5
+ draft:
+ label: Draft
+ weight: -5
+ published:
+ label: Published
+ weight: 0
+transitions:
+ archive:
+ label: Archive
+ from:
+ - published
+ to: archived
+ weight: 2
+ archived_draft:
+ label: 'Un-archive to Draft'
+ from:
+ - archived
+ to: draft
+ weight: 3
+ archived_published:
+ label: Un-archive
+ from:
+ - archived
+ to: published
+ weight: 4
+ create_new_draft:
+ label: 'Create New Draft'
+ from:
+ - draft
+ - published
+ to: draft
+ weight: 0
+ publish:
+ label: Publish
+ from:
+ - draft
+ - published
+ to: published
+ weight: 1
+type: content_moderation
+type_settings:
+ states:
+ archived:
+ published: false
+ default_revision: true
+ draft:
+ published: false
+ default_revision: false
+ published:
+ published: true
+ default_revision: true
+ entity_types: { }
diff --git a/core/modules/content_moderation/config/schema/content_moderation.schema.yml b/core/modules/content_moderation/config/schema/content_moderation.schema.yml
index 7f9e8fd..b791b67 100644
--- a/core/modules/content_moderation/config/schema/content_moderation.schema.yml
+++ b/core/modules/content_moderation/config/schema/content_moderation.schema.yml
@@ -1,79 +1,33 @@
-content_moderation.state.*:
- type: config_entity
- label: 'Moderation state config'
- mapping:
- id:
- type: string
- label: 'ID'
- label:
- type: label
- label: 'Label'
- published:
- type: boolean
- label: 'Is published'
- default_revision:
- type: boolean
- label: 'Is default revision'
- weight:
- type: integer
- label: 'Weight'
-
-content_moderation.state_transition.*:
- type: config_entity
- label: 'Moderation state transition config'
+views.filter.latest_revision:
+ type: views_filter
+ label: 'Latest revision'
mapping:
- id:
- type: string
- label: 'ID'
- label:
- type: label
- label: 'Label'
- stateFrom:
- type: string
- label: 'From state'
- stateTo:
+ value:
type: string
- label: 'To state'
- weight:
- type: integer
- label: 'Weight'
+ label: 'Value'
-node.type.*.third_party.content_moderation:
+workflow.type_settings.content_moderation:
type: mapping
- label: 'Enable moderation states for this node type'
mapping:
- enabled:
- type: boolean
- label: 'Moderation states enabled'
- allowed_moderation_states:
+ states:
type: sequence
+ label: 'Additional state configuration for content moderation'
sequence:
- type: string
- label: 'Moderation state'
- default_moderation_state:
- type: string
- label: 'Moderation state for new content'
-
-block_content.type.*.third_party.content_moderation:
- type: mapping
- label: 'Enable moderation states for this block content type'
- mapping:
- enabled:
- type: boolean
- label: 'Moderation states enabled'
- allowed_moderation_states:
+ type: mapping
+ label: 'States'
+ mapping:
+ published:
+ type: boolean
+ label: 'Is published'
+ default_revision:
+ type: boolean
+ label: 'Is default revision'
+ entity_types:
type: sequence
+ label: 'Entity types'
sequence:
- type: string
- label: 'Moderation state'
- default_moderation_state:
- type: string
- label: 'Moderation state for new block content'
-
-views.filter.latest_revision:
- type: views_filter
- label: 'Latest revision'
- mapping:
- value:
- type: string
- label: 'Value'
+ type: sequence
+ label: 'Bundles'
+ sequence:
+ type: string
+ label: 'Bundle ID'
diff --git a/core/modules/content_moderation/content_moderation.info.yml b/core/modules/content_moderation/content_moderation.info.yml
index 6d92b64..ca53b59 100644
--- a/core/modules/content_moderation/content_moderation.info.yml
+++ b/core/modules/content_moderation/content_moderation.info.yml
@@ -5,3 +5,5 @@ version: VERSION
core: 8.x
package: Core (Experimental)
configure: content_moderation.overview
+dependencies:
+ - workflows
diff --git a/core/modules/content_moderation/content_moderation.links.action.yml b/core/modules/content_moderation/content_moderation.links.action.yml
deleted file mode 100644
index cbb2d3f..0000000
--- a/core/modules/content_moderation/content_moderation.links.action.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-entity.moderation_state.add_form:
- route_name: 'entity.moderation_state.add_form'
- title: 'Add moderation state'
- appears_on:
- - entity.moderation_state.collection
-
-entity.moderation_state_transition.add_form:
- route_name: 'entity.moderation_state_transition.add_form'
- title: 'Add moderation state transition'
- appears_on:
- - entity.moderation_state_transition.collection
diff --git a/core/modules/content_moderation/content_moderation.links.menu.yml b/core/modules/content_moderation/content_moderation.links.menu.yml
deleted file mode 100644
index 0fcb3eb..0000000
--- a/core/modules/content_moderation/content_moderation.links.menu.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-# Moderation state menu items definition
-content_moderation.overview:
- title: 'Content moderation'
- route_name: content_moderation.overview
- description: 'Configure states and transitions for entities.'
- parent: system.admin_config_workflow
-
-entity.moderation_state.collection:
- title: 'Moderation states'
- route_name: entity.moderation_state.collection
- description: 'Administer moderation states.'
- parent: content_moderation.overview
- weight: 10
-
-# Moderation state transition menu items definition
-entity.moderation_state_transition.collection:
- title: 'Moderation state transitions'
- route_name: entity.moderation_state_transition.collection
- description: 'Administer moderation states transitions.'
- parent: content_moderation.overview
- weight: 20
diff --git a/core/modules/content_moderation/content_moderation.links.task.yml b/core/modules/content_moderation/content_moderation.links.task.yml
index d715219..f92e92e 100644
--- a/core/modules/content_moderation/content_moderation.links.task.yml
+++ b/core/modules/content_moderation/content_moderation.links.task.yml
@@ -1,3 +1,3 @@
-moderation_state.entities:
+content_moderation.workflows:
deriver: 'Drupal\content_moderation\Plugin\Derivative\DynamicLocalTasks'
weight: 100
diff --git a/core/modules/content_moderation/content_moderation.module b/core/modules/content_moderation/content_moderation.module
index 582242b..07ef7c0 100644
--- a/core/modules/content_moderation/content_moderation.module
+++ b/core/modules/content_moderation/content_moderation.module
@@ -18,9 +18,11 @@ use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
+use Drupal\workflows\WorkflowInterface;
use Drupal\node\NodeInterface;
use Drupal\node\Plugin\Action\PublishNode;
use Drupal\node\Plugin\Action\UnpublishNode;
+use Drupal\workflows\Entity\Workflow;
/**
* Implements hook_help().
@@ -31,15 +33,13 @@ function content_moderation_help($route_name, RouteMatchInterface $route_match)
case 'help.page.content_moderation':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
- $output .= '<p>' . t('The Content Moderation module provides basic moderation for content. This lets site admins define states for content, and then define transitions between those states. For more information, see the <a href=":content_moderation">online documentation for the Content Moderation module</a>.', [':content_moderation' => 'https://www.drupal.org/documentation/modules/content_moderation']) . '</p>';
+ $output .= '<p>' . t('The Content Moderation module provides moderation for content by applying workflows to content. For more information, see the <a href=":content_moderation">online documentation for the Content Moderation module</a>.', [':content_moderation' => 'https://www.drupal.org/documentation/modules/content_moderation']) . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
- $output .= '<dt>' . t('Moderation states') . '</dt>';
- $output .= '<dd>' . t('Moderation states provide the <em>Draft</em> and <em>Archived</em> states as additions to the basic <em>Published</em> option. You can click the blue <em>Add Moderation state</em> button and create new states.') . '</dd>';
- $output .= '<dt>' . t('Moderation state transitions') . '</dt>';
- $output .= '<dd>' . t('Using the "Moderation state transitions" screen, you can create the actual workflow. You decide the direction in which content moves from state to state, and which user roles are allowed to make that move.') . '</dd>';
+ $output .= '<dt>' . t('Configuring workflows') . '</dt>';
+ $output .= '<dd>' . t('Enable the Workflow UI module to create, edit and delete content moderation workflows.') . '</p>';
$output .= '<dt>' . t('Configure Content Moderation permissions') . '</dt>';
- $output .= '<dd>' . t('Each state is exposed as a permission. If a user has the permission for a transition, then they can move that node from the start state to the end state') . '</p>';
+ $output .= '<dd>' . t('Each transition is exposed as a permission. If a user has the permission for a transition, then they can move that node from the start state to the end state') . '</p>';
$output .= '</dl>';
return $output;
}
@@ -182,15 +182,17 @@ function content_moderation_node_access(NodeInterface $node, $operation, Account
$access_result->addCacheableDependency($node);
}
- elseif ($operation === 'update' && $moderation_info->isModeratedEntity($node) && $node->moderation_state && $node->moderation_state->target_id) {
+ elseif ($operation === 'update' && $moderation_info->isModeratedEntity($node) && $node->moderation_state) {
/** @var \Drupal\content_moderation\StateTransitionValidation $transition_validation */
$transition_validation = \Drupal::service('content_moderation.state_transition_validation');
- $valid_transition_targets = $transition_validation->getValidTransitionTargets($node, $account);
+ $valid_transition_targets = $transition_validation->getValidTransitions($node, $account);
$access_result = $valid_transition_targets ? AccessResult::neutral() : AccessResult::forbidden();
$access_result->addCacheableDependency($node);
$access_result->addCacheableDependency($account);
+ $workflow = \Drupal::service('content_moderation.moderation_information')->getWorkflowForEntity($node);
+ $access_result->addCacheableDependency($workflow);
foreach ($valid_transition_targets as $valid_transition_target) {
$access_result->addCacheableDependency($valid_transition_target);
}
@@ -222,3 +224,39 @@ function content_moderation_action_info_alter(&$definitions) {
$definitions['node_unpublish_action']['class'] = ModerationOptOutUnpublishNode::class;
}
}
+
+/**
+ * Implements hook_entity_bundle_info_alter().
+ */
+function content_moderation_entity_bundle_info_alter(&$bundles) {
+ /** @var \Drupal\workflows\WorkflowInterface $workflow */
+ foreach (Workflow::loadMultipleByType('content_moderation') as $workflow) {
+ /** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration $plugin */
+ $plugin = $workflow->getTypePlugin();
+ foreach ($plugin->getEntityTypes() as $entity_type_id) {
+ foreach ($plugin->getBundlesForEntityType($entity_type_id) as $bundle_id) {
+ if (isset($bundles[$entity_type_id][$bundle_id])) {
+ $bundles[$entity_type_id][$bundle_id]['workflow'] = $workflow->id();
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_insert().
+ */
+function content_moderation_workflow_insert(WorkflowInterface $entity) {
+ // Clear bundle cache so workflow gets added or removed from the bundle
+ // information.
+ \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
+ // Clear field cache so extra field is added or removed.
+ \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_update().
+ */
+function content_moderation_workflow_update(WorkflowInterface $entity) {
+ content_moderation_workflow_insert($entity);
+}
diff --git a/core/modules/content_moderation/content_moderation.permissions.yml b/core/modules/content_moderation/content_moderation.permissions.yml
index 293a77d..af28bbf 100644
--- a/core/modules/content_moderation/content_moderation.permissions.yml
+++ b/core/modules/content_moderation/content_moderation.permissions.yml
@@ -2,18 +2,13 @@ view any unpublished content:
title: 'View any unpublished content'
description: 'This permission is necessary for any users that may moderate content.'
-'view moderation states':
- title: 'View moderation states'
- description: 'View moderation states.'
+'view content moderation':
+ title: 'View content moderation'
+ description: 'View content moderation.'
-'administer moderation states':
- title: 'Administer moderation states'
- description: 'Create and edit moderation states.'
- 'restrict access': TRUE
-
-'administer moderation state transitions':
- title: 'Administer content moderation state transitions'
- description: 'Create and edit content moderation state transitions.'
+'administer content moderation':
+ title: 'Administer content moderation'
+ description: 'Administer workflows on content entities.'
'restrict access': TRUE
view latest version:
diff --git a/core/modules/content_moderation/content_moderation.routing.yml b/core/modules/content_moderation/content_moderation.routing.yml
deleted file mode 100644
index 912eed8..0000000
--- a/core/modules/content_moderation/content_moderation.routing.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-content_moderation.overview:
- path: '/admin/config/workflow/moderation'
- defaults:
- _controller: '\Drupal\system\Controller\SystemController::systemAdminMenuBlockPage'
- _title: 'Content moderation'
- requirements:
- _permission: 'access administration pages'
diff --git a/core/modules/content_moderation/content_moderation.services.yml b/core/modules/content_moderation/content_moderation.services.yml
index 75f0c64..904bc0d 100644
--- a/core/modules/content_moderation/content_moderation.services.yml
+++ b/core/modules/content_moderation/content_moderation.services.yml
@@ -6,10 +6,10 @@ services:
- { name: paramconverter, priority: 5 }
content_moderation.state_transition_validation:
class: \Drupal\content_moderation\StateTransitionValidation
- arguments: ['@entity_type.manager', '@entity.query']
+ arguments: ['@content_moderation.moderation_information']
content_moderation.moderation_information:
class: Drupal\content_moderation\ModerationInformation
- arguments: ['@entity_type.manager']
+ arguments: ['@entity_type.manager', '@entity_type.bundle.info']
access_check.latest_revision:
class: Drupal\content_moderation\Access\LatestRevisionCheck
arguments: ['@content_moderation.moderation_information']
diff --git a/core/modules/content_moderation/src/ContentModerationState.php b/core/modules/content_moderation/src/ContentModerationState.php
new file mode 100644
index 0000000..34262eb
--- /dev/null
+++ b/core/modules/content_moderation/src/ContentModerationState.php
@@ -0,0 +1,114 @@
+<?php
+
+namespace Drupal\content_moderation;
+
+use Drupal\workflows\StateInterface;
+
+/**
+ * A value object representing a workflow state for content moderation.
+ */
+class ContentModerationState implements StateInterface {
+
+ /**
+ * The vanilla state object from the Workflow module.
+ *
+ * @var \Drupal\workflows\StateInterface
+ */
+ protected $state;
+
+ /**
+ * If entities should be published if in this state.
+ *
+ * @var bool
+ */
+ protected $published;
+
+ /**
+ * If entities should be the default revision if in this state.
+ *
+ * @var bool
+ */
+ protected $defaultRevision;
+
+ /**
+ * ContentModerationState constructor.
+ *
+ * Decorates state objects to add methods to determine if an entity should be
+ * published or made the default revision.
+ *
+ * @param \Drupal\workflows\StateInterface $state
+ * The vanilla state object from the Workflow module.
+ * @param bool $published
+ * (optional) TRUE if entities should be published if in this state, FALSE
+ * if not. Defaults to FALSE.
+ * @param bool $default_revision
+ * (optional) TRUE if entities should be the default revision if in this
+ * state, FALSE if not. Defaults to FALSE.
+ */
+ public function __construct(StateInterface $state, $published = FALSE, $default_revision = FALSE) {
+ $this->state = $state;
+ $this->published = $published;
+ $this->defaultRevision = $default_revision;
+ }
+
+ /**
+ * Determines if entities should be published if in this state.
+ *
+ * @return bool
+ */
+ public function isPublishedState() {
+ return $this->published;
+ }
+
+ /**
+ * Determines if entities should be the default revision if in this state.
+ *
+ * @return bool
+ */
+ public function isDefaultRevisionState() {
+ return $this->defaultRevision;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function id() {
+ return $this->state->id();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function label() {
+ return $this->state->label();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function weight() {
+ return $this->state->weight();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function canTransitionTo($to_state_id) {
+ return $this->state->canTransitionTo($to_state_id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTransitionTo($to_state_id) {
+ return $this->state->getTransitionTo($to_state_id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTransitions() {
+ return $this->state->getTransitions();
+ }
+
+}
diff --git a/core/modules/content_moderation/src/Entity/ContentModerationState.php b/core/modules/content_moderation/src/Entity/ContentModerationState.php
index 978408f..d60dad7 100644
--- a/core/modules/content_moderation/src/Entity/ContentModerationState.php
+++ b/core/modules/content_moderation/src/Entity/ContentModerationState.php
@@ -55,10 +55,17 @@ class ContentModerationState extends ContentEntityBase implements ContentModerat
->setTranslatable(TRUE)
->setRevisionable(TRUE);
- $fields['moderation_state'] = BaseFieldDefinition::create('entity_reference')
+ $fields['workflow'] = BaseFieldDefinition::create('entity_reference')
+ ->setLabel(t('Workflow'))
+ ->setDescription(t('The workflow the moderation state is in.'))
+ ->setSetting('target_type', 'workflow')
+ ->setRequired(TRUE)
+ ->setTranslatable(TRUE)
+ ->setRevisionable(TRUE);
+
+ $fields['moderation_state'] = BaseFieldDefinition::create('string')
->setLabel(t('Moderation state'))
->setDescription(t('The moderation state of the referenced content.'))
- ->setSetting('target_type', 'moderation_state')
->setRequired(TRUE)
->setTranslatable(TRUE)
->setRevisionable(TRUE)
@@ -155,7 +162,7 @@ class ContentModerationState extends ContentEntityBase implements ContentModerat
if ($related_entity instanceof TranslatableInterface) {
$related_entity = $related_entity->getTranslation($this->activeLangcode);
}
- $related_entity->moderation_state->target_id = $this->moderation_state->target_id;
+ $related_entity->moderation_state = $this->moderation_state;
return $related_entity->save();
}
diff --git a/core/modules/content_moderation/src/Entity/Handler/NodeModerationHandler.php b/core/modules/content_moderation/src/Entity/Handler/NodeModerationHandler.php
index 83de187..247d352 100644
--- a/core/modules/content_moderation/src/Entity/Handler/NodeModerationHandler.php
+++ b/core/modules/content_moderation/src/Entity/Handler/NodeModerationHandler.php
@@ -2,8 +2,11 @@
namespace Drupal\content_moderation\Entity\Handler;
+use Drupal\content_moderation\ModerationInformationInterface;
use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Customizations for node entities.
@@ -11,6 +14,32 @@ use Drupal\Core\Form\FormStateInterface;
class NodeModerationHandler extends ModerationHandler {
/**
+ * The moderation information service.
+ *
+ * @var \Drupal\content_moderation\ModerationInformationInterface
+ */
+ protected $moderationInfo;
+
+ /**
+ * NodeModerationHandler constructor.
+ *
+ * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
+ * The moderation information service.
+ */
+ public function __construct(ModerationInformationInterface $moderation_info) {
+ $this->moderationInfo = $moderation_info;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+ return new static(
+ $container->get('content_moderation.moderation_information')
+ );
+ }
+
+ /**
* {@inheritdoc}
*/
public function onPresave(ContentEntityInterface $entity, $default_revision, $published_state) {
@@ -38,7 +67,7 @@ class NodeModerationHandler extends ModerationHandler {
/* @var \Drupal\node\Entity\NodeType $entity */
$entity = $form_state->getFormObject()->getEntity();
- if ($entity->getThirdPartySetting('content_moderation', 'enabled', FALSE)) {
+ if ($this->moderationInfo->getWorkFlowForEntity($entity)) {
// Force the revision checkbox on.
$form['workflow']['options']['#default_value']['revision'] = 'revision';
$form['workflow']['options']['revision']['#disabled'] = TRUE;
diff --git a/core/modules/content_moderation/src/Entity/ModerationState.php b/core/modules/content_moderation/src/Entity/ModerationState.php
deleted file mode 100644
index 379ff60..0000000
--- a/core/modules/content_moderation/src/Entity/ModerationState.php
+++ /dev/null
@@ -1,102 +0,0 @@
-<?php
-
-namespace Drupal\content_moderation\Entity;
-
-use Drupal\Core\Config\Entity\ConfigEntityBase;
-use Drupal\content_moderation\ModerationStateInterface;
-
-/**
- * Defines the Moderation state entity.
- *
- * @ConfigEntityType(
- * id = "moderation_state",
- * label = @Translation("Moderation state"),
- * handlers = {
- * "access" = "Drupal\content_moderation\ModerationStateAccessControlHandler",
- * "list_builder" = "Drupal\content_moderation\ModerationStateListBuilder",
- * "form" = {
- * "add" = "Drupal\content_moderation\Form\ModerationStateForm",
- * "edit" = "Drupal\content_moderation\Form\ModerationStateForm",
- * "delete" = "Drupal\content_moderation\Form\ModerationStateDeleteForm"
- * },
- * "route_provider" = {
- * "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider",
- * },
- * },
- * config_prefix = "state",
- * admin_permission = "administer moderation states",
- * entity_keys = {
- * "id" = "id",
- * "label" = "label",
- * "uuid" = "uuid",
- * "weight" = "weight",
- * },
- * links = {
- * "add-form" = "/admin/config/workflow/moderation/states/add",
- * "edit-form" = "/admin/config/workflow/moderation/states/{moderation_state}",
- * "delete-form" = "/admin/config/workflow/moderation/states/{moderation_state}/delete",
- * "collection" = "/admin/config/workflow/moderation/states"
- * },
- * config_export = {
- * "id",
- * "label",
- * "published",
- * "default_revision",
- * "weight",
- * },
- * )
- */
-class ModerationState extends ConfigEntityBase implements ModerationStateInterface {
-
- /**
- * The Moderation state ID.
- *
- * @var string
- */
- protected $id;
-
- /**
- * The Moderation state label.
- *
- * @var string
- */
- protected $label;
-
- /**
- * Whether this state represents a published node.
- *
- * @var bool
- */
- protected $published;
-
- /**
- * Relative weight of this state.
- *
- * @var int
- */
- protected $weight;
-
- /**
- * Whether this state represents a default revision of the node.
- *
- * If this is a published state, then this property is ignored.
- *
- * @var bool
- */
- protected $default_revision;
-
- /**
- * {@inheritdoc}
- */
- public function isPublishedState() {
- return $this->published;
- }
-
- /**
- * {@inheritdoc}
- */
- public function isDefaultRevisionState() {
- return $this->published || $this->default_revision;
- }
-
-}
diff --git a/core/modules/content_moderation/src/Entity/ModerationStateTransition.php b/core/modules/content_moderation/src/Entity/ModerationStateTransition.php
deleted file mode 100644
index 95a115b..0000000
--- a/core/modules/content_moderation/src/Entity/ModerationStateTransition.php
+++ /dev/null
@@ -1,114 +0,0 @@
-<?php
-
-namespace Drupal\content_moderation\Entity;
-
-use Drupal\Core\Config\Entity\ConfigEntityBase;
-use Drupal\content_moderation\ModerationStateTransitionInterface;
-
-/**
- * Defines the Moderation state transition entity.
- *
- * @ConfigEntityType(
- * id = "moderation_state_transition",
- * label = @Translation("Moderation state transition"),
- * handlers = {
- * "list_builder" = "Drupal\content_moderation\ModerationStateTransitionListBuilder",
- * "form" = {
- * "add" = "Drupal\content_moderation\Form\ModerationStateTransitionForm",
- * "edit" = "Drupal\content_moderation\Form\ModerationStateTransitionForm",
- * "delete" = "Drupal\content_moderation\Form\ModerationStateTransitionDeleteForm"
- * },
- * "route_provider" = {
- * "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider",
- * },
- * },
- * config_prefix = "state_transition",
- * admin_permission = "administer moderation state transitions",
- * entity_keys = {
- * "id" = "id",
- * "label" = "label",
- * "uuid" = "uuid",
- * "weight" = "weight"
- * },
- * links = {
- * "add-form" = "/admin/config/workflow/moderation/transitions/add",
- * "edit-form" = "/admin/config/workflow/moderation/transitions/{moderation_state_transition}",
- * "delete-form" = "/admin/config/workflow/moderation/transitions/{moderation_state_transition}/delete",
- * "collection" = "/admin/config/workflow/moderation/transitions"
- * }
- * )
- */
-class ModerationStateTransition extends ConfigEntityBase implements ModerationStateTransitionInterface {
-
- /**
- * The Moderation state transition ID.
- *
- * @var string
- */
- protected $id;
-
- /**
- * The Moderation state transition label.
- *
- * @var string
- */
- protected $label;
-
- /**
- * ID of from state.
- *
- * @var string
- */
- protected $stateFrom;
-
- /**
- * ID of to state.
- *
- * @var string
- */
- protected $stateTo;
-
- /**
- * Relative weight of this transition.
- *
- * @var int
- */
- protected $weight;
-
- /**
- * {@inheritdoc}
- */
- public function calculateDependencies() {
- parent::calculateDependencies();
-
- if ($this->stateFrom) {
- $this->addDependency('config', ModerationState::load($this->stateFrom)->getConfigDependencyName());
- }
- if ($this->stateTo) {
- $this->addDependency('config', ModerationState::load($this->stateTo)->getConfigDependencyName());
- }
- return $this;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getFromState() {
- return $this->stateFrom;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getToState() {
- return $this->stateTo;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getWeight() {
- return $this->weight;
- }
-
-}
diff --git a/core/modules/content_moderation/src/EntityOperations.php b/core/modules/content_moderation/src/EntityOperations.php
index d76a35b..b4d637f 100644
--- a/core/modules/content_moderation/src/EntityOperations.php
+++ b/core/modules/content_moderation/src/EntityOperations.php
@@ -2,14 +2,16 @@
namespace Drupal\content_moderation;
-use Drupal\content_moderation\Entity\ContentModerationState;
+use Drupal\content_moderation\Entity\ContentModerationState as ContentModerationStateEntity;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\TypedData\TranslatableInterface;
use Drupal\content_moderation\Form\EntityModerationForm;
+use Drupal\workflows\WorkflowInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -46,6 +48,13 @@ class EntityOperations implements ContainerInjectionInterface {
protected $tracker;
/**
+ * The entity bundle information service.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
+ */
+ protected $bundleInfo;
+
+ /**
* Constructs a new EntityOperations object.
*
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
@@ -56,12 +65,15 @@ class EntityOperations implements ContainerInjectionInterface {
* The form builder.
* @param \Drupal\content_moderation\RevisionTrackerInterface $tracker
* The revision tracker.
+ * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
+ * The entity bundle information service.
*/
- public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, RevisionTrackerInterface $tracker) {
+ public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, RevisionTrackerInterface $tracker, EntityTypeBundleInfoInterface $bundle_info) {
$this->moderationInfo = $moderation_info;
$this->entityTypeManager = $entity_type_manager;
$this->formBuilder = $form_builder;
$this->tracker = $tracker;
+ $this->bundleInfo = $bundle_info;
}
/**
@@ -72,7 +84,8 @@ class EntityOperations implements ContainerInjectionInterface {
$container->get('content_moderation.moderation_information'),
$container->get('entity_type.manager'),
$container->get('form_builder'),
- $container->get('content_moderation.revision_tracker')
+ $container->get('content_moderation.revision_tracker'),
+ $container->get('entity_type.bundle.info')
);
}
@@ -86,20 +99,20 @@ class EntityOperations implements ContainerInjectionInterface {
if (!$this->moderationInfo->isModeratedEntity($entity)) {
return;
}
- if ($entity->moderation_state->target_id) {
- $moderation_state = $this->entityTypeManager
- ->getStorage('moderation_state')
- ->load($entity->moderation_state->target_id);
- $published_state = $moderation_state->isPublishedState();
+
+ if ($entity->moderation_state->value) {
+ $workflow = $this->moderationInfo->getWorkFlowForEntity($entity);
+ /** @var \Drupal\content_moderation\ContentModerationState $current_state */
+ $current_state = $workflow->getState($entity->moderation_state->value);
// This entity is default if it is new, the default revision, or the
// default revision is not published.
$update_default_revision = $entity->isNew()
- || $moderation_state->isDefaultRevisionState()
- || !$this->isDefaultRevisionPublished($entity);
+ || $current_state->isDefaultRevisionState()
+ || !$this->isDefaultRevisionPublished($entity, $workflow);
// Fire per-entity-type logic for handling the save process.
- $this->entityTypeManager->getHandler($entity->getEntityTypeId(), 'moderation')->onPresave($entity, $update_default_revision, $published_state);
+ $this->entityTypeManager->getHandler($entity->getEntityTypeId(), 'moderation')->onPresave($entity, $update_default_revision, $current_state->isPublishedState());
}
}
@@ -140,15 +153,14 @@ class EntityOperations implements ContainerInjectionInterface {
* The entity to update or create a moderation state for.
*/
protected function updateOrCreateFromEntity(EntityInterface $entity) {
- $moderation_state = $entity->moderation_state->target_id;
+ $moderation_state = $entity->moderation_state->value;
+ $workflow = $this->moderationInfo->getWorkFlowForEntity($entity);
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
if (!$moderation_state) {
- $moderation_state = $this->entityTypeManager
- ->getStorage($entity->getEntityType()->getBundleEntityType())->load($entity->bundle())
- ->getThirdPartySetting('content_moderation', 'default_moderation_state');
+ $moderation_state = $workflow->getInitialState()->id();
}
- // @todo what if $entity->moderation_state->target_id is null at this point?
+ // @todo what if $entity->moderation_state is null at this point?
$entity_type_id = $entity->getEntityTypeId();
$entity_id = $entity->id();
$entity_revision_id = $entity->getRevisionId();
@@ -157,6 +169,7 @@ class EntityOperations implements ContainerInjectionInterface {
$entities = $storage->loadByProperties([
'content_entity_type_id' => $entity_type_id,
'content_entity_id' => $entity_id,
+ 'workflow' => $workflow->id(),
]);
/** @var \Drupal\content_moderation\ContentModerationStateInterface $content_moderation_state */
@@ -166,6 +179,7 @@ class EntityOperations implements ContainerInjectionInterface {
'content_entity_type_id' => $entity_type_id,
'content_entity_id' => $entity_id,
]);
+ $content_moderation_state->workflow->target_id = $workflow->id();
}
else {
// Create a new revision.
@@ -186,7 +200,7 @@ class EntityOperations implements ContainerInjectionInterface {
// Create the ContentModerationState entity for the inserted entity.
$content_moderation_state->set('content_entity_revision_id', $entity_revision_id);
$content_moderation_state->set('moderation_state', $moderation_state);
- ContentModerationState::updateOrCreateFromEntity($content_moderation_state);
+ ContentModerationStateEntity::updateOrCreateFromEntity($content_moderation_state);
}
/**
@@ -241,11 +255,13 @@ class EntityOperations implements ContainerInjectionInterface {
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity being saved.
+ * @param \Drupal\workflows\WorkflowInterface $workflow
+ * The workflow being applied to the entity.
*
* @return bool
* TRUE if the default revision is published. FALSE otherwise.
*/
- protected function isDefaultRevisionPublished(EntityInterface $entity) {
+ protected function isDefaultRevisionPublished(EntityInterface $entity, WorkflowInterface $workflow) {
$storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
$default_revision = $storage->load($entity->id());
@@ -260,7 +276,7 @@ class EntityOperations implements ContainerInjectionInterface {
$default_revision = $default_revision->getTranslation($entity->language()->getId());
}
- return $default_revision && $default_revision->moderation_state->entity->isPublishedState();
+ return $default_revision && $workflow->getState($default_revision->moderation_state->value)->isPublishedState();
}
}
diff --git a/core/modules/content_moderation/src/EntityTypeInfo.php b/core/modules/content_moderation/src/EntityTypeInfo.php
index c35919e..25abf6a 100644
--- a/core/modules/content_moderation/src/EntityTypeInfo.php
+++ b/core/modules/content_moderation/src/EntityTypeInfo.php
@@ -9,6 +9,7 @@ use Drupal\Core\Entity\ContentEntityFormInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\BaseFieldDefinition;
@@ -50,6 +51,13 @@ class EntityTypeInfo implements ContainerInjectionInterface {
protected $entityTypeManager;
/**
+ * The bundle information service.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
+ */
+ protected $bundleInfo;
+
+ /**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
@@ -77,11 +85,16 @@ class EntityTypeInfo implements ContainerInjectionInterface {
* The moderation information service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* Entity type manager.
+ * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
+ * Bundle information service.
+ * @param \Drupal\Core\Session\AccountInterface $current_user
+ * Current user.
*/
- public function __construct(TranslationInterface $translation, ModerationInformationInterface $moderation_information, EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user) {
+ public function __construct(TranslationInterface $translation, ModerationInformationInterface $moderation_information, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_info, AccountInterface $current_user) {
$this->stringTranslation = $translation;
$this->moderationInfo = $moderation_information;
$this->entityTypeManager = $entity_type_manager;
+ $this->bundleInfo = $bundle_info;
$this->currentUser = $current_user;
}
@@ -93,6 +106,7 @@ class EntityTypeInfo implements ContainerInjectionInterface {
$container->get('string_translation'),
$container->get('content_moderation.moderation_information'),
$container->get('entity_type.manager'),
+ $container->get('entity_type.bundle.info'),
$container->get('current_user')
);
}
@@ -196,7 +210,7 @@ class EntityTypeInfo implements ContainerInjectionInterface {
$operations = [];
$type = $entity->getEntityType();
$bundle_of = $type->getBundleOf();
- if ($this->currentUser->hasPermission('administer moderation states') && $bundle_of &&
+ if ($this->currentUser->hasPermission('administer content moderation') && $bundle_of &&
$this->moderationInfo->canModerateEntitiesOfEntityType($this->entityTypeManager->getDefinition($bundle_of))
) {
$operations['manage-moderation'] = [
@@ -262,16 +276,12 @@ class EntityTypeInfo implements ContainerInjectionInterface {
* - bundle: The machine name of a bundle, such as "page" or "article".
*/
protected function getModeratedBundles() {
- /** @var ConfigEntityTypeInterface $type */
- foreach ($this->filterNonRevisionableEntityTypes($this->entityTypeManager->getDefinitions()) as $type_name => $type) {
- $result = $this->entityTypeManager
- ->getStorage($type_name)
- ->getQuery()
- ->condition('third_party_settings.content_moderation.enabled', TRUE)
- ->execute();
-
- foreach ($result as $bundle_name) {
- yield ['entity' => $type->getBundleOf(), 'bundle' => $bundle_name];
+ $entity_types = array_filter($this->entityTypeManager->getDefinitions(), [$this->moderationInfo, 'canModerateEntitiesOfEntityType']);
+ foreach ($entity_types as $type_name => $type) {
+ foreach ($this->bundleInfo->getBundleInfo($type_name) as $bundle_id => $bundle) {
+ if ($this->moderationInfo->shouldModerateEntitiesOfBundle($type, $bundle_id)) {
+ yield ['entity' => $type_name, 'bundle' => $bundle_id];
+ }
}
}
}
@@ -291,9 +301,9 @@ class EntityTypeInfo implements ContainerInjectionInterface {
}
$fields = [];
- $fields['moderation_state'] = BaseFieldDefinition::create('entity_reference')
- ->setLabel($this->t('Moderation state'))
- ->setDescription($this->t('The moderation state of this piece of content.'))
+ $fields['moderation_state'] = BaseFieldDefinition::create('string')
+ ->setLabel(t('Moderation state'))
+ ->setDescription(t('The moderation state of this piece of content.'))
->setComputed(TRUE)
->setClass(ModerationStateFieldItemList::class)
->setSetting('target_type', 'moderation_state')
diff --git a/core/modules/content_moderation/src/Form/BundleModerationConfigurationForm.php b/core/modules/content_moderation/src/Form/BundleModerationConfigurationForm.php
index 88bbf7f..10b3d6a 100644
--- a/core/modules/content_moderation/src/Form/BundleModerationConfigurationForm.php
+++ b/core/modules/content_moderation/src/Form/BundleModerationConfigurationForm.php
@@ -2,12 +2,11 @@
namespace Drupal\content_moderation\Form;
-use Drupal\Core\Config\Entity\ThirdPartySettingsInterface;
+use Drupal\content_moderation\Plugin\WorkflowType\ContentModeration;
+use Drupal\workflows\WorkflowInterface;
use Drupal\Core\Entity\EntityForm;
-use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
-use Drupal\content_moderation\Entity\ModerationState;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -50,128 +49,59 @@ class BundleModerationConfigurationForm extends EntityForm {
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
- /* @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $bundle */
- $bundle = $form_state->getFormObject()->getEntity();
- $form['enable_moderation_state'] = [
- '#type' => 'checkbox',
- '#title' => $this->t('Enable moderation states.'),
- '#description' => $this->t('Content of this type must transition through moderation states in order to be published.'),
- '#default_value' => $bundle->getThirdPartySetting('content_moderation', 'enabled', FALSE),
- ];
-
- // Add a special message when moderation is being disabled.
- if ($bundle->getThirdPartySetting('content_moderation', 'enabled', FALSE)) {
- $form['enable_moderation_state_note'] = [
- '#type' => 'item',
- '#description' => $this->t('After disabling moderation, any existing forward drafts will be accessible via the "Revisions" tab.'),
- '#states' => [
- 'visible' => [
- ':input[name=enable_moderation_state]' => ['checked' => FALSE],
- ],
- ],
- ];
- }
-
- $states = $this->entityTypeManager->getStorage('moderation_state')->loadMultiple();
- $label = function(ModerationState $state) {
- return $state->label();
- };
-
- $options_published = array_map($label, array_filter($states, function(ModerationState $state) {
- return $state->isPublishedState();
+ /* @var \Drupal\Core\Config\Entity\ConfigEntityInterface $bundle */
+ $bundle = $this->getEntity();
+ $bundle_of_entity_type = $this->entityTypeManager->getDefinition($bundle->getEntityType()->getBundleOf());
+ /* @var \Drupal\workflows\WorkflowInterface[] $workflows */
+ $workflows = $this->entityTypeManager->getStorage('workflow')->loadMultiple();
+
+ $options = array_map(function (WorkflowInterface $workflow) {
+ return $workflow->label();
+ }, array_filter($workflows, function (WorkflowInterface $workflow) {
+ return $workflow->status() && $workflow->getTypePlugin() instanceof ContentModeration;
}));
- $options_unpublished = array_map($label, array_filter($states, function(ModerationState $state) {
- return !$state->isPublishedState();
- }));
-
- $form['allowed_moderation_states_unpublished'] = [
- '#type' => 'checkboxes',
- '#title' => $this->t('Allowed moderation states (Unpublished)'),
- '#description' => $this->t('The allowed unpublished moderation states this content-type can be assigned.'),
- '#default_value' => $bundle->getThirdPartySetting('content_moderation', 'allowed_moderation_states', array_keys($options_unpublished)),
- '#options' => $options_unpublished,
- '#required' => TRUE,
- '#states' => [
- 'visible' => [
- ':input[name=enable_moderation_state]' => ['checked' => TRUE],
- ],
- ],
+ $selected_workflow = array_reduce($workflows, function ($carry, WorkflowInterface $workflow) use ($bundle_of_entity_type, $bundle) {
+ $plugin = $workflow->getTypePlugin();
+ if ($plugin instanceof ContentModeration && $plugin->appliesToEntityTypeAndBundle($bundle_of_entity_type->id(), $bundle->id())) {
+ return $workflow->id();
+ }
+ return $carry;
+ });
+ $form['workflow'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Select the workflow to apply'),
+ '#default_value' => $selected_workflow,
+ '#options' => $options,
+ '#required' => FALSE,
+ '#empty_value' => '',
];
- $form['allowed_moderation_states_published'] = [
- '#type' => 'checkboxes',
- '#title' => $this->t('Allowed moderation states (Published)'),
- '#description' => $this->t('The allowed published moderation states this content-type can be assigned.'),
- '#default_value' => $bundle->getThirdPartySetting('content_moderation', 'allowed_moderation_states', array_keys($options_published)),
- '#options' => $options_published,
- '#required' => TRUE,
- '#states' => [
- 'visible' => [
- ':input[name=enable_moderation_state]' => ['checked' => TRUE],
- ],
- ],
+ $form['original_workflow'] = [
+ '#type' => 'value',
+ '#value' => $selected_workflow,
];
- // The key of the array needs to be a user-facing string so we have to fully
- // render the translatable string to a real string, or else PHP errors on an
- // object used as an array key.
- $options = [
- $this->t('Unpublished')->render() => $options_unpublished,
- $this->t('Published')->render() => $options_published,
+ $form['bundle'] = [
+ '#type' => 'value',
+ '#value' => $bundle->id(),
];
- $form['default_moderation_state'] = [
- '#type' => 'select',
- '#title' => $this->t('Default moderation state'),
- '#options' => $options,
- '#description' => $this->t('Select the moderation state for new content'),
- '#default_value' => $bundle->getThirdPartySetting('content_moderation', 'default_moderation_state', 'draft'),
- '#states' => [
- 'visible' => [
- ':input[name=enable_moderation_state]' => ['checked' => TRUE],
- ],
- ],
+ $form['entity_type'] = [
+ '#type' => 'value',
+ '#value' => $bundle_of_entity_type->id(),
];
- $form['#entity_builders'][] = '::formBuilderCallback';
- return parent::form($form, $form_state);
- }
-
- /**
- * Form builder callback.
- *
- * @todo This should be folded into the form method.
- *
- * @param string $entity_type_id
- * The entity type identifier.
- * @param \Drupal\Core\Entity\EntityInterface $bundle
- * The bundle entity updated with the submitted values.
- * @param array $form
- * The complete form array.
- * @param \Drupal\Core\Form\FormStateInterface $form_state
- * The current state of the form.
- */
- public function formBuilderCallback($entity_type_id, EntityInterface $bundle, &$form, FormStateInterface $form_state) {
- // @todo https://www.drupal.org/node/2779933 write a test for this.
- if ($bundle instanceof ThirdPartySettingsInterface) {
- $bundle->setThirdPartySetting('content_moderation', 'enabled', $form_state->getValue('enable_moderation_state'));
- $bundle->setThirdPartySetting('content_moderation', 'allowed_moderation_states', array_keys(array_filter($form_state->getValue('allowed_moderation_states_published') + $form_state->getValue('allowed_moderation_states_unpublished'))));
- $bundle->setThirdPartySetting('content_moderation', 'default_moderation_state', $form_state->getValue('default_moderation_state'));
+ // Add a special message when moderation is being disabled.
+ if ($selected_workflow) {
+ $form['enable_workflow_note'] = [
+ '#type' => 'item',
+ '#description' => $this->t('After disabling moderation, any existing forward drafts will be accessible via the "Revisions" tab.'),
+ '#access' => !empty($selected_workflow)
+ ];
}
- }
- /**
- * {@inheritdoc}
- */
- public function validateForm(array &$form, FormStateInterface $form_state) {
- if ($form_state->getValue('enable_moderation_state')) {
- $allowed = array_keys(array_filter($form_state->getValue('allowed_moderation_states_published') + $form_state->getValue('allowed_moderation_states_unpublished')));
-
- if (($default = $form_state->getValue('default_moderation_state')) && !in_array($default, $allowed, TRUE)) {
- $form_state->setErrorByName('default_moderation_state', $this->t('The default moderation state must be one of the allowed states.'));
- }
- }
+ return parent::form($form, $form_state);
}
/**
@@ -180,16 +110,33 @@ class BundleModerationConfigurationForm extends EntityForm {
public function submitForm(array &$form, FormStateInterface $form_state) {
// If moderation is enabled, revisions MUST be enabled as well. Otherwise we
// can't have forward revisions.
- if ($form_state->getValue('enable_moderation_state')) {
- /* @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $bundle */
- $bundle = $form_state->getFormObject()->getEntity();
+ drupal_set_message($this->t('Your settings have been saved.'));
+ }
- $this->entityTypeManager->getHandler($bundle->getEntityType()->getBundleOf(), 'moderation')->onBundleModerationConfigurationFormSubmit($bundle);
+ /**
+ * {@inheritdoc}
+ */
+ public function save(array $form, FormStateInterface $form_state) {
+ $entity_type_id = $form_state->getValue('entity_type');
+ $bundle_id = $form_state->getValue('bundle');
+ $new_workflow_id = $form_state->getValue('workflow');
+ $original_workflow_id = $form_state->getValue('original_workflow');
+ if ($new_workflow_id === $original_workflow_id) {
+ // Nothing to do.
+ return;
+ }
+ if ($original_workflow_id) {
+ /* @var \Drupal\workflows\WorkflowInterface $workflow */
+ $workflow = $this->entityTypeManager->getStorage('workflow')->load($original_workflow_id);
+ $workflow->getTypePlugin()->removeEntityTypeAndBundle($entity_type_id, $bundle_id);
+ $workflow->save();
+ }
+ if ($new_workflow_id) {
+ /* @var \Drupal\workflows\WorkflowInterface $workflow */
+ $workflow = $this->entityTypeManager->getStorage('workflow')->load($new_workflow_id);
+ $workflow->getTypePlugin()->addEntityTypeAndBundle($entity_type_id, $bundle_id);
+ $workflow->save();
}
-
- parent::submitForm($form, $form_state);
-
- drupal_set_message($this->t('Your settings have been saved.'));
}
}
diff --git a/core/modules/content_moderation/src/Form/EntityModerationForm.php b/core/modules/content_moderation/src/Form/EntityModerationForm.php
index 39baec0..6f9de1f 100644
--- a/core/modules/content_moderation/src/Form/EntityModerationForm.php
+++ b/core/modules/content_moderation/src/Form/EntityModerationForm.php
@@ -3,12 +3,11 @@
namespace Drupal\content_moderation\Form;
use Drupal\Core\Entity\ContentEntityInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
-use Drupal\content_moderation\Entity\ModerationStateTransition;
use Drupal\content_moderation\ModerationInformationInterface;
use Drupal\content_moderation\StateTransitionValidation;
+use Drupal\workflows\Transition;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -31,26 +30,16 @@ class EntityModerationForm extends FormBase {
protected $validation;
/**
- * The entity type manager.
- *
- * @var \Drupal\Core\Entity\EntityTypeManagerInterface
- */
- protected $entityTypeManager;
-
- /**
* EntityModerationForm constructor.
*
* @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
* The moderation information service.
* @param \Drupal\content_moderation\StateTransitionValidation $validation
* The moderation state transition validation service.
- * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
- * The entity type manager.
*/
- public function __construct(ModerationInformationInterface $moderation_info, StateTransitionValidation $validation, EntityTypeManagerInterface $entity_type_manager) {
+ public function __construct(ModerationInformationInterface $moderation_info, StateTransitionValidation $validation) {
$this->moderationInfo = $moderation_info;
$this->validation = $validation;
- $this->entityTypeManager = $entity_type_manager;
}
/**
@@ -59,8 +48,7 @@ class EntityModerationForm extends FormBase {
public static function create(ContainerInterface $container) {
return new static(
$container->get('content_moderation.moderation_information'),
- $container->get('content_moderation.state_transition_validation'),
- $container->get('entity_type.manager')
+ $container->get('content_moderation.state_transition_validation')
);
}
@@ -75,20 +63,21 @@ class EntityModerationForm extends FormBase {
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ContentEntityInterface $entity = NULL) {
- /** @var \Drupal\content_moderation\Entity\ModerationState $current_state */
- $current_state = $entity->moderation_state->entity;
+ $current_state = $entity->moderation_state->value;
+ $workflow = $this->moderationInfo->getWorkFlowForEntity($entity);
+ /** @var \Drupal\workflows\Transition[] $transitions */
$transitions = $this->validation->getValidTransitions($entity, $this->currentUser());
// Exclude self-transitions.
- $transitions = array_filter($transitions, function(ModerationStateTransition $transition) use ($current_state) {
- return $transition->getToState() != $current_state->id();
+ $transitions = array_filter($transitions, function(Transition $transition) use ($current_state) {
+ return $transition->to()->id() != $current_state;
});
$target_states = [];
- /** @var ModerationStateTransition $transition */
+
foreach ($transitions as $transition) {
- $target_states[$transition->getToState()] = $transition->label();
+ $target_states[$transition->to()->id()] = $transition->to()->label();
}
if (!count($target_states)) {
@@ -99,7 +88,7 @@ class EntityModerationForm extends FormBase {
$form['current'] = [
'#type' => 'item',
'#title' => $this->t('Status'),
- '#markup' => $current_state->label(),
+ '#markup' => $workflow->getState($current_state)->label(),
];
}
@@ -139,21 +128,19 @@ class EntityModerationForm extends FormBase {
// @todo should we just just be updating the content moderation state
// entity? That would prevent setting the revision log.
- $entity->moderation_state->target_id = $new_state;
+ $entity->set('moderation_state', $new_state);
$entity->revision_log = $form_state->getValue('revision_log');
$entity->save();
drupal_set_message($this->t('The moderation state has been updated.'));
- /** @var \Drupal\content_moderation\Entity\ModerationState $state */
- $state = $this->entityTypeManager->getStorage('moderation_state')->load($new_state);
-
+ $new_state = $this->moderationInfo->getWorkFlowForEntity($entity)->getState($new_state);
// The page we're on likely won't be visible if we just set the entity to
// the default state, as we hide that latest-revision tab if there is no
// forward revision. Redirect to the canonical URL instead, since that will
// still exist.
- if ($state->isDefaultRevisionState()) {
+ if ($new_state->isDefaultRevisionState()) {
$form_state->setRedirectUrl($entity->toUrl('canonical'));
}
}
diff --git a/core/modules/content_moderation/src/Form/ModerationStateForm.php b/core/modules/content_moderation/src/Form/ModerationStateForm.php
deleted file mode 100644
index 32d7a48..0000000
--- a/core/modules/content_moderation/src/Form/ModerationStateForm.php
+++ /dev/null
@@ -1,82 +0,0 @@
-<?php
-
-namespace Drupal\content_moderation\Form;
-
-use Drupal\content_moderation\Entity\ModerationState;
-use Drupal\Core\Entity\EntityForm;
-use Drupal\Core\Form\FormStateInterface;
-
-/**
- * Class ModerationStateForm.
- */
-class ModerationStateForm extends EntityForm {
-
- /**
- * {@inheritdoc}
- */
- public function form(array $form, FormStateInterface $form_state) {
- $form = parent::form($form, $form_state);
-
- /* @var \Drupal\content_moderation\ModerationStateInterface $moderation_state */
- $moderation_state = $this->entity;
- $form['label'] = array(
- '#type' => 'textfield',
- '#title' => $this->t('Label'),
- '#maxlength' => 255,
- '#default_value' => $moderation_state->label(),
- '#description' => $this->t('Label for the Moderation state.'),
- '#required' => TRUE,
- );
-
- $form['id'] = array(
- '#type' => 'machine_name',
- '#default_value' => $moderation_state->id(),
- '#machine_name' => array(
- 'exists' => [ModerationState::class, 'load'],
- ),
- '#disabled' => !$moderation_state->isNew(),
- );
-
- $form['published'] = [
- '#type' => 'checkbox',
- '#title' => $this->t('Published'),
- '#description' => $this->t('When content reaches this state it should be published.'),
- '#default_value' => $moderation_state->isPublishedState(),
- ];
-
- $form['default_revision'] = [
- '#type' => 'checkbox',
- '#title' => $this->t('Default revision'),
- '#description' => $this->t('When content reaches this state it should be made the default revision; this is implied for published states.'),
- '#default_value' => $moderation_state->isDefaultRevisionState(),
- // @todo Add form #state to force "make default" on when "published" is
- // on for a state.
- // @see https://www.drupal.org/node/2645614
- ];
-
- return $form;
- }
-
- /**
- * {@inheritdoc}
- */
- public function save(array $form, FormStateInterface $form_state) {
- $moderation_state = $this->entity;
- $status = $moderation_state->save();
-
- switch ($status) {
- case SAVED_NEW:
- drupal_set_message($this->t('Created the %label Moderation state.', [
- '%label' => $moderation_state->label(),
- ]));
- break;
-
- default:
- drupal_set_message($this->t('Saved the %label Moderation state.', [
- '%label' => $moderation_state->label(),
- ]));
- }
- $form_state->setRedirectUrl($moderation_state->toUrl('collection'));
- }
-
-}
diff --git a/core/modules/content_moderation/src/Form/ModerationStateTransitionDeleteForm.php b/core/modules/content_moderation/src/Form/ModerationStateTransitionDeleteForm.php
deleted file mode 100644
index f153f1f..0000000
--- a/core/modules/content_moderation/src/Form/ModerationStateTransitionDeleteForm.php
+++ /dev/null
@@ -1,49 +0,0 @@
-<?php
-
-namespace Drupal\content_moderation\Form;
-
-use Drupal\Core\Entity\EntityConfirmFormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Url;
-
-/**
- * Builds the form to delete Moderation state transition entities.
- */
-class ModerationStateTransitionDeleteForm extends EntityConfirmFormBase {
-
- /**
- * {@inheritdoc}
- */
- public function getQuestion() {
- return $this->t('Are you sure you want to delete %name?', array('%name' => $this->entity->label()));
- }
-
- /**
- * {@inheritdoc}
- */
- public function getCancelUrl() {
- return new Url('entity.moderation_state_transition.collection');
- }
-
- /**
- * {@inheritdoc}
- */
- public function getConfirmText() {
- return $this->t('Delete');
- }
-
- /**
- * {@inheritdoc}
- */
- public function submitForm(array &$form, FormStateInterface $form_state) {
- $this->entity->delete();
-
- drupal_set_message($this->t(
- 'Moderation transition %label deleted.',
- ['%label' => $this->entity->label()]
- ));
-
- $form_state->setRedirectUrl($this->getCancelUrl());
- }
-
-}
diff --git a/core/modules/content_moderation/src/Form/ModerationStateTransitionForm.php b/core/modules/content_moderation/src/Form/ModerationStateTransitionForm.php
deleted file mode 100644
index 8322c18..0000000
--- a/core/modules/content_moderation/src/Form/ModerationStateTransitionForm.php
+++ /dev/null
@@ -1,151 +0,0 @@
-<?php
-
-namespace Drupal\content_moderation\Form;
-
-use Drupal\Core\Entity\EntityForm;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\Entity\Query\QueryFactory;
-use Drupal\Core\Form\FormStateInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-
-/**
- * Class ModerationStateTransitionForm.
- *
- * @package Drupal\content_moderation\Form
- */
-class ModerationStateTransitionForm extends EntityForm {
-
- /**
- * The entity type manager.
- *
- * @var \Drupal\Core\Entity\EntityTypeManagerInterface
- */
- protected $entityTypeManager;
-
- /**
- * The entity query factory.
- *
- * @var \Drupal\Core\Entity\Query\QueryFactory
- */
- protected $queryFactory;
-
- /**
- * Constructs a new ModerationStateTransitionForm.
- *
- * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
- * The entity type manager.
- * @param \Drupal\Core\Entity\Query\QueryFactory $query_factory
- * The entity query factory.
- */
- public function __construct(EntityTypeManagerInterface $entity_type_manager, QueryFactory $query_factory) {
- $this->entityTypeManager = $entity_type_manager;
- $this->queryFactory = $query_factory;
- }
-
- /**
- * {@inheritdoc}
- */
- public static function create(ContainerInterface $container) {
- return new static($container->get('entity_type.manager'), $container->get('entity.query'));
- }
-
- /**
- * {@inheritdoc}
- */
- public function form(array $form, FormStateInterface $form_state) {
- $form = parent::form($form, $form_state);
-
- /* @var \Drupal\content_moderation\ModerationStateTransitionInterface $moderation_state_transition */
- $moderation_state_transition = $this->entity;
- $form['label'] = [
- '#type' => 'textfield',
- '#title' => $this->t('Label'),
- '#maxlength' => 255,
- '#default_value' => $moderation_state_transition->label(),
- '#description' => $this->t('Label for the Moderation state transition.'),
- '#required' => TRUE,
- ];
-
- $form['id'] = [
- '#type' => 'machine_name',
- '#default_value' => $moderation_state_transition->id(),
- '#machine_name' => [
- 'exists' => '\Drupal\content_moderation\Entity\ModerationStateTransition::load',
- ],
- '#disabled' => !$moderation_state_transition->isNew(),
- ];
-
- $options = [];
- foreach ($this->entityTypeManager->getStorage('moderation_state')
- ->loadMultiple() as $moderation_state) {
- $options[$moderation_state->id()] = $moderation_state->label();
- }
-
- $form['container'] = [
- '#type' => 'container',
- '#attributes' => [
- 'class' => ['container-inline'],
- ],
- ];
-
- $form['container']['stateFrom'] = [
- '#type' => 'select',
- '#title' => $this->t('Transition from'),
- '#options' => $options,
- '#required' => TRUE,
- '#empty_option' => $this->t('-- Select --'),
- '#default_value' => $moderation_state_transition->getFromState(),
- ];
-
- $form['container']['stateTo'] = [
- '#type' => 'select',
- '#options' => $options,
- '#required' => TRUE,
- '#title' => $this->t('Transition to'),
- '#empty_option' => $this->t('-- Select --'),
- '#default_value' => $moderation_state_transition->getToState(),
- ];
-
- // Make sure there's always at least a wide enough delta on weight to cover
- // the current value or the total number of transitions. That way we
- // never end up forcing a transition to change its weight needlessly.
- $num_transitions = $this->queryFactory->get('moderation_state_transition')
- ->count()
- ->execute();
- $delta = max(abs($moderation_state_transition->getWeight()), $num_transitions);
-
- $form['weight'] = [
- '#type' => 'weight',
- '#delta' => $delta,
- '#options' => $options,
- '#title' => $this->t('Weight'),
- '#default_value' => $moderation_state_transition->getWeight(),
- '#description' => $this->t('Orders the transitions in moderation forms and the administrative listing. Heavier items will sink and the lighter items will be positioned nearer the top.'),
- ];
-
- return $form;
- }
-
- /**
- * {@inheritdoc}
- */
- public function save(array $form, FormStateInterface $form_state) {
- $moderation_state_transition = $this->entity;
- $status = $moderation_state_transition->save();
-
- switch ($status) {
- case SAVED_NEW:
- drupal_set_message($this->t('Created the %label Moderation state transition.', [
- '%label' => $moderation_state_transition->label(),
- ]));
- break;
-
- default:
- drupal_set_message($this->t('Saved the %label Moderation state transition.', [
- '%label' => $moderation_state_transition->label(),
- ]));
- }
- $form_state->setRedirectUrl($moderation_state_transition->toUrl('collection'));
- }
-
-}
diff --git a/core/modules/content_moderation/src/ModerationInformation.php b/core/modules/content_moderation/src/ModerationInformation.php
index ed33902..e8ebf39 100644
--- a/core/modules/content_moderation/src/ModerationInformation.php
+++ b/core/modules/content_moderation/src/ModerationInformation.php
@@ -4,6 +4,7 @@ namespace Drupal\content_moderation;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
@@ -20,15 +21,23 @@ class ModerationInformation implements ModerationInformationInterface {
protected $entityTypeManager;
/**
+ * The bundle information service.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
+ */
+ protected $bundleInfo;
+
+ /**
* Creates a new ModerationInformation instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
- * @param \Drupal\Core\Session\AccountInterface $current_user
- * The current user.
+ * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
+ * The bundle information service.
*/
- public function __construct(EntityTypeManagerInterface $entity_type_manager) {
+ public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_info) {
$this->entityTypeManager = $entity_type_manager;
+ $this->bundleInfo = $bundle_info;
}
/**
@@ -54,10 +63,8 @@ class ModerationInformation implements ModerationInformationInterface {
*/
public function shouldModerateEntitiesOfBundle(EntityTypeInterface $entity_type, $bundle) {
if ($this->canModerateEntitiesOfEntityType($entity_type)) {
- $bundle_entity = $this->entityTypeManager->getStorage($entity_type->getBundleEntityType())->load($bundle);
- if ($bundle_entity) {
- return $bundle_entity->getThirdPartySetting('content_moderation', 'enabled', FALSE);
- }
+ $bundles = $this->bundleInfo->getBundleInfo($entity_type->id());
+ return isset($bundles[$bundle]['workflow']);
}
return FALSE;
}
@@ -123,10 +130,22 @@ class ModerationInformation implements ModerationInformationInterface {
* {@inheritdoc}
*/
public function isLiveRevision(ContentEntityInterface $entity) {
+ $workflow = $this->getWorkFlowForEntity($entity);
return $this->isLatestRevision($entity)
&& $entity->isDefaultRevision()
- && $entity->moderation_state->entity
- && $entity->moderation_state->entity->isPublishedState();
+ && $entity->moderation_state->value
+ && $workflow->getState($entity->moderation_state->value)->isPublishedState();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getWorkFlowForEntity(ContentEntityInterface $entity) {
+ $bundles = $this->bundleInfo->getBundleInfo($entity->getEntityTypeId());
+ if (isset($bundles[$entity->bundle()]['workflow'])) {
+ return $this->entityTypeManager->getStorage('workflow')->load($bundles[$entity->bundle()]['workflow']);
+ };
+ return NULL;
}
}
diff --git a/core/modules/content_moderation/src/ModerationInformationInterface.php b/core/modules/content_moderation/src/ModerationInformationInterface.php
index 95a658e..531057e 100644
--- a/core/modules/content_moderation/src/ModerationInformationInterface.php
+++ b/core/modules/content_moderation/src/ModerationInformationInterface.php
@@ -126,4 +126,15 @@ interface ModerationInformationInterface {
*/
public function isLiveRevision(ContentEntityInterface $entity);
+ /**
+ * Gets the workflow for the given content entity.
+ *
+ * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+ * The content entity to get the workflow for.
+ *
+ * @return \Drupal\workflows\WorkflowInterface|null
+ * The workflow entity. NULL if there is no workflow.
+ */
+ public function getWorkFlowForEntity(ContentEntityInterface $entity);
+
}
diff --git a/core/modules/content_moderation/src/ModerationStateAccessControlHandler.php b/core/modules/content_moderation/src/ModerationStateAccessControlHandler.php
deleted file mode 100644
index b2c86d7..0000000
--- a/core/modules/content_moderation/src/ModerationStateAccessControlHandler.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-
-namespace Drupal\content_moderation;
-
-use Drupal\Core\Entity\EntityAccessControlHandler;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\Core\Access\AccessResult;
-
-/**
- * Access controller for the Moderation State entity.
- *
- * @see \Drupal\workbench_moderation\Entity\ModerationState.
- */
-class ModerationStateAccessControlHandler extends EntityAccessControlHandler {
-
- /**
- * {@inheritdoc}
- */
- protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
- $admin_access = parent::checkAccess($entity, $operation, $account);
-
- // Allow view with other permission.
- if ($operation === 'view') {
- return AccessResult::allowedIfHasPermission($account, 'view moderation states')->orIf($admin_access);
- }
-
- return $admin_access;
- }
-
-}
diff --git a/core/modules/content_moderation/src/ModerationStateInterface.php b/core/modules/content_moderation/src/ModerationStateInterface.php
deleted file mode 100644
index 99f664f..0000000
--- a/core/modules/content_moderation/src/ModerationStateInterface.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-
-namespace Drupal\content_moderation;
-
-use Drupal\Core\Config\Entity\ConfigEntityInterface;
-
-/**
- * Provides an interface for defining Moderation state entities.
- */
-interface ModerationStateInterface extends ConfigEntityInterface {
-
- /**
- * Determines if content updated to this state should be published.
- *
- * @return bool
- * TRUE if content updated to this state should be published.
- */
- public function isPublishedState();
-
- /**
- * Determines if content updated to this state should be the default revision.
- *
- * @return bool
- * TRUE if content in this state should be the default revision.
- */
- public function isDefaultRevisionState();
-
-}
diff --git a/core/modules/content_moderation/src/ModerationStateListBuilder.php b/core/modules/content_moderation/src/ModerationStateListBuilder.php
deleted file mode 100644
index 05ba513..0000000
--- a/core/modules/content_moderation/src/ModerationStateListBuilder.php
+++ /dev/null
@@ -1,40 +0,0 @@
-<?php
-
-namespace Drupal\content_moderation;
-
-use Drupal\Core\Config\Entity\DraggableListBuilder;
-use Drupal\Core\Entity\EntityInterface;
-
-/**
- * Provides a listing of Moderation state entities.
- */
-class ModerationStateListBuilder extends DraggableListBuilder {
-
- /**
- * {@inheritdoc}
- */
- public function getFormId() {
- return 'moderation_state_admin_overview_form';
- }
-
- /**
- * {@inheritdoc}
- */
- public function buildHeader() {
- $header['label'] = $this->t('Moderation state');
- $header['id'] = $this->t('Machine name');
-
- return $header + parent::buildHeader();
- }
-
- /**
- * {@inheritdoc}
- */
- public function buildRow(EntityInterface $entity) {
- $row['label'] = $entity->label();
- $row['id']['#markup'] = $entity->id();
-
- return $row + parent::buildRow($entity);
- }
-
-}
diff --git a/core/modules/content_moderation/src/ModerationStateTransitionInterface.php b/core/modules/content_moderation/src/ModerationStateTransitionInterface.php
deleted file mode 100644
index 91b5b13..0000000
--- a/core/modules/content_moderation/src/ModerationStateTransitionInterface.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-
-namespace Drupal\content_moderation;
-
-use Drupal\Core\Config\Entity\ConfigEntityInterface;
-
-/**
- * Provides an interface for defining Moderation state transition entities.
- */
-interface ModerationStateTransitionInterface extends ConfigEntityInterface {
-
- /**
- * Gets the from state for the given transition.
- *
- * @return string
- * The moderation state ID for the from state.
- */
- public function getFromState();
-
- /**
- * Gets the to state for the given transition.
- *
- * @return string
- * The moderation state ID for the to state.
- */
- public function getToState();
-
- /**
- * Gets the weight for the given transition.
- *
- * @return int
- * The weight of this transition.
- */
- public function getWeight();
-
-}
diff --git a/core/modules/content_moderation/src/ModerationStateTransitionListBuilder.php b/core/modules/content_moderation/src/ModerationStateTransitionListBuilder.php
deleted file mode 100644
index 577283e..0000000
--- a/core/modules/content_moderation/src/ModerationStateTransitionListBuilder.php
+++ /dev/null
@@ -1,173 +0,0 @@
-<?php
-
-namespace Drupal\content_moderation;
-
-use Drupal\Core\Config\Entity\DraggableListBuilder;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Entity\EntityStorageInterface;
-use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\user\RoleStorageInterface;
-use Symfony\Component\DependencyInjection\ContainerInterface;
-
-/**
- * Provides a listing of Moderation state transition entities.
- */
-class ModerationStateTransitionListBuilder extends DraggableListBuilder {
-
- /**
- * Moderation state entity storage.
- *
- * @var \Drupal\Core\Entity\EntityStorageInterface
- */
- protected $stateStorage;
-
- /**
- * The role storage.
- *
- * @var \Drupal\user\RoleStorageInterface
- */
- protected $roleStorage;
-
- /**
- * {@inheritdoc}
- */
- public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
- return new static(
- $entity_type,
- $container->get('entity.manager')->getStorage($entity_type->id()),
- $container->get('entity.manager')->getStorage('moderation_state'),
- $container->get('entity.manager')->getStorage('user_role')
- );
- }
-
- /**
- * Constructs a new ModerationStateTransitionListBuilder.
- *
- * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
- * Entity Type.
- * @param \Drupal\Core\Entity\EntityStorageInterface $transition_storage
- * Moderation state transition entity storage.
- * @param \Drupal\Core\Entity\EntityStorageInterface $state_storage
- * Moderation state entity storage.
- * @param \Drupal\user\RoleStorageInterface $role_storage
- * The role storage.
- */
- public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $transition_storage, EntityStorageInterface $state_storage, RoleStorageInterface $role_storage) {
- parent::__construct($entity_type, $transition_storage);
- $this->stateStorage = $state_storage;
- $this->roleStorage = $role_storage;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getFormId() {
- return 'content_moderation_transition_list';
- }
-
- /**
- * {@inheritdoc}
- */
- public function buildHeader() {
- $header['to'] = $this->t('To state');
- $header['label'] = $this->t('Button label');
- $header['roles'] = $this->t('Allowed roles');
-
- return $header + parent::buildHeader();
- }
-
- /**
- * {@inheritdoc}
- */
- public function buildRow(EntityInterface $entity) {
- $row['to']['#markup'] = $this->stateStorage->load($entity->getToState())->label();
- $row['label'] = $entity->label();
- $row['roles']['#markup'] = implode(', ', user_role_names(FALSE, 'use ' . $entity->id() . ' transition'));
-
- return $row + parent::buildRow($entity);
- }
-
- /**
- * {@inheritdoc}
- */
- public function render() {
- $build = parent::render();
-
- $build['item'] = [
- '#type' => 'item',
- '#markup' => $this->t('On this screen you can define <em>transitions</em>. Every time an entity is saved, it undergoes a transition. It is not possible to save an entity if it tries do a transition not defined here. Transitions do not necessarily mean a state change, it is possible to transition from a state to the same state but that transition needs to be defined here as well.'),
- '#weight' => -5,
- ];
-
- return $build;
- }
-
- /**
- * {@inheritdoc}
- */
- public function buildForm(array $form, FormStateInterface $form_state) {
- $this->entities = $this->load();
-
- // Get all the moderation states and sort them by weight.
- $states = $this->stateStorage->loadMultiple();
- uasort($states, array($this->entityType->getClass(), 'sort'));
-
- /** @var \Drupal\content_moderation\ModerationStateTransitionInterface $entity */
- $groups = array_fill_keys(array_keys($states), []);
- foreach ($this->entities as $entity) {
- $groups[$entity->getFromState()][] = $entity;
- }
-
- foreach ($groups as $group_name => $entities) {
- $form[$group_name] = [
- '#type' => 'details',
- '#title' => $this->t('From @state to...', ['@state' => $states[$group_name]->label()]),
- // Make sure that the first group is always open.
- '#open' => $group_name === array_keys($groups)[0],
- ];
-
- $form[$group_name][$this->entitiesKey] = array(
- '#type' => 'table',
- '#header' => $this->buildHeader(),
- '#empty' => t('There is no @label yet.', array('@label' => $this->entityType->getLabel())),
- '#tabledrag' => array(
- array(
- 'action' => 'order',
- 'relationship' => 'sibling',
- 'group' => 'weight',
- ),
- ),
- );
-
- $delta = 10;
- // Change the delta of the weight field if have more than 20 entities.
- if (!empty($this->weightKey)) {
- $count = count($this->entities);
- if ($count > 20) {
- $delta = ceil($count / 2);
- }
- }
- foreach ($entities as $entity) {
- $row = $this->buildRow($entity);
- if (isset($row['label'])) {
- $row['label'] = array('#markup' => $row['label']);
- }
- if (isset($row['weight'])) {
- $row['weight']['#delta'] = $delta;
- }
- $form[$group_name][$this->entitiesKey][$entity->id()] = $row;
- }
- }
-
- $form['actions']['#type'] = 'actions';
- $form['actions']['submit'] = array(
- '#type' => 'submit',
- '#value' => t('Save order'),
- '#button_type' => 'primary',
- );
-
- return $form;
- }
-
-}
diff --git a/core/modules/content_moderation/src/Permissions.php b/core/modules/content_moderation/src/Permissions.php
index 027684c..201239f 100644
--- a/core/modules/content_moderation/src/Permissions.php
+++ b/core/modules/content_moderation/src/Permissions.php
@@ -3,8 +3,7 @@
namespace Drupal\content_moderation;
use Drupal\Core\StringTranslation\StringTranslationTrait;
-use Drupal\content_moderation\Entity\ModerationState;
-use Drupal\content_moderation\Entity\ModerationStateTransition;
+use Drupal\workflows\Entity\Workflow;
/**
* Defines a class for dynamic permissions based on transitions.
@@ -22,19 +21,17 @@ class Permissions {
public function transitionPermissions() {
// @todo https://www.drupal.org/node/2779933 write a test for this.
$perms = [];
- /* @var \Drupal\content_moderation\ModerationStateInterface[] $states */
- $states = ModerationState::loadMultiple();
- /* @var \Drupal\content_moderation\ModerationStateTransitionInterface $transition */
- foreach (ModerationStateTransition::loadMultiple() as $id => $transition) {
- $perms['use ' . $id . ' transition'] = [
- 'title' => $this->t('Use the %transition_name transition', [
- '%transition_name' => $transition->label(),
- ]),
- 'description' => $this->t('Move content from %from state to %to state.', [
- '%from' => $states[$transition->getFromState()]->label(),
- '%to' => $states[$transition->getToState()]->label(),
- ]),
- ];
+
+ /** @var \Drupal\workflows\WorkflowInterface $workflow */
+ foreach (Workflow::loadMultipleByType('content_moderation') as $id => $workflow) {
+ foreach ($workflow->getTransitions() as $transition) {
+ $perms['use ' . $workflow->id() . ' transition ' . $transition->id()] = [
+ 'title' => $this->t('Use %transition transition from %workflow workflow.', [
+ '%transition' => $transition->label(),
+ '%workflow' => $workflow->label(),
+ ]),
+ ];
+ }
}
return $perms;
diff --git a/core/modules/content_moderation/src/Plugin/Field/FieldFormatter/ContentModerationStateFormatter.php b/core/modules/content_moderation/src/Plugin/Field/FieldFormatter/ContentModerationStateFormatter.php
new file mode 100644
index 0000000..4e9da16
--- /dev/null
+++ b/core/modules/content_moderation/src/Plugin/Field/FieldFormatter/ContentModerationStateFormatter.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Drupal\content_moderation\Plugin\Field\FieldFormatter;
+
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FormatterBase;
+use Drupal\Core\Field\FieldItemListInterface;
+
+/**
+ * Plugin implementation of the 'content_moderation_state' formatter.
+ *
+ * @FieldFormatter(
+ * id = "content_moderation_state",
+ * label = @Translation("Content moderation state"),
+ * field_types = {
+ * "string",
+ * }
+ * )
+ */
+class ContentModerationStateFormatter extends FormatterBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function viewElements(FieldItemListInterface $items, $langcode) {
+ $elements = array();
+
+ $entity = $items->getEntity();
+ $workflow = $entity->workflow->entity;
+ foreach ($items as $delta => $item) {
+ if (!$item->isEmpty()) {
+ $elements[$delta] = [
+ '#markup' => $workflow->getState($item->value)->label(),
+ ];
+ }
+ }
+
+ return $elements;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function isApplicable(FieldDefinitionInterface $field_definition) {
+ return $field_definition->getName() === 'moderation_state';
+ }
+
+}
diff --git a/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php b/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php
index d6dc89d..8c17d82 100644
--- a/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php
+++ b/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php
@@ -3,9 +3,7 @@
namespace Drupal\content_moderation\Plugin\Field\FieldWidget;
use Drupal\Core\Entity\ContentEntityInterface;
-use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\Plugin\Field\FieldWidget\OptionsSelectWidget;
@@ -23,7 +21,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
* id = "moderation_state_default",
* label = @Translation("Moderation state"),
* field_types = {
- * "entity_reference"
+ * "string"
* }
* )
*/
@@ -37,20 +35,6 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact
protected $currentUser;
/**
- * Moderation state transition entity query.
- *
- * @var \Drupal\Core\Entity\Query\QueryInterface
- */
- protected $moderationStateTransitionEntityQuery;
-
- /**
- * Moderation state storage.
- *
- * @var \Drupal\Core\Entity\EntityStorageInterface
- */
- protected $moderationStateStorage;
-
- /**
* Moderation information service.
*
* @var \Drupal\content_moderation\ModerationInformation
@@ -65,13 +49,6 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact
protected $entityTypeManager;
/**
- * Moderation state transition storage.
- *
- * @var \Drupal\Core\Entity\EntityStorageInterface
- */
- protected $moderationStateTransitionStorage;
-
- /**
* Moderation state transition validation service.
*
* @var \Drupal\content_moderation\StateTransitionValidation
@@ -95,22 +72,13 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact
* Current user service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* Entity type manager.
- * @param \Drupal\Core\Entity\EntityStorageInterface $moderation_state_storage
- * Moderation state storage.
- * @param \Drupal\Core\Entity\EntityStorageInterface $moderation_state_transition_storage
- * Moderation state transition storage.
- * @param \Drupal\Core\Entity\Query\QueryInterface $entity_query
- * Moderation transition entity query service.
* @param \Drupal\content_moderation\ModerationInformation $moderation_information
* Moderation information service.
* @param \Drupal\content_moderation\StateTransitionValidation $validator
* Moderation state transition validation service
*/
- public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, EntityStorageInterface $moderation_state_storage, EntityStorageInterface $moderation_state_transition_storage, QueryInterface $entity_query, ModerationInformation $moderation_information, StateTransitionValidation $validator) {
+ public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, ModerationInformation $moderation_information, StateTransitionValidation $validator) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
- $this->moderationStateTransitionEntityQuery = $entity_query;
- $this->moderationStateTransitionStorage = $moderation_state_transition_storage;
- $this->moderationStateStorage = $moderation_state_storage;
$this->entityTypeManager = $entity_type_manager;
$this->currentUser = $current_user;
$this->moderationInformation = $moderation_information;
@@ -129,9 +97,6 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact
$configuration['third_party_settings'],
$container->get('current_user'),
$container->get('entity_type.manager'),
- $container->get('entity_type.manager')->getStorage('moderation_state'),
- $container->get('entity_type.manager')->getStorage('moderation_state_transition'),
- $container->get('entity.query')->get('moderation_state_transition', 'AND'),
$container->get('content_moderation.moderation_information'),
$container->get('content_moderation.state_transition_validation')
);
@@ -151,19 +116,18 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact
return $element + ['#access' => FALSE];
}
- $default = $items->get($delta)->value ?: $bundle_entity->getThirdPartySetting('content_moderation', 'default_moderation_state', FALSE);
- /** @var \Drupal\content_moderation\ModerationStateInterface $default_state */
- $default_state = $this->entityTypeManager->getStorage('moderation_state')->load($default);
- if (!$default || !$default_state) {
+ $workflow = $this->moderationInformation->getWorkFlowForEntity($entity);
+ $default = $items->get($delta)->value ? $workflow->getState($items->get($delta)->value) : $workflow->getInitialState();
+ if (!$default) {
throw new \UnexpectedValueException(sprintf('The %s bundle has an invalid moderation state configuration, moderation states are enabled but no default is set.', $bundle_entity->label()));
}
+ /** @var \Drupal\workflows\Transition[] $transitions */
$transitions = $this->validator->getValidTransitions($entity, $this->currentUser);
$target_states = [];
- /** @var \Drupal\content_moderation\Entity\ModerationStateTransition $transition */
foreach ($transitions as $transition) {
- $target_states[$transition->getToState()] = $transition->label();
+ $target_states[$transition->to()->id()] = $transition->label();
}
// @todo https://www.drupal.org/node/2779933 write a test for this.
@@ -171,8 +135,8 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact
'#access' => FALSE,
'#type' => 'select',
'#options' => $target_states,
- '#default_value' => $default,
- '#published' => $default ? $default_state->isPublishedState() : FALSE,
+ '#default_value' => $default->id(),
+ '#published' => $default->isPublishedState(),
'#key_column' => $this->column,
];
$element['#element_validate'][] = array(get_class($this), 'validateElement');
@@ -197,7 +161,7 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact
public static function updateStatus($entity_type_id, ContentEntityInterface $entity, array $form, FormStateInterface $form_state) {
$element = $form_state->getTriggeringElement();
if (isset($element['#moderation_state'])) {
- $entity->moderation_state->target_id = $element['#moderation_state'];
+ $entity->moderation_state->value = $element['#moderation_state'];
}
}
@@ -249,7 +213,7 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
- return parent::isApplicable($field_definition) && $field_definition->getName() === 'moderation_state';
+ return $field_definition->getName() === 'moderation_state';
}
}
diff --git a/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php b/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php
index c32521c..036f346 100644
--- a/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php
+++ b/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php
@@ -2,8 +2,7 @@
namespace Drupal\content_moderation\Plugin\Field;
-use Drupal\content_moderation\Entity\ModerationState;
-use Drupal\Core\Field\EntityReferenceFieldItemList;
+use Drupal\Core\Field\FieldItemList;
/**
* A computed field that provides a content entity's moderation state.
@@ -11,19 +10,20 @@ use Drupal\Core\Field\EntityReferenceFieldItemList;
* It links content entities to a moderation state configuration entity via a
* moderation state content entity.
*/
-class ModerationStateFieldItemList extends EntityReferenceFieldItemList {
+class ModerationStateFieldItemList extends FieldItemList {
/**
* Gets the moderation state entity linked to a content entity revision.
*
- * @return \Drupal\content_moderation\ModerationStateInterface|null
- * The moderation state configuration entity linked to a content entity
- * revision.
+ * @return string|null
+ * The moderation state linked to a content entity revision.
*/
protected function getModerationState() {
$entity = $this->getEntity();
- if (!\Drupal::service('content_moderation.moderation_information')->shouldModerateEntitiesOfBundle($entity->getEntityType(), $entity->bundle())) {
+ /** @var \Drupal\content_moderation\ModerationInformationInterface $moderation_info */
+ $moderation_info = \Drupal::service('content_moderation.moderation_information');
+ if (!$moderation_info->shouldModerateEntitiesOfBundle($entity->getEntityType(), $entity->bundle())) {
return NULL;
}
@@ -32,6 +32,7 @@ class ModerationStateFieldItemList extends EntityReferenceFieldItemList {
->condition('content_entity_type_id', $entity->getEntityTypeId())
->condition('content_entity_id', $entity->id())
->condition('content_entity_revision_id', $entity->getRevisionId())
+ ->condition('workflow', $moderation_info->getWorkFlowForEntity($entity)->id())
->allRevisions()
->sort('revision_id', 'DESC')
->execute();
@@ -53,17 +54,15 @@ class ModerationStateFieldItemList extends EntityReferenceFieldItemList {
}
}
- return $content_moderation_state->get('moderation_state')->entity;
+ return $content_moderation_state->get('moderation_state')->value;
}
}
// It is possible that the bundle does not exist at this point. For example,
// the node type form creates a fake Node entity to get default values.
// @see \Drupal\node\NodeTypeForm::form()
- $bundle_entity = \Drupal::entityTypeManager()
- ->getStorage($entity->getEntityType()->getBundleEntityType())
- ->load($entity->bundle());
- if ($bundle_entity && ($default = $bundle_entity->getThirdPartySetting('content_moderation', 'default_moderation_state'))) {
- return ModerationState::load($default);
+ $workflow = $moderation_info->getWorkFlowForEntity($entity);
+ if ($workflow) {
+ return $workflow->getInitialState()->id();
}
}
@@ -93,10 +92,11 @@ class ModerationStateFieldItemList extends EntityReferenceFieldItemList {
// Compute the value of the moderation state.
$index = 0;
if (!isset($this->list[$index]) || $this->list[$index]->isEmpty()) {
+
$moderation_state = $this->getModerationState();
// Do not store NULL values in the static cache.
if ($moderation_state) {
- $this->list[$index] = $this->createItem($index, ['entity' => $moderation_state]);
+ $this->list[$index] = $this->createItem($index, $moderation_state);
}
}
}
diff --git a/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraintValidator.php b/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraintValidator.php
index ca75604..d90e19e 100644
--- a/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraintValidator.php
+++ b/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraintValidator.php
@@ -2,7 +2,6 @@
namespace Drupal\content_moderation\Plugin\Validation\Constraint;
-use Drupal\content_moderation\Entity\ModerationState as ModerationStateEntity;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
@@ -93,21 +92,13 @@ class ModerationStateConstraintValidator extends ConstraintValidator implements
$original_entity = $original_entity->getTranslation($entity->language()->getId());
}
- if ($entity->moderation_state->target_id) {
- $new_state_id = $entity->moderation_state->target_id;
- }
- else {
- $new_state_id = $default = $this->entityTypeManager
- ->getStorage($entity->getEntityType()->getBundleEntityType())->load($entity->bundle())
- ->getThirdPartySetting('content_moderation', 'default_moderation_state');
- }
- if ($new_state_id) {
- $new_state = ModerationStateEntity::load($new_state_id);
- }
- // @todo - what if $new_state_id references something that does not exist or
+ $workflow = $this->moderationInformation->getWorkFlowForEntity($entity);
+ $new_state = $workflow->getState($entity->moderation_state->value) ?: $workflow->getInitialState();
+ $original_state = $workflow->getState($original_entity->moderation_state->value);
+ // @todo - what if $new_state references something that does not exist or
// is null.
- if (!$this->validation->isTransitionAllowed($original_entity->moderation_state->entity, $new_state)) {
- $this->context->addViolation($constraint->message, ['%from' => $original_entity->moderation_state->entity->label(), '%to' => $new_state->label()]);
+ if (!$original_state->canTransitionTo($new_state->id())) {
+ $this->context->addViolation($constraint->message, ['%from' => $original_state->label(), '%to' => $new_state->label()]);
}
}
@@ -126,9 +117,9 @@ class ModerationStateConstraintValidator extends ConstraintValidator implements
protected function isFirstTimeModeration(EntityInterface $entity) {
$original_entity = $this->moderationInformation->getLatestRevision($entity->getEntityTypeId(), $entity->id());
- $original_id = $original_entity->moderation_state->target_id;
+ $original_id = $original_entity->moderation_state;
- return !($entity->moderation_state->target_id && $original_entity && $original_id);
+ return !($entity->moderation_state && $original_entity && $original_id);
}
}
diff --git a/core/modules/content_moderation/src/Plugin/WorkflowType/ContentModeration.php b/core/modules/content_moderation/src/Plugin/WorkflowType/ContentModeration.php
new file mode 100644
index 0000000..e61f25e
--- /dev/null
+++ b/core/modules/content_moderation/src/Plugin/WorkflowType/ContentModeration.php
@@ -0,0 +1,166 @@
+<?php
+
+namespace Drupal\content_moderation\Plugin\WorkflowType;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\content_moderation\ContentModerationState;
+use Drupal\workflows\Plugin\WorkflowTypeBase;
+use Drupal\workflows\StateInterface;
+use Drupal\workflows\WorkflowInterface;
+
+/**
+ * Attaches workflows to content entity types and their bundles.
+ *
+ * @WorkflowType(
+ * id = "content_moderation",
+ * label = @Translation("Content moderation"),
+ * )
+ */
+class ContentModeration extends WorkflowTypeBase {
+
+ use StringTranslationTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function checkWorkflowAccess(WorkflowInterface $entity, $operation, AccountInterface $account) {
+ if ($operation === 'view') {
+ return AccessResult::allowedIfHasPermission($account, 'view content moderation');
+ }
+ return parent::checkWorkflowAccess($entity, $operation, $account);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function decorateState(StateInterface $state) {
+ if (isset($this->configuration['states'][$state->id()])) {
+ $state = new ContentModerationState($state, $this->configuration['states'][$state->id()]['published'], $this->configuration['states'][$state->id()]['default_revision']);
+ }
+ else {
+ $state = new ContentModerationState($state);
+ }
+ return $state;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildStateConfigurationForm(FormStateInterface $form_state, WorkflowInterface $workflow, StateInterface $state = NULL) {
+ /** @var \Drupal\content_moderation\ContentModerationState $state */
+ $form = [];
+ $form['published'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Published'),
+ '#description' => $this->t('When content reaches this state it should be published.'),
+ '#default_value' => isset($state) ? $state->isPublishedState() : FALSE,
+ ];
+
+ $form['default_revision'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Default revision'),
+ '#description' => $this->t('When content reaches this state it should be made the default revision; this is implied for published states.'),
+ '#default_value' => isset($state) ? $state->isDefaultRevisionState() : FALSE,
+ // @todo Add form #state to force "make default" on when "published" is
+ // on for a state.
+ // @see https://www.drupal.org/node/2645614
+ ];
+ return $form;
+ }
+
+ /**
+ * Gets the entity types the workflow is applied to.
+ *
+ * @return string[]
+ * The entity types the workflow is applied to.
+ */
+ public function getEntityTypes() {
+ return array_keys($this->configuration['entity_types']);
+ }
+
+ /**
+ * Gets the bundles of the entity type the workflow is applied to.
+ *
+ * @param string $entity_type_id
+ * The entity type ID to get the bundles for.
+ *
+ * @return string[]
+ * The bundles of the entity type the workflow is applied to.
+ */
+ public function getBundlesForEntityType($entity_type_id) {
+ return $this->configuration['entity_types'][$entity_type_id];
+ }
+
+ /**
+ * Checks if the workflow applies to the supplied entity type and bundle.
+ *
+ * @return bool
+ * TRUE if the workflow applies to the supplied entity type and bundle.
+ * FALSE if not.
+ */
+ public function appliesToEntityTypeAndBundle($entity_type_id, $bundle_id) {
+ if (isset($this->configuration['entity_types'][$entity_type_id])) {
+ return in_array($bundle_id, $this->configuration['entity_types'][$entity_type_id], TRUE);
+ }
+ return FALSE;
+ }
+
+ /**
+ * Removes an entity type ID / bundle ID from the workflow.
+ *
+ * @param string $entity_type_id
+ * The entity type ID to remove.
+ * @param string $bundle_id
+ * The bundle ID to remove.
+ */
+ public function removeEntityTypeAndBundle($entity_type_id, $bundle_id) {
+ $key = array_search($bundle_id, $this->configuration['entity_types'][$entity_type_id], TRUE);
+ if ($key !== FALSE) {
+ unset($this->configuration['entity_types'][$entity_type_id][$key]);
+ if (empty($this->configuration['entity_types'][$entity_type_id])) {
+ unset($this->configuration['entity_types'][$entity_type_id]);
+ }
+ else {
+ $this->configuration['entity_types'][$entity_type_id] = array_values($this->configuration['entity_types'][$entity_type_id]);
+ }
+ }
+ }
+
+ /**
+ * Add an entity type ID / bundle ID to the workflow.
+ *
+ * @param string $entity_type_id
+ * The entity type ID to add.
+ * @param string $bundle_id
+ * The bundle ID to add.
+ */
+ public function addEntityTypeAndBundle($entity_type_id, $bundle_id) {
+ if (!$this->appliesToEntityTypeAndBundle($entity_type_id, $bundle_id)) {
+ $this->configuration['entity_types'][$entity_type_id][] = $bundle_id;
+ natsort($this->configuration['entity_types'][$entity_type_id]);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function defaultConfiguration() {
+ // This plugin does not store anything per transition.
+ return [
+ 'states' => [],
+ 'entity_types' => [],
+ ];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function calculateDependencies() {
+ // @todo : Implement calculateDependencies() method.
+ return [];
+ }
+
+}
diff --git a/core/modules/content_moderation/src/Routing/EntityTypeModerationRouteProvider.php b/core/modules/content_moderation/src/Routing/EntityTypeModerationRouteProvider.php
index c722a67..d1dcd2b 100644
--- a/core/modules/content_moderation/src/Routing/EntityTypeModerationRouteProvider.php
+++ b/core/modules/content_moderation/src/Routing/EntityTypeModerationRouteProvider.php
@@ -47,7 +47,7 @@ class EntityTypeModerationRouteProvider implements EntityRouteProviderInterface
'_entity_form' => "{$entity_type_id}.moderation",
'_title' => 'Moderation',
])
- ->setRequirement('_permission', 'administer moderation states')
+ ->setRequirement('_permission', 'administer content moderation')
->setOption('parameters', [
$entity_type_id => ['type' => 'entity:' . $entity_type_id],
]);
diff --git a/core/modules/content_moderation/src/StateTransitionValidation.php b/core/modules/content_moderation/src/StateTransitionValidation.php
index 2e2a4e2..e3d3376 100644
--- a/core/modules/content_moderation/src/StateTransitionValidation.php
+++ b/core/modules/content_moderation/src/StateTransitionValidation.php
@@ -3,10 +3,8 @@
namespace Drupal\content_moderation;
use Drupal\Core\Entity\ContentEntityInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\Entity\Query\QueryFactory;
use Drupal\Core\Session\AccountInterface;
-use Drupal\content_moderation\Entity\ModerationStateTransition;
+use Drupal\workflows\Transition;
/**
* Validates whether a certain state transition is allowed.
@@ -14,18 +12,11 @@ use Drupal\content_moderation\Entity\ModerationStateTransition;
class StateTransitionValidation implements StateTransitionValidationInterface {
/**
- * Entity type manager.
+ * The moderation information service.
*
- * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ * @var \Drupal\content_moderation\ModerationInformationInterface
*/
- protected $entityTypeManager;
-
- /**
- * Entity query factory.
- *
- * @var \Drupal\Core\Entity\Query\QueryFactory
- */
- protected $queryFactory;
+ protected $moderationInfo;
/**
* Stores the possible state transitions.
@@ -37,211 +28,23 @@ class StateTransitionValidation implements StateTransitionValidationInterface {
/**
* Constructs a new StateTransitionValidation.
*
- * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
- * The entity type manager service.
- * @param \Drupal\Core\Entity\Query\QueryFactory $query_factory
- * The entity query factory.
- */
- public function __construct(EntityTypeManagerInterface $entity_type_manager, QueryFactory $query_factory) {
- $this->entityTypeManager = $entity_type_manager;
- $this->queryFactory = $query_factory;
- }
-
- /**
- * Computes a mapping of possible transitions.
- *
- * This method is uncached and will recalculate the list on every request.
- * In most cases you want to use getPossibleTransitions() instead.
- *
- * @see static::getPossibleTransitions()
- *
- * @return array[]
- * An array containing all possible transitions. Each entry is keyed by the
- * "from" state, and the value is an array of all legal "to" states based
- * on the currently defined transition objects.
- */
- protected function calculatePossibleTransitions() {
- $transitions = $this->transitionStorage()->loadMultiple();
-
- $possible_transitions = [];
- /** @var \Drupal\content_moderation\ModerationStateTransitionInterface $transition */
- foreach ($transitions as $transition) {
- $possible_transitions[$transition->getFromState()][] = $transition->getToState();
- }
- return $possible_transitions;
- }
-
- /**
- * Returns a mapping of possible transitions.
- *
- * @return array[]
- * An array containing all possible transitions. Each entry is keyed by the
- * "from" state, and the value is an array of all legal "to" states based
- * on the currently defined transition objects.
+ * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info
+ * The moderation information service.
*/
- protected function getPossibleTransitions() {
- if (empty($this->possibleTransitions)) {
- $this->possibleTransitions = $this->calculatePossibleTransitions();
- }
- return $this->possibleTransitions;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getValidTransitionTargets(ContentEntityInterface $entity, AccountInterface $user) {
- $bundle = $this->loadBundleEntity($entity->getEntityType()->getBundleEntityType(), $entity->bundle());
-
- $states_for_bundle = $bundle->getThirdPartySetting('content_moderation', 'allowed_moderation_states', []);
-
- /** @var \Drupal\content_moderation\Entity\ModerationState $current_state */
- $current_state = $entity->moderation_state->entity;
-
- $all_transitions = $this->getPossibleTransitions();
- $destination_ids = $all_transitions[$current_state->id()];
-
- $destination_ids = array_intersect($states_for_bundle, $destination_ids);
- $destinations = $this->entityTypeManager->getStorage('moderation_state')->loadMultiple($destination_ids);
-
- return array_filter($destinations, function(ModerationStateInterface $destination_state) use ($current_state, $user) {
- return $this->userMayTransition($current_state, $destination_state, $user);
- });
+ public function __construct(ModerationInformationInterface $moderation_info) {
+ $this->moderationInfo = $moderation_info;
}
/**
* {@inheritdoc}
*/
public function getValidTransitions(ContentEntityInterface $entity, AccountInterface $user) {
- $bundle = $this->loadBundleEntity($entity->getEntityType()->getBundleEntityType(), $entity->bundle());
-
- /** @var \Drupal\content_moderation\Entity\ModerationState $current_state */
- $current_state = $entity->moderation_state->entity;
- $current_state_id = $current_state ? $current_state->id() : $bundle->getThirdPartySetting('content_moderation', 'default_moderation_state');
-
- // Determine the states that are legal on this bundle.
- $legal_bundle_states = $bundle->getThirdPartySetting('content_moderation', 'allowed_moderation_states', []);
+ $workflow = $this->moderationInfo->getWorkFlowForEntity($entity);
+ $current_state = $entity->moderation_state->value ? $workflow->getState($entity->moderation_state->value) : $workflow->getInitialState();
- // Legal transitions include those that are possible from the current state,
- // filtered by those whose target is legal on this bundle and that the
- // user has access to execute.
- $transitions = array_filter($this->getTransitionsFrom($current_state_id), function(ModerationStateTransition $transition) use ($legal_bundle_states, $user) {
- return in_array($transition->getToState(), $legal_bundle_states, TRUE)
- && $user->hasPermission('use ' . $transition->id() . ' transition');
+ return array_filter($current_state->getTransitions(), function(Transition $transition) use ($workflow, $user) {
+ return $user->hasPermission('use ' . $workflow->id() . ' transition ' . $transition->id());
});
-
- return $transitions;
- }
-
- /**
- * Returns a list of possible transitions from a given state.
- *
- * This list is based only on those transitions that exist, not what
- * transitions are legal in a given context.
- *
- * @param string $state_name
- * The machine name of the state from which we are transitioning.
- *
- * @return ModerationStateTransition[]
- * A list of possible transitions from a given state.
- */
- protected function getTransitionsFrom($state_name) {
- $result = $this->transitionStateQuery()
- ->condition('stateFrom', $state_name)
- ->sort('weight')
- ->execute();
-
- return $this->transitionStorage()->loadMultiple($result);
- }
-
- /**
- * {@inheritdoc}
- */
- public function userMayTransition(ModerationStateInterface $from, ModerationStateInterface $to, AccountInterface $user) {
- if ($transition = $this->getTransitionFromStates($from, $to)) {
- return $user->hasPermission('use ' . $transition->id() . ' transition');
- }
- return FALSE;
- }
-
- /**
- * Returns the transition object that transitions from one state to another.
- *
- * @param \Drupal\content_moderation\ModerationStateInterface $from
- * The origin state.
- * @param \Drupal\content_moderation\ModerationStateInterface $to
- * The destination state.
- *
- * @return ModerationStateTransition|null
- * A transition object, or NULL if there is no such transition.
- */
- protected function getTransitionFromStates(ModerationStateInterface $from, ModerationStateInterface $to) {
- $from = $this->transitionStateQuery()
- ->condition('stateFrom', $from->id())
- ->condition('stateTo', $to->id())
- ->execute();
-
- $transitions = $this->transitionStorage()->loadMultiple($from);
-
- if ($transitions) {
- return current($transitions);
- }
- return NULL;
- }
-
- /**
- * {@inheritdoc}
- */
- public function isTransitionAllowed(ModerationStateInterface $from, ModerationStateInterface $to) {
- $allowed_transitions = $this->calculatePossibleTransitions();
- if (isset($allowed_transitions[$from->id()])) {
- return in_array($to->id(), $allowed_transitions[$from->id()], TRUE);
- }
- return FALSE;
- }
-
- /**
- * Returns a transition state entity query.
- *
- * @return \Drupal\Core\Entity\Query\QueryInterface
- * A transition state entity query.
- */
- protected function transitionStateQuery() {
- return $this->queryFactory->get('moderation_state_transition', 'AND');
- }
-
- /**
- * Returns the transition entity storage service.
- *
- * @return \Drupal\Core\Entity\EntityStorageInterface
- * The transition state entity storage.
- */
- protected function transitionStorage() {
- return $this->entityTypeManager->getStorage('moderation_state_transition');
- }
-
- /**
- * Returns the state entity storage service.
- *
- * @return \Drupal\Core\Entity\EntityStorageInterface
- * The moderation state entity storage.
- */
- protected function stateStorage() {
- return $this->entityTypeManager->getStorage('moderation_state');
- }
-
- /**
- * Loads a specific bundle entity.
- *
- * @param string $bundle_entity_type_id
- * The bundle entity type ID.
- * @param string $bundle_id
- * The bundle ID.
- *
- * @return \Drupal\Core\Config\Entity\ConfigEntityInterface|null
- * The specific bundle entity.
- */
- protected function loadBundleEntity($bundle_entity_type_id, $bundle_id) {
- return $this->entityTypeManager->getStorage($bundle_entity_type_id)->load($bundle_id);
}
}
diff --git a/core/modules/content_moderation/src/StateTransitionValidationInterface.php b/core/modules/content_moderation/src/StateTransitionValidationInterface.php
index 5ef0dd1..1acbf05 100644
--- a/core/modules/content_moderation/src/StateTransitionValidationInterface.php
+++ b/core/modules/content_moderation/src/StateTransitionValidationInterface.php
@@ -11,20 +11,6 @@ use Drupal\Core\Session\AccountInterface;
interface StateTransitionValidationInterface {
/**
- * Gets a list of states a user may transition an entity to.
- *
- * @param \Drupal\Core\Entity\ContentEntityInterface $entity
- * The entity to be transitioned.
- * @param \Drupal\Core\Session\AccountInterface $user
- * The account that wants to perform a transition.
- *
- * @return \Drupal\content_moderation\Entity\ModerationState[]
- * Returns an array of States to which the specified user may transition the
- * entity.
- */
- public function getValidTransitionTargets(ContentEntityInterface $entity, AccountInterface $user);
-
- /**
* Gets a list of transitions that are legal for this user on this entity.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
@@ -32,40 +18,9 @@ interface StateTransitionValidationInterface {
* @param \Drupal\Core\Session\AccountInterface $user
* The account that wants to perform a transition.
*
- * @return \Drupal\content_moderation\Entity\ModerationStateTransition[]
+ * @return \Drupal\workflows\Transition[]
* The list of transitions that are legal for this user on this entity.
*/
public function getValidTransitions(ContentEntityInterface $entity, AccountInterface $user);
- /**
- * Determines if a user is allowed to transition from one state to another.
- *
- * This method will also return FALSE if there is no transition between the
- * specified states at all.
- *
- * @param \Drupal\content_moderation\ModerationStateInterface $from
- * The origin state.
- * @param \Drupal\content_moderation\ModerationStateInterface $to
- * The destination state.
- * @param \Drupal\Core\Session\AccountInterface $user
- * The user to validate.
- *
- * @return bool
- * TRUE if the given user may transition between those two states.
- */
- public function userMayTransition(ModerationStateInterface $from, ModerationStateInterface $to, AccountInterface $user);
-
- /**
- * Determines a transition allowed.
- *
- * @param \Drupal\content_moderation\ModerationStateInterface $from
- * The origin state.
- * @param \Drupal\content_moderation\ModerationStateInterface $to
- * The destination state.
- *
- * @return bool
- * Is the transition allowed.
- */
- public function isTransitionAllowed(ModerationStateInterface $from, ModerationStateInterface $to);
-
}
diff --git a/core/modules/content_moderation/src/Tests/ModerationFormTest.php b/core/modules/content_moderation/src/Tests/ModerationFormTest.php
index d6c92b9..16da4e4 100644
--- a/core/modules/content_moderation/src/Tests/ModerationFormTest.php
+++ b/core/modules/content_moderation/src/Tests/ModerationFormTest.php
@@ -15,10 +15,7 @@ class ModerationFormTest extends ModerationStateTestBase {
protected function setUp() {
parent::setUp();
$this->drupalLogin($this->adminUser);
- $this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE, [
- 'draft',
- 'published',
- ], 'draft');
+ $this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE);
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content');
}
diff --git a/core/modules/content_moderation/src/Tests/ModerationLocaleTest.php b/core/modules/content_moderation/src/Tests/ModerationLocaleTest.php
index 0d40356..a78c104 100644
--- a/core/modules/content_moderation/src/Tests/ModerationLocaleTest.php
+++ b/core/modules/content_moderation/src/Tests/ModerationLocaleTest.php
@@ -28,13 +28,7 @@ class ModerationLocaleTest extends ModerationStateTestBase {
$this->drupalLogin($this->rootUser);
// Enable moderation on Article node type.
- $this->createContentTypeFromUi(
- 'Article',
- 'article',
- TRUE,
- ['draft', 'published', 'archived'],
- 'draft'
- );
+ $this->createContentTypeFromUi('Article', 'article', TRUE);
// Add French language.
$edit = [
@@ -103,9 +97,9 @@ class ModerationLocaleTest extends ModerationStateTestBase {
$french_node = $english_node->getTranslation('fr');
$this->assertEqual('French node', $french_node->label());
- $this->assertEqual($english_node->moderation_state->target_id, 'published');
+ $this->assertEqual($english_node->moderation_state->value, 'published');
$this->assertTrue($english_node->isPublished());
- $this->assertEqual($french_node->moderation_state->target_id, 'draft');
+ $this->assertEqual($french_node->moderation_state->value, 'draft');
$this->assertFalse($french_node->isPublished());
// Create another article with its translation. This time we will publish
@@ -133,9 +127,9 @@ class ModerationLocaleTest extends ModerationStateTestBase {
$this->assertText(t('Article Translated node has been updated.'));
$english_node = $this->drupalGetNodeByTitle('Another node', TRUE);
$french_node = $english_node->getTranslation('fr');
- $this->assertEqual($french_node->moderation_state->target_id, 'published');
+ $this->assertEqual($french_node->moderation_state->value, 'published');
$this->assertTrue($french_node->isPublished());
- $this->assertEqual($english_node->moderation_state->target_id, 'draft');
+ $this->assertEqual($english_node->moderation_state->value, 'draft');
$this->assertFalse($english_node->isPublished());
// Now check that we can create a new draft of the translation.
@@ -146,7 +140,7 @@ class ModerationLocaleTest extends ModerationStateTestBase {
$this->assertText(t('Article New draft of translated node has been updated.'));
$english_node = $this->drupalGetNodeByTitle('Another node', TRUE);
$french_node = $english_node->getTranslation('fr');
- $this->assertEqual($french_node->moderation_state->target_id, 'published');
+ $this->assertEqual($french_node->moderation_state->value, 'published');
$this->assertTrue($french_node->isPublished());
$this->assertEqual($french_node->getTitle(), 'Translated node', 'The default revision of the published translation remains the same.');
@@ -158,7 +152,7 @@ class ModerationLocaleTest extends ModerationStateTestBase {
$this->assertText(t('The moderation state has been updated.'));
$english_node = $this->drupalGetNodeByTitle('Another node', TRUE);
$french_node = $english_node->getTranslation('fr');
- $this->assertEqual($french_node->moderation_state->target_id, 'published');
+ $this->assertEqual($french_node->moderation_state->value, 'published');
$this->assertTrue($french_node->isPublished());
$this->assertEqual($french_node->getTitle(), 'New draft of translated node', 'The draft has replaced the published revision.');
@@ -166,7 +160,7 @@ class ModerationLocaleTest extends ModerationStateTestBase {
$this->drupalPostForm('node/' . $english_node->id() . '/edit', [], t('Save and Publish (this translation)'));
$this->assertText(t('Article Another node has been updated.'));
$english_node = $this->drupalGetNodeByTitle('Another node', TRUE);
- $this->assertEqual($english_node->moderation_state->target_id, 'published');
+ $this->assertEqual($english_node->moderation_state->value, 'published');
// Archive the node and its translation.
$this->drupalPostForm('node/' . $english_node->id() . '/edit', [], t('Save and Archive (this translation)'));
@@ -175,9 +169,9 @@ class ModerationLocaleTest extends ModerationStateTestBase {
$this->assertText(t('Article New draft of translated node has been updated.'));
$english_node = $this->drupalGetNodeByTitle('Another node', TRUE);
$french_node = $english_node->getTranslation('fr');
- $this->assertEqual($english_node->moderation_state->target_id, 'archived');
+ $this->assertEqual($english_node->moderation_state->value, 'archived');
$this->assertFalse($english_node->isPublished());
- $this->assertEqual($french_node->moderation_state->target_id, 'archived');
+ $this->assertEqual($french_node->moderation_state->value, 'archived');
$this->assertFalse($french_node->isPublished());
// Create another article with its translation. This time publishing english
diff --git a/core/modules/content_moderation/src/Tests/ModerationStateBlockTest.php b/core/modules/content_moderation/src/Tests/ModerationStateBlockTest.php
index 03a4b0e..e42f536 100644
--- a/core/modules/content_moderation/src/Tests/ModerationStateBlockTest.php
+++ b/core/modules/content_moderation/src/Tests/ModerationStateBlockTest.php
@@ -59,12 +59,7 @@ class ModerationStateBlockTest extends ModerationStateTestBase {
// Enable moderation for custom blocks at
// admin/structure/block/block-content/manage/basic/moderation.
- $edit = [
- 'enable_moderation_state' => TRUE,
- 'allowed_moderation_states_unpublished[draft]' => TRUE,
- 'allowed_moderation_states_published[published]' => TRUE,
- 'default_moderation_state' => 'draft',
- ];
+ $edit = ['workflow' => 'editorial'];
$this->drupalPostForm(NULL, $edit, t('Save'));
$this->assertText(t('Your settings have been saved.'));
diff --git a/core/modules/content_moderation/src/Tests/ModerationStateNodeTest.php b/core/modules/content_moderation/src/Tests/ModerationStateNodeTest.php
index e2069b4..a89ec9f 100644
--- a/core/modules/content_moderation/src/Tests/ModerationStateNodeTest.php
+++ b/core/modules/content_moderation/src/Tests/ModerationStateNodeTest.php
@@ -4,6 +4,7 @@ namespace Drupal\content_moderation\Tests;
use Drupal\Core\Url;
use Drupal\node\Entity\Node;
+use Drupal\workflows\Entity\Workflow;
/**
* Tests general content moderation workflow for nodes.
@@ -18,13 +19,7 @@ class ModerationStateNodeTest extends ModerationStateTestBase {
protected function setUp() {
parent::setUp();
$this->drupalLogin($this->adminUser);
- $this->createContentTypeFromUi(
- 'Moderated content',
- 'moderated_content',
- TRUE,
- ['draft', 'needs_review', 'published'],
- 'draft'
- );
+ $this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE);
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content');
}
@@ -35,19 +30,11 @@ class ModerationStateNodeTest extends ModerationStateTestBase {
$this->drupalPostForm('node/add/moderated_content', [
'title[0][value]' => 'moderated content',
], t('Save and Create New Draft'));
- $nodes = \Drupal::entityTypeManager()
- ->getStorage('node')
- ->loadByProperties([
- 'title' => 'moderated content',
- ]);
-
- if (!$nodes) {
+ $node = $this->getNodeByTitle('moderated content');
+ if (!$node) {
$this->fail('Test node was not saved correctly.');
- return;
}
-
- $node = reset($nodes);
- $this->assertEqual('draft', $node->moderation_state->target_id);
+ $this->assertEqual('draft', $node->moderation_state->value);
$path = 'node/' . $node->id() . '/edit';
// Set up published revision.
@@ -56,7 +43,7 @@ class ModerationStateNodeTest extends ModerationStateTestBase {
/* @var \Drupal\node\NodeInterface $node */
$node = \Drupal::entityTypeManager()->getStorage('node')->load($node->id());
$this->assertTrue($node->isPublished());
- $this->assertEqual('published', $node->moderation_state->target_id);
+ $this->assertEqual('published', $node->moderation_state->value);
// Verify that the state field is not shown.
$this->assertNoText('Published');
@@ -65,30 +52,40 @@ class ModerationStateNodeTest extends ModerationStateTestBase {
$this->drupalPostForm('node/' . $node->id() . '/delete', array(), t('Delete'));
$this->assertText(t('The Moderated content moderated content has been deleted.'));
+ // Disable content moderation.
+ $this->drupalPostForm('admin/structure/types/manage/moderated_content/moderation', ['workflow' => ''], t('Save'));
$this->drupalGet('admin/structure/types/manage/moderated_content/moderation');
- $this->assertFieldByName('enable_moderation_state');
- $this->assertFieldChecked('edit-enable-moderation-state');
- $this->drupalPostForm(NULL, ['enable_moderation_state' => FALSE], t('Save'));
- $this->drupalGet('admin/structure/types/manage/moderated_content/moderation');
- $this->assertFieldByName('enable_moderation_state');
- $this->assertNoFieldChecked('edit-enable-moderation-state');
+ $this->assertOptionSelected('edit-workflow', '');
+ // Ensure the parent environment is up-to-date.
+ // @see content_moderation_workflow_insert()
+ \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
+ \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+ // Create a new node.
$this->drupalPostForm('node/add/moderated_content', [
'title[0][value]' => 'non-moderated content',
], t('Save and publish'));
- $nodes = \Drupal::entityTypeManager()
- ->getStorage('node')
- ->loadByProperties([
- 'title' => 'non-moderated content',
- ]);
-
- if (!$nodes) {
+ $node = $this->getNodeByTitle('non-moderated content');
+ if (!$node) {
$this->fail('Non-moderated test node was not saved correctly.');
- return;
}
+ $this->assertEqual(NULL, $node->moderation_state->value);
+
+ // \Drupal\content_moderation\Form\BundleModerationConfigurationForm()
+ // should not list workflows with no states.
+ $workflow = Workflow::create(['id' => 'stateless', 'label' => 'Stateless', 'type' => 'content_moderation']);
+ $workflow->save();
- $node = reset($nodes);
- $this->assertEqual(NULL, $node->moderation_state->target_id);
+ $this->drupalGet('admin/structure/types/manage/moderated_content/moderation');
+ $this->assertNoText('Stateless');
+ $workflow
+ ->addState('draft', 'Draft')
+ ->addState('published', 'Published')
+ ->addTransition('publish', 'Publish', ['draft', 'published'], 'published')
+ ->save();
+ $this->drupalGet('admin/structure/types/manage/moderated_content/moderation');
+ $this->assertText('Stateless');
}
/**
diff --git a/core/modules/content_moderation/src/Tests/ModerationStateNodeTypeTest.php b/core/modules/content_moderation/src/Tests/ModerationStateNodeTypeTest.php
index debb32c..39e1a2c 100644
--- a/core/modules/content_moderation/src/Tests/ModerationStateNodeTypeTest.php
+++ b/core/modules/content_moderation/src/Tests/ModerationStateNodeTypeTest.php
@@ -42,12 +42,16 @@ class ModerationStateNodeTypeTest extends ModerationStateTestBase {
], t('Save and publish'));
$this->assertText('Not moderated Test has been created.');
- // Now enable moderation state.
- $this->enableModerationThroughUi(
- 'not_moderated',
- ['draft', 'needs_review', 'published'],
- 'draft'
- );
+ // Now enable moderation state, ensuring all the expected links and tabs are
+ // present.
+ $this->drupalGet('admin/structure/types');
+ $this->assertLinkByHref('admin/structure/types/manage/not_moderated/moderation');
+ $this->drupalGet('admin/structure/types/manage/not_moderated');
+ $this->assertLinkByHref('admin/structure/types/manage/not_moderated/moderation');
+ $this->drupalGet('admin/structure/types/manage/not_moderated/moderation');
+ $this->assertOptionSelected('edit-workflow', '');
+ $edit['workflow'] = 'editorial';
+ $this->drupalPostForm(NULL, $edit, t('Save'));
// And make sure it works.
$nodes = \Drupal::entityTypeManager()->getStorage('node')
diff --git a/core/modules/content_moderation/src/Tests/ModerationStateStatesTest.php b/core/modules/content_moderation/src/Tests/ModerationStateStatesTest.php
deleted file mode 100644
index 3c5fd14..0000000
--- a/core/modules/content_moderation/src/Tests/ModerationStateStatesTest.php
+++ /dev/null
@@ -1,75 +0,0 @@
-<?php
-
-namespace Drupal\content_moderation\Tests;
-
-/**
- * Tests moderation state config entity.
- *
- * @group content_moderation
- */
-class ModerationStateStatesTest extends ModerationStateTestBase {
-
- /**
- * Tests route access/permissions.
- */
- public function testAccess() {
- $paths = [
- 'admin/config/workflow/moderation',
- 'admin/config/workflow/moderation/states',
- 'admin/config/workflow/moderation/states/add',
- 'admin/config/workflow/moderation/states/draft',
- 'admin/config/workflow/moderation/states/draft/delete',
- ];
-
- foreach ($paths as $path) {
- $this->drupalGet($path);
- // No access.
- $this->assertResponse(403);
- }
- $this->drupalLogin($this->adminUser);
- foreach ($paths as $path) {
- $this->drupalGet($path);
- // User has access.
- $this->assertResponse(200);
- }
- }
-
- /**
- * Tests administration of moderation state entity.
- */
- public function testStateAdministration() {
- $this->drupalLogin($this->adminUser);
- $this->drupalGet('admin/config/workflow/moderation');
- $this->assertLink('Moderation states');
- $this->assertLink('Moderation state transitions');
- $this->clickLink('Moderation states');
- $this->assertLink('Add moderation state');
- $this->assertText('Draft');
- // Edit the draft.
- $this->clickLink('Edit', 0);
- $this->assertFieldByName('label', 'Draft');
- $this->assertNoFieldChecked('edit-published');
- $this->drupalPostForm(NULL, [
- 'label' => 'Drafty',
- ], t('Save'));
- $this->assertText('Saved the Drafty Moderation state.');
- $this->drupalGet('admin/config/workflow/moderation/states/draft');
- $this->assertFieldByName('label', 'Drafty');
- $this->drupalPostForm(NULL, [
- 'label' => 'Draft',
- ], t('Save'));
- $this->assertText('Saved the Draft Moderation state.');
- $this->clickLink(t('Add moderation state'));
- $this->drupalPostForm(NULL, [
- 'label' => 'Expired',
- 'id' => 'expired',
- ], t('Save'));
- $this->assertText('Created the Expired Moderation state.');
- $this->drupalGet('admin/config/workflow/moderation/states/expired');
- $this->clickLink('Delete');
- $this->assertText('Are you sure you want to delete Expired?');
- $this->drupalPostForm(NULL, [], t('Delete'));
- $this->assertText('Moderation state Expired deleted');
- }
-
-}
diff --git a/core/modules/content_moderation/src/Tests/ModerationStateTestBase.php b/core/modules/content_moderation/src/Tests/ModerationStateTestBase.php
index ddd275e..22307df 100644
--- a/core/modules/content_moderation/src/Tests/ModerationStateTestBase.php
+++ b/core/modules/content_moderation/src/Tests/ModerationStateTestBase.php
@@ -5,7 +5,6 @@ namespace Drupal\content_moderation\Tests;
use Drupal\Core\Session\AccountInterface;
use Drupal\simpletest\WebTestBase;
use Drupal\user\Entity\Role;
-use Drupal\content_moderation\Entity\ModerationState;
/**
* Defines a base class for moderation state tests.
@@ -30,18 +29,15 @@ abstract class ModerationStateTestBase extends WebTestBase {
* @var array
*/
protected $permissions = [
- 'administer moderation states',
- 'administer moderation state transitions',
- 'use draft_draft transition',
- 'use draft_published transition',
- 'use published_draft transition',
- 'use published_archived transition',
+ 'administer content moderation',
'access administration pages',
'administer content types',
'administer nodes',
'view latest version',
'view any unpublished content',
'access content overview',
+ 'use editorial transition create_new_draft',
+ 'use editorial transition publish',
];
/**
@@ -68,6 +64,21 @@ abstract class ModerationStateTestBase extends WebTestBase {
}
/**
+ * Gets the permission machine name for a transition.
+ *
+ * @param string $workflow_id
+ * The workflow ID.
+ * @param string $transition_id
+ * The transition ID.
+ *
+ * @return string
+ * The permission machine name for a transition.
+ */
+ protected function getWorkflowTransitionPermission($workflow_id, $transition_id) {
+ return 'use ' . $workflow_id . ' transition ' . $transition_id;
+ }
+
+ /**
* Creates a content-type from the UI.
*
* @param string $content_type_name
@@ -76,12 +87,10 @@ abstract class ModerationStateTestBase extends WebTestBase {
* Machine name.
* @param bool $moderated
* TRUE if should be moderated.
- * @param string[] $allowed_states
- * Array of allowed state IDs.
- * @param string $default_state
- * Default state.
+ * @param string $workflow_id
+ * The workflow to attach to the bundle.
*/
- protected function createContentTypeFromUi($content_type_name, $content_type_id, $moderated = FALSE, array $allowed_states = [], $default_state = NULL) {
+ protected function createContentTypeFromUi($content_type_name, $content_type_id, $moderated = FALSE, $workflow_id = 'editorial') {
$this->drupalGet('admin/structure/types');
$this->clickLink('Add content type');
$edit = [
@@ -91,7 +100,7 @@ abstract class ModerationStateTestBase extends WebTestBase {
$this->drupalPostForm(NULL, $edit, t('Save content type'));
if ($moderated) {
- $this->enableModerationThroughUi($content_type_id, $allowed_states, $default_state);
+ $this->enableModerationThroughUi($content_type_id, $workflow_id);
}
}
@@ -100,31 +109,16 @@ abstract class ModerationStateTestBase extends WebTestBase {
*
* @param string $content_type_id
* Machine name.
- * @param string[] $allowed_states
- * Array of allowed state IDs.
- * @param string $default_state
- * Default state.
+ * @param string $workflow_id
+ * The workflow to attach to the bundle.
*/
- protected function enableModerationThroughUi($content_type_id, array $allowed_states, $default_state) {
- $this->drupalGet('admin/structure/types');
- $this->assertLinkByHref('admin/structure/types/manage/' . $content_type_id . '/moderation');
- $this->drupalGet('admin/structure/types/manage/' . $content_type_id);
- $this->assertLinkByHref('admin/structure/types/manage/' . $content_type_id . '/moderation');
- $this->drupalGet('admin/structure/types/manage/' . $content_type_id . '/moderation');
- $this->assertFieldByName('enable_moderation_state');
- $this->assertNoFieldChecked('edit-enable-moderation-state');
-
- $edit['enable_moderation_state'] = 1;
-
- /** @var ModerationState $state */
- foreach (ModerationState::loadMultiple() as $state) {
- $key = $state->isPublishedState() ? 'allowed_moderation_states_published[' . $state->id() . ']' : 'allowed_moderation_states_unpublished[' . $state->id() . ']';
- $edit[$key] = in_array($state->id(), $allowed_states, TRUE) ? $state->id() : FALSE;
- }
-
- $edit['default_moderation_state'] = $default_state;
-
- $this->drupalPostForm(NULL, $edit, t('Save'));
+ protected function enableModerationThroughUi($content_type_id, $workflow_id = 'editorial') {
+ $edit['workflow'] = $workflow_id;
+ $this->drupalPostForm('admin/structure/types/manage/' . $content_type_id . '/moderation', $edit, t('Save'));
+ // Ensure the parent environment is up-to-date.
+ // @see content_moderation_workflow_insert()
+ \Drupal::service('entity_type.bundle.info')->clearCachedBundles();
+ \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
}
/**
diff --git a/core/modules/content_moderation/src/Tests/ModerationStateTransitionsTest.php b/core/modules/content_moderation/src/Tests/ModerationStateTransitionsTest.php
deleted file mode 100644
index 703561b..0000000
--- a/core/modules/content_moderation/src/Tests/ModerationStateTransitionsTest.php
+++ /dev/null
@@ -1,91 +0,0 @@
-<?php
-
-namespace Drupal\content_moderation\Tests;
-
-/**
- * Tests moderation state transition config entity.
- *
- * @group content_moderation
- */
-class ModerationStateTransitionsTest extends ModerationStateTestBase {
-
- /**
- * Tests route access/permissions.
- */
- public function testAccess() {
- $paths = [
- 'admin/config/workflow/moderation/transitions',
- 'admin/config/workflow/moderation/transitions/add',
- 'admin/config/workflow/moderation/transitions/draft_published',
- 'admin/config/workflow/moderation/transitions/draft_published/delete',
- ];
-
- foreach ($paths as $path) {
- $this->drupalGet($path);
- // No access.
- $this->assertResponse(403);
- }
- $this->drupalLogin($this->adminUser);
- foreach ($paths as $path) {
- $this->drupalGet($path);
- // User has access.
- $this->assertResponse(200);
- }
- }
-
- /**
- * Tests administration of moderation state transition entity.
- */
- public function testTransitionAdministration() {
- $this->drupalLogin($this->adminUser);
-
- $this->drupalGet('admin/config/workflow/moderation');
- $this->clickLink('Moderation state transitions');
- $this->assertLink('Add moderation state transition');
- $this->assertText('Create New Draft');
-
- // Edit the Draft » Draft review.
- $this->drupalGet('admin/config/workflow/moderation/transitions/draft_draft');
- $this->assertFieldByName('label', 'Create New Draft');
- $this->assertFieldByName('stateFrom', 'draft');
- $this->assertFieldByName('stateTo', 'draft');
- $this->drupalPostForm(NULL, [
- 'label' => 'Create Draft',
- ], t('Save'));
- $this->assertText('Saved the Create Draft Moderation state transition.');
- $this->drupalGet('admin/config/workflow/moderation/transitions/draft_draft');
- $this->assertFieldByName('label', 'Create Draft');
- // Now set it back.
- $this->drupalPostForm(NULL, [
- 'label' => 'Create New Draft',
- ], t('Save'));
- $this->assertText('Saved the Create New Draft Moderation state transition.');
-
- // Add a new state.
- $this->drupalGet('admin/config/workflow/moderation/states/add');
- $this->drupalPostForm(NULL, [
- 'label' => 'Expired',
- 'id' => 'expired',
- ], t('Save'));
- $this->assertText('Created the Expired Moderation state.');
-
- // Add a new transition.
- $this->drupalGet('admin/config/workflow/moderation/transitions');
- $this->clickLink(t('Add moderation state transition'));
- $this->drupalPostForm(NULL, [
- 'label' => 'Published » Expired',
- 'id' => 'published_expired',
- 'stateFrom' => 'published',
- 'stateTo' => 'expired',
- ], t('Save'));
- $this->assertText('Created the Published » Expired Moderation state transition.');
-
- // Delete the new transition.
- $this->drupalGet('admin/config/workflow/moderation/transitions/published_expired');
- $this->clickLink('Delete');
- $this->assertText('Are you sure you want to delete Published » Expired?');
- $this->drupalPostForm(NULL, [], t('Delete'));
- $this->assertText('Moderation transition Published » Expired deleted');
- }
-
-}
diff --git a/core/modules/content_moderation/src/Tests/NodeAccessTest.php b/core/modules/content_moderation/src/Tests/NodeAccessTest.php
index 1b05406..7392a7e 100644
--- a/core/modules/content_moderation/src/Tests/NodeAccessTest.php
+++ b/core/modules/content_moderation/src/Tests/NodeAccessTest.php
@@ -15,13 +15,7 @@ class NodeAccessTest extends ModerationStateTestBase {
protected function setUp() {
parent::setUp();
$this->drupalLogin($this->adminUser);
- $this->createContentTypeFromUi(
- 'Moderated content',
- 'moderated_content',
- TRUE,
- ['draft', 'published'],
- 'draft'
- );
+ $this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE);
$this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content');
}
@@ -35,20 +29,11 @@ class NodeAccessTest extends ModerationStateTestBase {
$this->drupalPostForm('node/add/moderated_content', [
'title[0][value]' => 'moderated content',
], t('Save and Create New Draft'));
- $nodes = \Drupal::entityTypeManager()
- ->getStorage('node')
- ->loadByProperties([
- 'title' => 'moderated content',
- ]);
-
- if (!$nodes) {
+ $node = $this->getNodeByTitle('moderated content');
+ if (!$node) {
$this->fail('Test node was not saved correctly.');
- return;
}
- /** @var \Drupal\node\NodeInterface $node */
- $node = reset($nodes);
-
$view_path = 'node/' . $node->id();
$edit_path = 'node/' . $node->id() . '/edit';
$latest_path = 'node/' . $node->id() . '/latest';
@@ -75,8 +60,7 @@ class NodeAccessTest extends ModerationStateTestBase {
// Now make a new user and verify that the new user's access is correct.
$user = $this->createUser([
- 'use draft_draft transition',
- 'use published_draft transition',
+ 'use editorial transition create_new_draft',
'view latest version',
'view any unpublished content',
]);
@@ -92,7 +76,7 @@ class NodeAccessTest extends ModerationStateTestBase {
// Now make another user, who should not be able to see forward revisions.
$user = $this->createUser([
- 'use published_draft transition',
+ 'use editorial transition create_new_draft',
]);
$this->drupalLogin($user);
diff --git a/core/modules/content_moderation/src/ViewsData.php b/core/modules/content_moderation/src/ViewsData.php
index 19bcfa5..b357abc 100644
--- a/core/modules/content_moderation/src/ViewsData.php
+++ b/core/modules/content_moderation/src/ViewsData.php
@@ -204,6 +204,7 @@ class ViewsData {
],
],
],
+ 'field' => ['default_formatter' => 'content_moderation_state'],
];
$revision_table = $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable();
@@ -222,6 +223,7 @@ class ViewsData {
],
],
],
+ 'field' => ['default_formatter' => 'content_moderation_state'],
];
}
diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.latest.yml b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.latest.yml
index 46a64ab..62e972e 100644
--- a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.latest.yml
+++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.latest.yml
@@ -300,9 +300,7 @@ display:
empty_zero: false
hide_alter_empty: true
click_sort_column: target_id
- type: entity_reference_label
- settings:
- link: true
+ type: content_moderation_state
group_column: target_id
group_columns: { }
group_rows: true
diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_base_table_test.yml b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_base_table_test.yml
index 6f95251..343806f 100644
--- a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_base_table_test.yml
+++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_base_table_test.yml
@@ -193,9 +193,7 @@ display:
empty_zero: false
hide_alter_empty: true
click_sort_column: target_id
- type: entity_reference_label
- settings:
- link: false
+ type: content_moderation_state
group_column: target_id
group_columns: { }
group_rows: true
@@ -258,9 +256,7 @@ display:
empty_zero: false
hide_alter_empty: true
click_sort_column: target_id
- type: entity_reference_label
- settings:
- link: false
+ type: content_moderation_state
group_column: target_id
group_columns: { }
group_rows: true
@@ -323,8 +319,7 @@ display:
empty_zero: false
hide_alter_empty: true
click_sort_column: target_id
- type: entity_reference_entity_id
- settings: { }
+ type: string
group_column: target_id
group_columns: { }
group_rows: true
diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_latest_revision.yml b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_latest_revision.yml
index 7673394..4727efa 100644
--- a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_latest_revision.yml
+++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_latest_revision.yml
@@ -306,7 +306,7 @@ display:
empty_zero: false
hide_alter_empty: true
click_sort_column: target_id
- type: entity_reference_entity_id
+ type: string
settings: { }
group_column: target_id
group_columns: { }
@@ -370,7 +370,7 @@ display:
empty_zero: false
hide_alter_empty: true
click_sort_column: target_id
- type: entity_reference_entity_id
+ type: string
settings: { }
group_column: target_id
group_columns: { }
diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_revision_test.yml b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_revision_test.yml
index 2362098..78fca38 100644
--- a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_revision_test.yml
+++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_revision_test.yml
@@ -191,7 +191,7 @@ display:
empty_zero: false
hide_alter_empty: true
click_sort_column: target_id
- type: entity_reference_entity_id
+ type: string
settings: { }
group_column: target_id
group_columns: { }
diff --git a/core/modules/content_moderation/tests/src/Functional/LatestRevisionViewsFilterTest.php b/core/modules/content_moderation/tests/src/Functional/LatestRevisionViewsFilterTest.php
index 77ae046..cf14b23 100644
--- a/core/modules/content_moderation/tests/src/Functional/LatestRevisionViewsFilterTest.php
+++ b/core/modules/content_moderation/tests/src/Functional/LatestRevisionViewsFilterTest.php
@@ -5,6 +5,7 @@ namespace Drupal\Tests\content_moderation\Functional;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\BrowserTestBase;
+use Drupal\workflows\Entity\Workflow;
/**
* Tests the "Latest Revision" views filter.
@@ -25,7 +26,7 @@ class LatestRevisionViewsFilterTest extends BrowserTestBase {
* Tests view shows the correct node IDs.
*/
public function testViewShowsCorrectNids() {
- $node_type = $this->createNodeType('Test', 'test');
+ $this->createNodeType('Test', 'test');
$permissions = [
'access content',
@@ -45,8 +46,9 @@ class LatestRevisionViewsFilterTest extends BrowserTestBase {
$node_0->save();
// Now enable moderation for subsequent nodes.
- $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
- $node_type->save();
+ $workflow = Workflow::load('editorial');
+ $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'test');
+ $workflow->save();
// Make a node that is only ever in Draft.
/** @var Node $node_1 */
@@ -55,7 +57,7 @@ class LatestRevisionViewsFilterTest extends BrowserTestBase {
'title' => 'Node 1 - Rev 1',
'uid' => $editor1->id(),
]);
- $node_1->moderation_state->target_id = 'draft';
+ $node_1->moderation_state->value = 'draft';
$node_1->save();
// Make a node that is in Draft, then Published.
@@ -65,11 +67,11 @@ class LatestRevisionViewsFilterTest extends BrowserTestBase {
'title' => 'Node 2 - Rev 1',
'uid' => $editor1->id(),
]);
- $node_2->moderation_state->target_id = 'draft';
+ $node_2->moderation_state->value = 'draft';
$node_2->save();
$node_2->setTitle('Node 2 - Rev 2');
- $node_2->moderation_state->target_id = 'published';
+ $node_2->moderation_state->value = 'published';
$node_2->save();
// Make a node that is in Draft, then Published, then Draft.
@@ -79,15 +81,15 @@ class LatestRevisionViewsFilterTest extends BrowserTestBase {
'title' => 'Node 3 - Rev 1',
'uid' => $editor1->id(),
]);
- $node_3->moderation_state->target_id = 'draft';
+ $node_3->moderation_state->value = 'draft';
$node_3->save();
$node_3->setTitle('Node 3 - Rev 2');
- $node_3->moderation_state->target_id = 'published';
+ $node_3->moderation_state->value = 'published';
$node_3->save();
$node_3->setTitle('Node 3 - Rev 3');
- $node_3->moderation_state->target_id = 'draft';
+ $node_3->moderation_state->value = 'draft';
$node_3->save();
// Now show the View, and confirm that only the correct titles are showing.
diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationStateAccessTest.php b/core/modules/content_moderation/tests/src/Functional/ModerationStateAccessTest.php
index 799d89a..7d2f746 100644
--- a/core/modules/content_moderation/tests/src/Functional/ModerationStateAccessTest.php
+++ b/core/modules/content_moderation/tests/src/Functional/ModerationStateAccessTest.php
@@ -5,6 +5,7 @@ namespace Drupal\Tests\content_moderation\Functional;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\BrowserTestBase;
+use Drupal\workflows\Entity\Workflow;
/**
* Tests the view access control handler for moderation state entities.
@@ -31,7 +32,7 @@ class ModerationStateAccessTest extends BrowserTestBase {
$permissions = [
'access content',
'view all revisions',
- 'view moderation states',
+ 'view content moderation',
];
$editor1 = $this->drupalCreateUser($permissions);
$this->drupalLogin($editor1);
@@ -41,7 +42,7 @@ class ModerationStateAccessTest extends BrowserTestBase {
'title' => 'Draft node',
'uid' => $editor1->id(),
]);
- $node_1->moderation_state->target_id = 'draft';
+ $node_1->moderation_state->value = 'draft';
$node_1->save();
$node_2 = Node::create([
@@ -49,26 +50,26 @@ class ModerationStateAccessTest extends BrowserTestBase {
'title' => 'Published node',
'uid' => $editor1->id(),
]);
- $node_2->moderation_state->target_id = 'published';
+ $node_2->moderation_state->value = 'published';
$node_2->save();
// Resave the node with a new state.
$node_2->setTitle('Archived node');
- $node_2->moderation_state->target_id = 'archived';
+ $node_2->moderation_state->value = 'archived';
$node_2->save();
// Now show the View, and confirm that the state labels are showing.
$this->drupalGet('/latest');
$page = $this->getSession()->getPage();
- $this->assertTrue($page->hasLink('Draft'));
- $this->assertTrue($page->hasLink('Archived'));
- $this->assertFalse($page->hasLink('Published'));
+ $this->assertTrue($page->hasContent('Draft'));
+ $this->assertTrue($page->hasContent('Archived'));
+ $this->assertFalse($page->hasContent('Published'));
// Now log in as an admin and test the same thing.
$permissions = [
'access content',
'view all revisions',
- 'administer moderation states',
+ 'administer content moderation',
];
$admin1 = $this->drupalCreateUser($permissions);
$this->drupalLogin($admin1);
@@ -76,9 +77,9 @@ class ModerationStateAccessTest extends BrowserTestBase {
$this->drupalGet('/latest');
$page = $this->getSession()->getPage();
$this->assertEquals(200, $this->getSession()->getStatusCode());
- $this->assertTrue($page->hasLink('Draft'));
- $this->assertTrue($page->hasLink('Archived'));
- $this->assertFalse($page->hasLink('Published'));
+ $this->assertTrue($page->hasContent('Draft'));
+ $this->assertTrue($page->hasContent('Archived'));
+ $this->assertFalse($page->hasContent('Published'));
}
/**
@@ -98,9 +99,11 @@ class ModerationStateAccessTest extends BrowserTestBase {
'type' => $machine_name,
'label' => $label,
]);
- $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
$node_type->save();
+ $workflow = Workflow::load('editorial');
+ $workflow->getTypePlugin()->addEntityTypeAndBundle('node', $machine_name);
+ $workflow->save();
return $node_type;
}
diff --git a/core/modules/content_moderation/tests/src/Kernel/ContentModerationSchemaTest.php b/core/modules/content_moderation/tests/src/Kernel/ContentModerationSchemaTest.php
deleted file mode 100644
index 67ce175..0000000
--- a/core/modules/content_moderation/tests/src/Kernel/ContentModerationSchemaTest.php
+++ /dev/null
@@ -1,89 +0,0 @@
-<?php
-
-namespace Drupal\Tests\content_moderation\Kernel;
-
-use Drupal\block_content\Entity\BlockContentType;
-use Drupal\Tests\SchemaCheckTestTrait;
-use Drupal\KernelTests\KernelTestBase;
-use Drupal\node\Entity\NodeType;
-use Drupal\content_moderation\Entity\ModerationState;
-use Drupal\content_moderation\Entity\ModerationStateTransition;
-
-/**
- * Ensures that content moderation schema is correct.
- *
- * @group content_moderation
- */
-class ContentModerationSchemaTest extends KernelTestBase {
-
- use SchemaCheckTestTrait;
-
- /**
- * {@inheritdoc}
- */
- public static $modules = [
- 'content_moderation',
- 'node',
- 'user',
- 'block_content',
- 'system',
- ];
-
- /**
- * Tests content moderation default schema.
- */
- public function testContentModerationDefaultConfig() {
- $this->installConfig(['content_moderation']);
- $typed_config = \Drupal::service('config.typed');
- $moderation_states = ModerationState::loadMultiple();
- foreach ($moderation_states as $moderation_state) {
- $this->assertConfigSchema($typed_config, $moderation_state->getEntityType()->getConfigPrefix() . '.' . $moderation_state->id(), $moderation_state->toArray());
- }
- $moderation_state_transitions = ModerationStateTransition::loadMultiple();
- foreach ($moderation_state_transitions as $moderation_state_transition) {
- $this->assertConfigSchema($typed_config, $moderation_state_transition->getEntityType()->getConfigPrefix() . '.' . $moderation_state_transition->id(), $moderation_state_transition->toArray());
- }
-
- }
-
- /**
- * Tests content moderation third party schema for node types.
- */
- public function testContentModerationNodeTypeConfig() {
- $this->installEntitySchema('node');
- $this->installEntitySchema('user');
- $this->installConfig(['content_moderation']);
- $typed_config = \Drupal::service('config.typed');
- $moderation_states = ModerationState::loadMultiple();
- $node_type = NodeType::create([
- 'type' => 'example',
- ]);
- $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
- $node_type->setThirdPartySetting('content_moderation', 'allowed_moderation_states', array_keys($moderation_states));
- $node_type->setThirdPartySetting('content_moderation', 'default_moderation_state', '');
- $node_type->save();
- $this->assertConfigSchema($typed_config, $node_type->getEntityType()->getConfigPrefix() . '.' . $node_type->id(), $node_type->toArray());
- }
-
- /**
- * Tests content moderation third party schema for block content types.
- */
- public function testContentModerationBlockContentTypeConfig() {
- $this->installEntitySchema('block_content');
- $this->installEntitySchema('user');
- $this->installConfig(['content_moderation']);
- $typed_config = \Drupal::service('config.typed');
- $moderation_states = ModerationState::loadMultiple();
- $block_content_type = BlockContentType::create([
- 'id' => 'basic',
- 'label' => 'basic',
- 'revision' => TRUE,
- ]);
- $block_content_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
- $block_content_type->setThirdPartySetting('content_moderation', 'allowed_moderation_states', array_keys($moderation_states));
- $block_content_type->setThirdPartySetting('content_moderation', 'default_moderation_state', '');
- $block_content_type->save();
- $this->assertConfigSchema($typed_config, $block_content_type->getEntityType()->getConfigPrefix() . '.' . $block_content_type->id(), $block_content_type->toArray());
- }
-
-}
diff --git a/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php b/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php
index 67a7a74..b5b5a75 100644
--- a/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php
+++ b/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php
@@ -3,7 +3,6 @@
namespace Drupal\Tests\content_moderation\Kernel;
use Drupal\content_moderation\Entity\ContentModerationState;
-use Drupal\content_moderation\Entity\ModerationState;
use Drupal\entity_test\Entity\EntityTestBundle;
use Drupal\entity_test\Entity\EntityTestWithBundle;
use Drupal\KernelTests\KernelTestBase;
@@ -11,6 +10,7 @@ use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\node\NodeInterface;
+use Drupal\workflows\Entity\Workflow;
/**
* Tests links between a content entity and a content_moderation_state entity.
@@ -31,6 +31,7 @@ class ContentModerationStateTest extends KernelTestBase {
'language',
'content_translation',
'text',
+ 'workflows',
];
/**
@@ -54,24 +55,25 @@ class ContentModerationStateTest extends KernelTestBase {
$node_type = NodeType::create([
'type' => 'example',
]);
- $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
- $node_type->setThirdPartySetting('content_moderation', 'allowed_moderation_states', ['draft', 'published']);
- $node_type->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft');
$node_type->save();
+
+ $workflow = Workflow::load('editorial');
+ $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
+ $workflow->save();
+
$node = Node::create([
'type' => 'example',
'title' => 'Test title',
]);
$node->save();
$node = $this->reloadNode($node);
- $this->assertEquals('draft', $node->moderation_state->entity->id());
+ $this->assertEquals('draft', $node->moderation_state->value);
- $published = ModerationState::load('published');
- $node->moderation_state->entity = $published;
+ $node->moderation_state->value = 'published';
$node->save();
$node = $this->reloadNode($node);
- $this->assertEquals('published', $node->moderation_state->entity->id());
+ $this->assertEquals('published', $node->moderation_state->value);
// Change the state without saving the node.
$content_moderation_state = ContentModerationState::load(1);
@@ -80,7 +82,7 @@ class ContentModerationStateTest extends KernelTestBase {
$content_moderation_state->save();
$node = $this->reloadNode($node, 3);
- $this->assertEquals('draft', $node->moderation_state->entity->id());
+ $this->assertEquals('draft', $node->moderation_state->value);
$this->assertFalse($node->isPublished());
// Get the default revision.
@@ -88,11 +90,11 @@ class ContentModerationStateTest extends KernelTestBase {
$this->assertTrue($node->isPublished());
$this->assertEquals(2, $node->getRevisionId());
- $node->moderation_state->target_id = 'published';
+ $node->moderation_state->value = 'published';
$node->save();
$node = $this->reloadNode($node, 4);
- $this->assertEquals('published', $node->moderation_state->entity->id());
+ $this->assertEquals('published', $node->moderation_state->value);
// Get the default revision.
$node = $this->reloadNode($node);
@@ -110,10 +112,12 @@ class ContentModerationStateTest extends KernelTestBase {
$node_type = NodeType::create([
'type' => 'example',
]);
- $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
- $node_type->setThirdPartySetting('content_moderation', 'allowed_moderation_states', ['draft', 'published']);
- $node_type->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft');
$node_type->save();
+
+ $workflow = Workflow::load('editorial');
+ $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
+ $workflow->save();
+
$english_node = Node::create([
'type' => 'example',
'title' => 'Test title',
@@ -122,7 +126,7 @@ class ContentModerationStateTest extends KernelTestBase {
$english_node
->setPublished(FALSE)
->save();
- $this->assertEquals('draft', $english_node->moderation_state->entity->id());
+ $this->assertEquals('draft', $english_node->moderation_state->value);
$this->assertFalse($english_node->isPublished());
// Create a French translation.
@@ -131,34 +135,34 @@ class ContentModerationStateTest extends KernelTestBase {
// Revision 1 (fr).
$french_node->save();
$french_node = $this->reloadNode($english_node)->getTranslation('fr');
- $this->assertEquals('draft', $french_node->moderation_state->entity->id());
+ $this->assertEquals('draft', $french_node->moderation_state->value);
$this->assertFalse($french_node->isPublished());
// Move English node to create another draft.
$english_node = $this->reloadNode($english_node);
- $english_node->moderation_state->target_id = 'draft';
+ $english_node->moderation_state->value = 'draft';
// Revision 2 (en, fr).
$english_node->save();
$english_node = $this->reloadNode($english_node);
- $this->assertEquals('draft', $english_node->moderation_state->entity->id());
+ $this->assertEquals('draft', $english_node->moderation_state->value);
// French node should still be in draft.
$french_node = $this->reloadNode($english_node)->getTranslation('fr');
- $this->assertEquals('draft', $french_node->moderation_state->entity->id());
+ $this->assertEquals('draft', $french_node->moderation_state->value);
// Publish the French node.
- $french_node->moderation_state->target_id = 'published';
+ $french_node->moderation_state->value = 'published';
// Revision 3 (en, fr).
$french_node->save();
$french_node = $this->reloadNode($french_node)->getTranslation('fr');
$this->assertTrue($french_node->isPublished());
- $this->assertEquals('published', $french_node->moderation_state->entity->id());
+ $this->assertEquals('published', $french_node->moderation_state->value);
$this->assertTrue($french_node->isPublished());
$english_node = $french_node->getTranslation('en');
- $this->assertEquals('draft', $english_node->moderation_state->entity->id());
+ $this->assertEquals('draft', $english_node->moderation_state->value);
// Publish the English node.
- $english_node->moderation_state->target_id = 'published';
+ $english_node->moderation_state->value = 'published';
// Revision 4 (en, fr).
$english_node->save();
$english_node = $this->reloadNode($english_node);
@@ -167,7 +171,7 @@ class ContentModerationStateTest extends KernelTestBase {
// Move the French node back to draft.
$french_node = $this->reloadNode($english_node)->getTranslation('fr');
$this->assertTrue($french_node->isPublished());
- $french_node->moderation_state->target_id = 'draft';
+ $french_node->moderation_state->value = 'draft';
// Revision 5 (en, fr).
$french_node->save();
$french_node = $this->reloadNode($english_node, 5)->getTranslation('fr');
@@ -175,7 +179,7 @@ class ContentModerationStateTest extends KernelTestBase {
$this->assertTrue($french_node->getTranslation('en')->isPublished());
// Republish the French node.
- $french_node->moderation_state->target_id = 'published';
+ $french_node->moderation_state->value = 'published';
// Revision 6 (en, fr).
$french_node->save();
$french_node = $this->reloadNode($english_node)->getTranslation('fr');
@@ -189,9 +193,9 @@ class ContentModerationStateTest extends KernelTestBase {
$content_moderation_state->save();
$english_node = $this->reloadNode($french_node, $french_node->getRevisionId() + 1);
- $this->assertEquals('draft', $english_node->moderation_state->entity->id());
+ $this->assertEquals('draft', $english_node->moderation_state->value);
$french_node = $this->reloadNode($english_node)->getTranslation('fr');
- $this->assertEquals('published', $french_node->moderation_state->entity->id());
+ $this->assertEquals('published', $french_node->moderation_state->value);
// This should unpublish the French node.
$content_moderation_state = ContentModerationState::load(1);
@@ -202,9 +206,9 @@ class ContentModerationStateTest extends KernelTestBase {
$content_moderation_state->save();
$english_node = $this->reloadNode($english_node, $english_node->getRevisionId());
- $this->assertEquals('draft', $english_node->moderation_state->entity->id());
+ $this->assertEquals('draft', $english_node->moderation_state->value);
$french_node = $this->reloadNode($english_node, '8')->getTranslation('fr');
- $this->assertEquals('draft', $french_node->moderation_state->entity->id());
+ $this->assertEquals('draft', $french_node->moderation_state->value);
// Switching the moderation state to an unpublished state should update the
// entity.
$this->assertFalse($french_node->isPublished());
@@ -231,14 +235,12 @@ class ContentModerationStateTest extends KernelTestBase {
$entity_test_bundle = EntityTestBundle::create([
'id' => 'example',
]);
- $entity_test_bundle->setThirdPartySetting('content_moderation', 'enabled', TRUE);
- $entity_test_bundle->setThirdPartySetting('content_moderation', 'allowed_moderation_states', [
- 'draft',
- 'published'
- ]);
- $entity_test_bundle->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft');
$entity_test_bundle->save();
+ $workflow = Workflow::load('editorial');
+ $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_with_bundle', 'example');
+ $workflow->save();
+
// Check that the tested entity type is not translatable.
$entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_with_bundle');
$this->assertFalse($entity_type->isTranslatable(), 'The test entity type is not translatable.');
@@ -248,12 +250,12 @@ class ContentModerationStateTest extends KernelTestBase {
'type' => 'example'
]);
$entity_test_with_bundle->save();
- $this->assertEquals('draft', $entity_test_with_bundle->moderation_state->entity->id());
+ $this->assertEquals('draft', $entity_test_with_bundle->moderation_state->value);
- $entity_test_with_bundle->moderation_state->target_id = 'published';
+ $entity_test_with_bundle->moderation_state->value = 'published';
$entity_test_with_bundle->save();
- $this->assertEquals('published', EntityTestWithBundle::load($entity_test_with_bundle->id())->moderation_state->entity->id());
+ $this->assertEquals('published', EntityTestWithBundle::load($entity_test_with_bundle->id())->moderation_state->value);
}
/**
@@ -275,14 +277,12 @@ class ContentModerationStateTest extends KernelTestBase {
$entity_test_bundle = EntityTestBundle::create([
'id' => 'example',
]);
- $entity_test_bundle->setThirdPartySetting('content_moderation', 'enabled', TRUE);
- $entity_test_bundle->setThirdPartySetting('content_moderation', 'allowed_moderation_states', [
- 'draft',
- 'published'
- ]);
- $entity_test_bundle->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft');
$entity_test_bundle->save();
+ $workflow = Workflow::load('editorial');
+ $workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_with_bundle', 'example');
+ $workflow->save();
+
// Check that the tested entity type is not translatable.
$entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_with_bundle');
$this->assertFalse($entity_type->isTranslatable(), 'The test entity type is not translatable.');
@@ -292,12 +292,12 @@ class ContentModerationStateTest extends KernelTestBase {
'type' => 'example'
]);
$entity_test_with_bundle->save();
- $this->assertEquals('draft', $entity_test_with_bundle->moderation_state->entity->id());
+ $this->assertEquals('draft', $entity_test_with_bundle->moderation_state->value);
- $entity_test_with_bundle->moderation_state->target_id = 'published';
+ $entity_test_with_bundle->moderation_state->value = 'published';
$entity_test_with_bundle->save();
- $this->assertEquals('published', EntityTestWithBundle::load($entity_test_with_bundle->id())->moderation_state->entity->id());
+ $this->assertEquals('published', EntityTestWithBundle::load($entity_test_with_bundle->id())->moderation_state->value);
}
/**
diff --git a/core/modules/content_moderation/tests/src/Kernel/EntityOperationsTest.php b/core/modules/content_moderation/tests/src/Kernel/EntityOperationsTest.php
index 929356e..60e9edf 100644
--- a/core/modules/content_moderation/tests/src/Kernel/EntityOperationsTest.php
+++ b/core/modules/content_moderation/tests/src/Kernel/EntityOperationsTest.php
@@ -4,9 +4,9 @@ namespace Drupal\Tests\content_moderation\Kernel;
use Drupal\KernelTests\KernelTestBase;
-use Drupal\content_moderation\Entity\ModerationState;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
+use Drupal\workflows\Entity\Workflow;
/**
* @coversDefaultClass \Drupal\content_moderation\EntityOperations
@@ -23,6 +23,7 @@ class EntityOperationsTest extends KernelTestBase {
'node',
'user',
'system',
+ 'workflows',
];
/**
@@ -47,8 +48,10 @@ class EntityOperationsTest extends KernelTestBase {
'type' => 'page',
'label' => 'Page',
]);
- $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
$node_type->save();
+ $workflow = Workflow::load('editorial');
+ $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'page');
+ $workflow->save();
}
/**
@@ -60,7 +63,7 @@ class EntityOperationsTest extends KernelTestBase {
'type' => 'page',
'title' => 'A',
]);
- $page->moderation_state->target_id = 'draft';
+ $page->moderation_state->value = 'draft';
$page->save();
$id = $page->id();
@@ -75,7 +78,7 @@ class EntityOperationsTest extends KernelTestBase {
// Moderate the entity to published.
$page->setTitle('B');
- $page->moderation_state->target_id = 'published';
+ $page->moderation_state->value = 'published';
$page->save();
// Verify the entity is now published and public.
@@ -86,7 +89,7 @@ class EntityOperationsTest extends KernelTestBase {
// Make a new forward-revision in Draft.
$page->setTitle('C');
- $page->moderation_state->target_id = 'draft';
+ $page->moderation_state->value = 'draft';
$page->save();
// Verify normal loads return the still-default previous version.
@@ -105,7 +108,7 @@ class EntityOperationsTest extends KernelTestBase {
$this->assertEquals('C', $page->getTitle());
$page->setTitle('D');
- $page->moderation_state->target_id = 'published';
+ $page->moderation_state->value = 'published';
$page->save();
// Verify normal loads return the still-default previous version.
@@ -116,7 +119,7 @@ class EntityOperationsTest extends KernelTestBase {
// Now check that we can immediately add a new published revision over it.
$page->setTitle('E');
- $page->moderation_state->target_id = 'published';
+ $page->moderation_state->value = 'published';
$page->save();
$page = Node::load($id);
@@ -134,7 +137,7 @@ class EntityOperationsTest extends KernelTestBase {
'type' => 'page',
'title' => 'A',
]);
- $page->moderation_state->target_id = 'published';
+ $page->moderation_state->value = 'published';
$page->save();
$id = $page->id();
@@ -151,29 +154,12 @@ class EntityOperationsTest extends KernelTestBase {
* Verifies that an unpublished state may be made the default revision.
*/
public function testArchive() {
- $published_id = $this->randomMachineName();
- $published_state = ModerationState::create([
- 'id' => $published_id,
- 'label' => $this->randomString(),
- 'published' => TRUE,
- 'default_revision' => TRUE,
- ]);
- $published_state->save();
-
- $archived_id = $this->randomMachineName();
- $archived_state = ModerationState::create([
- 'id' => $archived_id,
- 'label' => $this->randomString(),
- 'published' => FALSE,
- 'default_revision' => TRUE,
- ]);
- $archived_state->save();
-
$page = Node::create([
'type' => 'page',
'title' => $this->randomString(),
]);
- $page->moderation_state->target_id = $published_id;
+
+ $page->moderation_state->value = 'published';
$page->save();
$id = $page->id();
@@ -184,7 +170,7 @@ class EntityOperationsTest extends KernelTestBase {
// When the page is moderated to the archived state, then the latest
// revision should be the default revision, and it should be unpublished.
- $page->moderation_state->target_id = $archived_id;
+ $page->moderation_state->value = 'archived';
$page->save();
$new_revision_id = $page->getRevisionId();
diff --git a/core/modules/content_moderation/tests/src/Kernel/EntityRevisionConverterTest.php b/core/modules/content_moderation/tests/src/Kernel/EntityRevisionConverterTest.php
index 89c84f9..6f209d1 100644
--- a/core/modules/content_moderation/tests/src/Kernel/EntityRevisionConverterTest.php
+++ b/core/modules/content_moderation/tests/src/Kernel/EntityRevisionConverterTest.php
@@ -6,6 +6,7 @@ use Drupal\entity_test\Entity\EntityTest;
use Drupal\KernelTests\KernelTestBase;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
+use Drupal\workflows\Entity\Workflow;
/**
* @coversDefaultClass \Drupal\content_moderation\ParamConverter\EntityRevisionConverter
@@ -19,6 +20,7 @@ class EntityRevisionConverterTest extends KernelTestBase {
'system',
'content_moderation',
'node',
+ 'workflows',
];
/**
@@ -59,17 +61,21 @@ class EntityRevisionConverterTest extends KernelTestBase {
* @covers ::convert
*/
public function testConvertWithRevisionableEntityType() {
+ $this->installConfig(['content_moderation']);
$node_type = NodeType::create([
'type' => 'article',
]);
- $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
$node_type->save();
+ $workflow = Workflow::load('editorial');
+ $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'article');
+ $workflow->save();
$revision_ids = [];
$node = Node::create([
'title' => 'test',
'type' => 'article',
]);
+ $node->moderation_state->value = 'published';
$node->save();
$revision_ids[] = $node->getRevisionId();
@@ -79,7 +85,7 @@ class EntityRevisionConverterTest extends KernelTestBase {
$revision_ids[] = $node->getRevisionId();
$node->setNewRevision(TRUE);
- $node->isDefaultRevision(FALSE);
+ $node->moderation_state->value = 'draft';
$node->save();
$revision_ids[] = $node->getRevisionId();
diff --git a/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php b/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php
index 97e61f1..7c4f97d 100644
--- a/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php
+++ b/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php
@@ -6,6 +6,7 @@ use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
+use Drupal\workflows\Entity\Workflow;
/**
* @coversDefaultClass \Drupal\content_moderation\Plugin\Validation\Constraint\ModerationStateConstraintValidator
@@ -23,6 +24,7 @@ class EntityStateChangeValidationTest extends KernelTestBase {
'system',
'language',
'content_translation',
+ 'workflows',
];
/**
@@ -47,20 +49,23 @@ class EntityStateChangeValidationTest extends KernelTestBase {
$node_type = NodeType::create([
'type' => 'example',
]);
- $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
$node_type->save();
+ $workflow = Workflow::load('editorial');
+ $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
+ $workflow->save();
+
$node = Node::create([
'type' => 'example',
'title' => 'Test title',
]);
- $node->moderation_state->target_id = 'draft';
+ $node->moderation_state->value = 'draft';
$node->save();
- $node->moderation_state->target_id = 'published';
+ $node->moderation_state->value = 'published';
$this->assertCount(0, $node->validate());
$node->save();
- $this->assertEquals('published', $node->moderation_state->entity->id());
+ $this->assertEquals('published', $node->moderation_state->value);
}
/**
@@ -72,16 +77,19 @@ class EntityStateChangeValidationTest extends KernelTestBase {
$node_type = NodeType::create([
'type' => 'example',
]);
- $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
$node_type->save();
+ $workflow = Workflow::load('editorial');
+ $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
+ $workflow->save();
+
$node = Node::create([
'type' => 'example',
'title' => 'Test title',
]);
- $node->moderation_state->target_id = 'draft';
+ $node->moderation_state->value = 'draft';
$node->save();
- $node->moderation_state->target_id = 'archived';
+ $node->moderation_state->value = 'archived';
$violations = $node->validate();
$this->assertCount(1, $violations);
@@ -106,12 +114,9 @@ class EntityStateChangeValidationTest extends KernelTestBase {
$nid = $node->id();
// Enable moderation for our node type.
- /** @var NodeType $node_type */
- $node_type = NodeType::load('example');
- $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
- $node_type->setThirdPartySetting('content_moderation', 'allowed_moderation_states', ['draft', 'published']);
- $node_type->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft');
- $node_type->save();
+ $workflow = Workflow::load('editorial');
+ $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
+ $workflow->save();
$node = Node::load($nid);
@@ -155,12 +160,9 @@ class EntityStateChangeValidationTest extends KernelTestBase {
$node_fr->save();
// Enable moderation for our node type.
- /** @var NodeType $node_type */
- $node_type = NodeType::load('example');
- $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
- $node_type->setThirdPartySetting('content_moderation', 'allowed_moderation_states', ['draft', 'published']);
- $node_type->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft');
- $node_type->save();
+ $workflow = Workflow::load('editorial');
+ $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
+ $workflow->save();
// Reload the French version of the node.
$node = Node::load($nid);
diff --git a/core/modules/content_moderation/tests/src/Kernel/ModerationStateEntityTest.php b/core/modules/content_moderation/tests/src/Kernel/ModerationStateEntityTest.php
deleted file mode 100644
index f312cde..0000000
--- a/core/modules/content_moderation/tests/src/Kernel/ModerationStateEntityTest.php
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-
-namespace Drupal\Tests\content_moderation\Kernel;
-
-use Drupal\KernelTests\KernelTestBase;
-use Drupal\content_moderation\Entity\ModerationState;
-
-/**
- * @coversDefaultClass \Drupal\content_moderation\Entity\ModerationState
- *
- * @group content_moderation
- */
-class ModerationStateEntityTest extends KernelTestBase {
-
- /**
- * {@inheritdoc}
- */
- public static $modules = ['content_moderation'];
-
- /**
- * {@inheritdoc}
- */
- protected function setUp() {
- parent::setUp();
-
- $this->installEntitySchema('moderation_state');
- }
-
- /**
- * Verify moderation state methods based on entity properties.
- *
- * @covers ::isPublishedState
- * @covers ::isDefaultRevisionState
- *
- * @dataProvider moderationStateProvider
- */
- public function testModerationStateProperties($published, $default_revision, $is_published, $is_default) {
- $moderation_state_id = $this->randomMachineName();
- $moderation_state = ModerationState::create([
- 'id' => $moderation_state_id,
- 'label' => $this->randomString(),
- 'published' => $published,
- 'default_revision' => $default_revision,
- ]);
- $moderation_state->save();
-
- $moderation_state = ModerationState::load($moderation_state_id);
- $this->assertEquals($is_published, $moderation_state->isPublishedState());
- $this->assertEquals($is_default, $moderation_state->isDefaultRevisionState());
- }
-
- /**
- * Data provider for ::testModerationStateProperties.
- */
- public function moderationStateProvider() {
- return [
- // Draft, Needs review; should not touch the default revision.
- [FALSE, FALSE, FALSE, FALSE],
- // Published; this state should update and publish the default revision.
- [TRUE, TRUE, TRUE, TRUE],
- // Archive; this state should update but not publish the default revision.
- [FALSE, TRUE, FALSE, TRUE],
- // We try to prevent creating this state via the UI, but when a moderation
- // state is a published state, it should also become the default revision.
- [TRUE, FALSE, TRUE, TRUE],
- ];
- }
-
-}
diff --git a/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php b/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php
index c57963e..3f984da 100644
--- a/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php
+++ b/core/modules/content_moderation/tests/src/Kernel/ModerationStateFieldItemListTest.php
@@ -5,6 +5,7 @@ namespace Drupal\Tests\content_moderation\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
+use Drupal\workflows\Entity\Workflow;
/**
* @coversDefaultClass \Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList
@@ -22,6 +23,7 @@ class ModerationStateFieldItemListTest extends KernelTestBase {
'user',
'system',
'language',
+ 'workflows',
];
/**
@@ -44,10 +46,11 @@ class ModerationStateFieldItemListTest extends KernelTestBase {
$node_type = NodeType::create([
'type' => 'example',
]);
- $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
- $node_type->setThirdPartySetting('content_moderation', 'allowed_moderation_states', ['draft']);
- $node_type->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft');
$node_type->save();
+ $workflow = Workflow::load('editorial');
+ $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
+ $workflow->save();
+
$this->testNode = Node::create([
'type' => 'example',
'title' => 'Test title',
@@ -61,7 +64,7 @@ class ModerationStateFieldItemListTest extends KernelTestBase {
* Test the field item list when accessing an index.
*/
public function testArrayIndex() {
- $this->assertEquals('draft', $this->testNode->moderation_state[0]->entity->id());
+ $this->assertEquals('draft', $this->testNode->moderation_state[0]->value);
}
/**
@@ -70,7 +73,7 @@ class ModerationStateFieldItemListTest extends KernelTestBase {
public function testArrayIteration() {
$states = [];
foreach ($this->testNode->moderation_state as $item) {
- $states[] = $item->entity->id();
+ $states[] = $item->value;
}
$this->assertEquals(['draft'], $states);
}
diff --git a/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php b/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php
index c869619..6b127c8 100644
--- a/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php
+++ b/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php
@@ -6,6 +6,7 @@ use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
use Drupal\views\Views;
+use Drupal\workflows\Entity\Workflow;
/**
* Tests the views integration of content_moderation.
@@ -21,6 +22,7 @@ class ViewsDataIntegrationTest extends ViewsKernelTestBase {
'content_moderation_test_views',
'node',
'content_moderation',
+ 'workflows',
];
/**
@@ -39,8 +41,10 @@ class ViewsDataIntegrationTest extends ViewsKernelTestBase {
$node_type = NodeType::create([
'type' => 'page',
]);
- $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE);
$node_type->save();
+ $workflow = Workflow::load('editorial');
+ $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'page');
+ $workflow->save();
}
/**
@@ -53,14 +57,14 @@ class ViewsDataIntegrationTest extends ViewsKernelTestBase {
'type' => 'page',
'title' => 'Test title first revision',
]);
- $node->moderation_state->target_id = 'published';
+ $node->moderation_state->value = 'published';
$node->save();
$revision = clone $node;
$revision->setNewRevision(TRUE);
$revision->isDefaultRevision(FALSE);
$revision->title->value = 'Test title second revision';
- $revision->moderation_state->target_id = 'draft';
+ $revision->moderation_state->value = 'draft';
$revision->save();
$view = Views::getView('test_content_moderation_latest_revision');
@@ -90,14 +94,14 @@ class ViewsDataIntegrationTest extends ViewsKernelTestBase {
'type' => 'page',
'title' => 'Test title first revision',
]);
- $node->moderation_state->target_id = 'published';
+ $node->moderation_state->value = 'published';
$node->save();
$revision = clone $node;
$revision->setNewRevision(TRUE);
$revision->isDefaultRevision(FALSE);
$revision->title->value = 'Test title second revision';
- $revision->moderation_state->target_id = 'draft';
+ $revision->moderation_state->value = 'draft';
$revision->save();
$view = Views::getView('test_content_moderation_revision_test');
@@ -124,14 +128,14 @@ class ViewsDataIntegrationTest extends ViewsKernelTestBase {
'type' => 'page',
'title' => 'Test title first revision',
]);
- $node->moderation_state->target_id = 'published';
+ $node->moderation_state->value = 'published';
$node->save();
$revision = clone $node;
$revision->setNewRevision(TRUE);
$revision->isDefaultRevision(FALSE);
$revision->title->value = 'Test title second revision';
- $revision->moderation_state->target_id = 'draft';
+ $revision->moderation_state->value = 'draft';
$revision->save();
$view = Views::getView('test_content_moderation_base_table_test');
diff --git a/core/modules/content_moderation/tests/src/Unit/ModerationInformationTest.php b/core/modules/content_moderation/tests/src/Unit/ModerationInformationTest.php
index ff46e41..d7a7373 100644
--- a/core/modules/content_moderation/tests/src/Unit/ModerationInformationTest.php
+++ b/core/modules/content_moderation/tests/src/Unit/ModerationInformationTest.php
@@ -3,13 +3,14 @@
namespace Drupal\Tests\content_moderation\Unit;
use Drupal\content_moderation\Entity\Handler\ModerationHandler;
-use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityType;
use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\content_moderation\ModerationInformation;
+use Drupal\workflows\WorkflowInterface;
/**
* @coversDefaultClass \Drupal\content_moderation\ModerationInformation
@@ -30,43 +31,42 @@ class ModerationInformationTest extends \PHPUnit_Framework_TestCase {
/**
* Returns a mock Entity Type Manager.
*
- * @param \Drupal\Core\Entity\EntityStorageInterface $entity_bundle_storage
- * Entity bundle storage.
- *
* @return EntityTypeManagerInterface
* The mocked entity type manager.
*/
- protected function getEntityTypeManager(EntityStorageInterface $entity_bundle_storage) {
+ protected function getEntityTypeManager() {
$entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class);
- $entity_type_manager->getStorage('entity_test_bundle')->willReturn($entity_bundle_storage);
return $entity_type_manager->reveal();
}
/**
* Sets up content moderation and entity manager mocking.
*
- * @param bool $status
- * TRUE if content_moderation should be enabled, FALSE if not.
+ * @param string $bundle
+ * The bundle ID.
+ * @param string|null $workflow
+ * The workflow ID. If nul no workflow information is added to the bundle.
*
* @return \Drupal\Core\Entity\EntityTypeManagerInterface
* The mocked entity type manager.
*/
- public function setupModerationEntityManager($status) {
- $bundle = $this->prophesize(ConfigEntityInterface::class);
- $bundle->getThirdPartySetting('content_moderation', 'enabled', FALSE)->willReturn($status);
-
- $entity_storage = $this->prophesize(EntityStorageInterface::class);
- $entity_storage->load('test_bundle')->willReturn($bundle->reveal());
-
- return $this->getEntityTypeManager($entity_storage->reveal());
+ public function setupModerationBundleInfo($bundle, $workflow = NULL) {
+ $bundle_info_array = [];
+ if ($workflow) {
+ $bundle_info_array['workflow'] = $workflow;
+ }
+ $bundle_info = $this->prophesize(EntityTypeBundleInfoInterface::class);
+ $bundle_info->getBundleInfo("test_entity_type")->willReturn([$bundle => $bundle_info_array]);
+
+ return $bundle_info->reveal();
}
/**
- * @dataProvider providerBoolean
+ * @dataProvider providerWorkflow
* @covers ::isModeratedEntity
*/
- public function testIsModeratedEntity($status) {
- $moderation_information = new ModerationInformation($this->setupModerationEntityManager($status), $this->getUser());
+ public function testIsModeratedEntity($workflow, $expected) {
+ $moderation_information = new ModerationInformation($this->getEntityTypeManager(), $this->setupModerationBundleInfo('test_bundle', $workflow));
$entity_type = new ContentEntityType([
'id' => 'test_entity_type',
@@ -77,50 +77,55 @@ class ModerationInformationTest extends \PHPUnit_Framework_TestCase {
$entity->getEntityType()->willReturn($entity_type);
$entity->bundle()->willReturn('test_bundle');
- $this->assertEquals($status, $moderation_information->isModeratedEntity($entity->reveal()));
+ $this->assertEquals($expected, $moderation_information->isModeratedEntity($entity->reveal()));
}
/**
- * @covers ::isModeratedEntity
+ * @dataProvider providerWorkflow
+ * @covers ::getWorkFlowForEntity
*/
- public function testIsModeratedEntityForNonBundleEntityType() {
- $entity_type = new ContentEntityType([
- 'id' => 'test_entity_type',
- ]);
+ public function testGetWorkFlowForEntity($workflow) {
+ $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class);
+ if ($workflow) {
+ $workflow_entity = $this->prophesize(WorkflowInterface::class)->reveal();
+ $workflow_storage = $this->prophesize(EntityStorageInterface::class);
+ $workflow_storage->load('workflow')->willReturn($workflow_entity)->shouldBeCalled();
+ $entity_type_manager->getStorage('workflow')->willReturn($workflow_storage->reveal());
+ }
+ else {
+ $workflow_entity = NULL;
+ }
+ $moderation_information = new ModerationInformation($entity_type_manager->reveal(), $this->setupModerationBundleInfo('test_bundle', $workflow));
$entity = $this->prophesize(ContentEntityInterface::class);
- $entity->getEntityType()->willReturn($entity_type);
- $entity->bundle()->willReturn('test_entity_type');
-
- $entity_storage = $this->prophesize(EntityStorageInterface::class);
- $entity_type_manager = $this->getEntityTypeManager($entity_storage->reveal());
- $moderation_information = new ModerationInformation($entity_type_manager, $this->getUser());
+ $entity->getEntityTypeId()->willReturn('test_entity_type');
+ $entity->bundle()->willReturn('test_bundle');
- $this->assertEquals(FALSE, $moderation_information->isModeratedEntity($entity->reveal()));
+ $this->assertEquals($workflow_entity, $moderation_information->getWorkFlowForEntity($entity->reveal()));
}
/**
- * @dataProvider providerBoolean
+ * @dataProvider providerWorkflow
* @covers ::shouldModerateEntitiesOfBundle
*/
- public function testShouldModerateEntities($status) {
+ public function testShouldModerateEntities($workflow, $expected) {
$entity_type = new ContentEntityType([
'id' => 'test_entity_type',
'bundle_entity_type' => 'entity_test_bundle',
'handlers' => ['moderation' => ModerationHandler::class],
]);
- $moderation_information = new ModerationInformation($this->setupModerationEntityManager($status), $this->getUser());
+ $moderation_information = new ModerationInformation($this->getEntityTypeManager(), $this->setupModerationBundleInfo('test_bundle', $workflow));
- $this->assertEquals($status, $moderation_information->shouldModerateEntitiesOfBundle($entity_type, 'test_bundle'));
+ $this->assertEquals($expected, $moderation_information->shouldModerateEntitiesOfBundle($entity_type, 'test_bundle'));
}
/**
* Data provider for several tests.
*/
- public function providerBoolean() {
+ public function providerWorkflow() {
return [
- [FALSE],
- [TRUE],
+ [NULL, FALSE],
+ ['workflow', TRUE],
];
}
diff --git a/core/modules/content_moderation/tests/src/Unit/StateTransitionValidationTest.php b/core/modules/content_moderation/tests/src/Unit/StateTransitionValidationTest.php
index b057478..e518aed 100644
--- a/core/modules/content_moderation/tests/src/Unit/StateTransitionValidationTest.php
+++ b/core/modules/content_moderation/tests/src/Unit/StateTransitionValidationTest.php
@@ -2,13 +2,14 @@
namespace Drupal\Tests\content_moderation\Unit;
-use Drupal\Core\Entity\EntityStorageInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\Entity\Query\QueryFactory;
+use Drupal\content_moderation\ModerationInformationInterface;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Session\AccountInterface;
-use Drupal\content_moderation\ModerationStateInterface;
-use Drupal\content_moderation\ModerationStateTransitionInterface;
use Drupal\content_moderation\StateTransitionValidation;
+use Drupal\workflows\Entity\Workflow;
+use Drupal\workflows\WorkflowTypeInterface;
+use Drupal\workflows\WorkflowTypeManager;
use Prophecy\Argument;
/**
@@ -18,216 +19,6 @@ use Prophecy\Argument;
class StateTransitionValidationTest extends \PHPUnit_Framework_TestCase {
/**
- * Builds a mock storage object for Transitions.
- *
- * @return EntityStorageInterface
- * The mocked storage object for Transitions.
- */
- protected function setupTransitionStorage() {
- $entity_storage = $this->prophesize(EntityStorageInterface::class);
-
- $list = $this->setupTransitionEntityList();
- $entity_storage->loadMultiple()->willReturn($list);
- $entity_storage->loadMultiple(Argument::type('array'))->will(function ($args) use ($list) {
- $keys = $args[0];
- if (empty($keys)) {
- return $list;
- }
-
- $return = array_map(function($key) use ($list) {
- return $list[$key];
- }, $keys);
-
- return $return;
- });
- return $entity_storage->reveal();
- }
-
- /**
- * Builds an array of mocked Transition objects.
- *
- * @return ModerationStateTransitionInterface[]
- * An array of mocked Transition objects.
- */
- protected function setupTransitionEntityList() {
- $transition = $this->prophesize(ModerationStateTransitionInterface::class);
- $transition->id()->willReturn('draft__needs_review');
- $transition->getFromState()->willReturn('draft');
- $transition->getToState()->willReturn('needs_review');
- $list[$transition->reveal()->id()] = $transition->reveal();
-
- $transition = $this->prophesize(ModerationStateTransitionInterface::class);
- $transition->id()->willReturn('needs_review__staging');
- $transition->getFromState()->willReturn('needs_review');
- $transition->getToState()->willReturn('staging');
- $list[$transition->reveal()->id()] = $transition->reveal();
-
- $transition = $this->prophesize(ModerationStateTransitionInterface::class);
- $transition->id()->willReturn('staging__published');
- $transition->getFromState()->willReturn('staging');
- $transition->getToState()->willReturn('published');
- $list[$transition->reveal()->id()] = $transition->reveal();
-
- $transition = $this->prophesize(ModerationStateTransitionInterface::class);
- $transition->id()->willReturn('needs_review__draft');
- $transition->getFromState()->willReturn('needs_review');
- $transition->getToState()->willReturn('draft');
- $list[$transition->reveal()->id()] = $transition->reveal();
-
- $transition = $this->prophesize(ModerationStateTransitionInterface::class);
- $transition->id()->willReturn('draft__draft');
- $transition->getFromState()->willReturn('draft');
- $transition->getToState()->willReturn('draft');
- $list[$transition->reveal()->id()] = $transition->reveal();
-
- $transition = $this->prophesize(ModerationStateTransitionInterface::class);
- $transition->id()->willReturn('needs_review__needs_review');
- $transition->getFromState()->willReturn('needs_review');
- $transition->getToState()->willReturn('needs_review');
- $list[$transition->reveal()->id()] = $transition->reveal();
-
- $transition = $this->prophesize(ModerationStateTransitionInterface::class);
- $transition->id()->willReturn('published__published');
- $transition->getFromState()->willReturn('published');
- $transition->getToState()->willReturn('published');
- $list[$transition->reveal()->id()] = $transition->reveal();
-
- return $list;
- }
-
- /**
- * Builds a mock storage object for States.
- *
- * @return EntityStorageInterface
- * The mocked storage object for States.
- */
- protected function setupStateStorage() {
- $entity_storage = $this->prophesize(EntityStorageInterface::class);
-
- $state = $this->prophesize(ModerationStateInterface::class);
- $state->id()->willReturn('draft');
- $state->label()->willReturn('Draft');
- $state->isPublishedState()->willReturn(FALSE);
- $state->isDefaultRevisionState()->willReturn(FALSE);
- $states['draft'] = $state->reveal();
-
- $state = $this->prophesize(ModerationStateInterface::class);
- $state->id()->willReturn('needs_review');
- $state->label()->willReturn('Needs Review');
- $state->isPublishedState()->willReturn(FALSE);
- $state->isDefaultRevisionState()->willReturn(FALSE);
- $states['needs_review'] = $state->reveal();
-
- $state = $this->prophesize(ModerationStateInterface::class);
- $state->id()->willReturn('staging');
- $state->label()->willReturn('Staging');
- $state->isPublishedState()->willReturn(FALSE);
- $state->isDefaultRevisionState()->willReturn(FALSE);
- $states['staging'] = $state->reveal();
-
- $state = $this->prophesize(ModerationStateInterface::class);
- $state->id()->willReturn('published');
- $state->label()->willReturn('Published');
- $state->isPublishedState()->willReturn(TRUE);
- $state->isDefaultRevisionState()->willReturn(TRUE);
- $states['published'] = $state->reveal();
-
- $state = $this->prophesize(ModerationStateInterface::class);
- $state->id()->willReturn('archived');
- $state->label()->willReturn('Archived');
- $state->isPublishedState()->willReturn(TRUE);
- $state->isDefaultRevisionState()->willReturn(TRUE);
- $states['archived'] = $state->reveal();
-
- $entity_storage->loadMultiple()->willReturn($states);
-
- foreach ($states as $id => $state) {
- $entity_storage->load($id)->willReturn($state);
- }
-
- return $entity_storage->reveal();
- }
-
- /**
- * Builds a mocked Entity Type Manager.
- *
- * @return EntityTypeManagerInterface
- * The mocked Entity Type Manager.
- */
- protected function setupEntityTypeManager(EntityStorageInterface $storage) {
- $entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class);
- $entityTypeManager->getStorage('moderation_state')->willReturn($storage);
- $entityTypeManager->getStorage('moderation_state_transition')->willReturn($this->setupTransitionStorage());
-
- return $entityTypeManager->reveal();
- }
-
- /**
- * Builds a mocked query factory that does nothing.
- *
- * @return QueryFactory
- * The mocked query factory that does nothing.
- */
- protected function setupQueryFactory() {
- $factory = $this->prophesize(QueryFactory::class);
-
- return $factory->reveal();
- }
-
- /**
- * @covers ::isTransitionAllowed
- * @covers ::calculatePossibleTransitions
- *
- * @dataProvider providerIsTransitionAllowedWithValidTransition
- */
- public function testIsTransitionAllowedWithValidTransition($from_id, $to_id) {
- $storage = $this->setupStateStorage();
- $state_transition_validation = new StateTransitionValidation($this->setupEntityTypeManager($storage), $this->setupQueryFactory());
- $this->assertTrue($state_transition_validation->isTransitionAllowed($storage->load($from_id), $storage->load($to_id)));
- }
-
- /**
- * Data provider for self::testIsTransitionAllowedWithValidTransition().
- */
- public function providerIsTransitionAllowedWithValidTransition() {
- return [
- ['draft', 'draft'],
- ['draft', 'needs_review'],
- ['needs_review', 'needs_review'],
- ['needs_review', 'staging'],
- ['staging', 'published'],
- ['needs_review', 'draft'],
- ];
- }
-
- /**
- * @covers ::isTransitionAllowed
- * @covers ::calculatePossibleTransitions
- *
- * @dataProvider providerIsTransitionAllowedWithInValidTransition
- */
- public function testIsTransitionAllowedWithInValidTransition($from_id, $to_id) {
- $storage = $this->setupStateStorage();
- $state_transition_validation = new StateTransitionValidation($this->setupEntityTypeManager($storage), $this->setupQueryFactory());
- $this->assertFalse($state_transition_validation->isTransitionAllowed($storage->load($from_id), $storage->load($to_id)));
- }
-
- /**
- * Data provider for self::testIsTransitionAllowedWithInValidTransition().
- */
- public function providerIsTransitionAllowedWithInValidTransition() {
- return [
- ['published', 'needs_review'],
- ['published', 'staging'],
- ['staging', 'needs_review'],
- ['staging', 'staging'],
- ['needs_review', 'published'],
- ['published', 'archived'],
- ['archived', 'published'],
- ];
- }
-
- /**
* Verifies user-aware transition validation.
*
* @param string $from_id
@@ -239,7 +30,7 @@ class StateTransitionValidationTest extends \PHPUnit_Framework_TestCase {
* @param bool $allowed
* Whether or not to grant a user this permission.
* @param bool $result
- * Whether userMayTransition() is expected to return TRUE or FALSE.
+ * Whether getValidTransitions() is expected to have the.
*
* @dataProvider userTransitionsProvider
*/
@@ -250,10 +41,45 @@ class StateTransitionValidationTest extends \PHPUnit_Framework_TestCase {
$user->hasPermission($permission)->willReturn($allowed);
$user->hasPermission(Argument::type('string'))->willReturn(FALSE);
- $storage = $this->setupStateStorage();
- $validator = new Validator($this->setupEntityTypeManager($storage), $this->setupQueryFactory());
+ $entity = $this->prophesize(ContentEntityInterface::class);
+ $entity = $entity->reveal();
+ $entity->moderation_state = new \stdClass();
+ $entity->moderation_state->value = $from_id;
+
+ $validator = new StateTransitionValidation($this->setUpModerationInformation($entity));
+ $has_transition = FALSE;
+ foreach ($validator->getValidTransitions($entity, $user->reveal()) as $transition) {
+ if ($transition->to()->id() === $to_id) {
+ $has_transition = TRUE;
+ break;
+ }
+ }
+ $this->assertSame($result, $has_transition);
+ }
- $this->assertEquals($result, $validator->userMayTransition($storage->load($from_id), $storage->load($to_id), $user->reveal()));
+ protected function setUpModerationInformation(ContentEntityInterface $entity) {
+ // Create a container so that the plugin manager and workflow type can be
+ // mocked.
+ $container = new ContainerBuilder();
+ $workflow_type = $this->prophesize(WorkflowTypeInterface::class);
+ $workflow_type->decorateState(Argument::any())->willReturnArgument(0);
+ $workflow_type->decorateTransition(Argument::any())->willReturnArgument(0);
+ $workflow_manager = $this->prophesize(WorkflowTypeManager::class);
+ $workflow_manager->createInstance('content_moderation', Argument::any())->willReturn($workflow_type->reveal());
+ $container->set('plugin.manager.workflows.type', $workflow_manager->reveal());
+ \Drupal::setContainer($container);
+
+ $workflow = new Workflow(['id' => 'process', 'type' => 'content_moderation'], 'workflow');
+ $workflow
+ ->addState('draft', 'draft')
+ ->addState('needs_review', 'needs_review')
+ ->addState('published', 'published')
+ ->addTransition('draft', 'draft', ['draft'], 'draft')
+ ->addTransition('review', 'review', ['draft'], 'needs_review')
+ ->addTransition('publish', 'publish', ['needs_review', 'published'], 'published');
+ $moderation_info = $this->prophesize(ModerationInformationInterface::class);
+ $moderation_info->getWorkFlowForEntity($entity)->willReturn($workflow);
+ return $moderation_info->reveal();
}
/**
@@ -261,37 +87,15 @@ class StateTransitionValidationTest extends \PHPUnit_Framework_TestCase {
*/
public function userTransitionsProvider() {
// The user has the right permission, so let it through.
- $ret[] = ['draft', 'draft', 'use draft__draft transition', TRUE, TRUE];
+ $ret[] = ['draft', 'draft', 'use process transition draft', TRUE, TRUE];
// The user doesn't have the right permission, block it.
- $ret[] = ['draft', 'draft', 'use draft__draft transition', FALSE, FALSE];
+ $ret[] = ['draft', 'draft', 'use process transition draft', FALSE, FALSE];
// The user has some other permission that doesn't matter.
- $ret[] = ['draft', 'draft', 'use draft__needs_review transition', TRUE, FALSE];
-
- // The user has permission, but the transition isn't allowed anyway.
- $ret[] = ['published', 'needs_review', 'use published__needs_review transition', TRUE, FALSE];
+ $ret[] = ['draft', 'draft', 'use process transition review', TRUE, FALSE];
return $ret;
}
}
-
-/**
- * Testable subclass for selected tests.
- *
- * EntityQuery is beyond untestable, so we have to subclass and override the
- * method that uses it.
- */
-class Validator extends StateTransitionValidation {
-
- /**
- * {@inheritdoc}
- */
- protected function getTransitionFromStates(ModerationStateInterface $from, ModerationStateInterface $to) {
- if ($from->id() === 'draft' && $to->id() === 'draft') {
- return $this->transitionStorage()->loadMultiple(['draft__draft'])[0];
- }
- }
-
-}
diff --git a/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml b/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml
index 464a007..d34d949 100644
--- a/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml
+++ b/core/modules/system/tests/modules/entity_test/config/schema/entity_test.schema.yml
@@ -27,19 +27,3 @@ entity_test.entity_test_bundle.*:
description:
type: text
label: 'Description'
-
-entity_test.entity_test_bundle.*.third_party.content_moderation:
- type: mapping
- label: 'Enable moderation states for this entity test type'
- mapping:
- enabled:
- type: boolean
- label: 'Moderation states enabled'
- allowed_moderation_states:
- type: sequence
- sequence:
- type: string
- label: 'Moderation state'
- default_moderation_state:
- type: string
- label: 'Moderation state for new entity test'
diff --git a/core/modules/workflows/config/schema/workflows.schema.yml b/core/modules/workflows/config/schema/workflows.schema.yml
new file mode 100644
index 0000000..e19eb6b
--- /dev/null
+++ b/core/modules/workflows/config/schema/workflows.schema.yml
@@ -0,0 +1,51 @@
+workflows.workflow.*:
+ type: config_entity
+ label: 'Workflow'
+ mapping:
+ id:
+ type: string
+ label: 'ID'
+ label:
+ type: label
+ label: 'Label'
+ type:
+ type: string
+ label: 'Workflow type'
+ type_settings:
+ type: workflow.type_settings.[%parent.type]
+ label: 'Custom settings for workflow type'
+ states:
+ type: sequence
+ label: 'States'
+ sequence:
+ type: mapping
+ label: 'State'
+ mapping:
+ label:
+ type: label
+ label: 'Label'
+ weight:
+ type: integer
+ label: 'Weight'
+ transitions:
+ type: sequence
+ label: 'Transitions'
+ sequence:
+ type: mapping
+ label: 'Transition from state to state'
+ mapping:
+ label:
+ type: label
+ label: 'Transition label'
+ from:
+ type: sequence
+ label: 'From state IDs'
+ sequence:
+ type: string
+ label: 'From state ID'
+ to:
+ type: string
+ label: 'To state ID'
+ weight:
+ type: integer
+ label: 'Weight'
diff --git a/core/modules/workflows/src/Annotation/WorkflowType.php b/core/modules/workflows/src/Annotation/WorkflowType.php
new file mode 100644
index 0000000..2aa3ff9
--- /dev/null
+++ b/core/modules/workflows/src/Annotation/WorkflowType.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Drupal\workflows\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines an 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\WorkflowManager
+ * @see plugin_api
+ *
+ * @Annotation
+ *
+ * @internal
+ * The workflow system is currently experimental and should only be leveraged
+ * by experimental modules and development releases of contributed modules.
+ */
+class WorkflowType extends Plugin {
+
+ /**
+ * The plugin ID.
+ *
+ * @var string
+ */
+ public $id;
+
+ /**
+ * The label of the workflow.
+ *
+ * Describes how the plugin is used to apply a workflow to something.
+ *
+ * @var \Drupal\Core\Annotation\Translation
+ *
+ * @ingroup plugin_translatable
+ */
+ public $label = '';
+
+}
diff --git a/core/modules/workflows/src/Entity/Workflow.php b/core/modules/workflows/src/Entity/Workflow.php
new file mode 100644
index 0000000..3bf8d22
--- /dev/null
+++ b/core/modules/workflows/src/Entity/Workflow.php
@@ -0,0 +1,506 @@
+<?php
+
+namespace Drupal\workflows\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
+use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection;
+use Drupal\workflows\State;
+use Drupal\workflows\Transition;
+use Drupal\workflows\WorkflowInterface;
+
+/**
+ * Defines the workflow entity.
+ *
+ * @ConfigEntityType(
+ * id = "workflow",
+ * label = @Translation("Workflow"),
+ * handlers = {
+ * "access" = "Drupal\workflows\WorkflowAccessControlHandler",
+ * "route_provider" = {
+ * "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
+ * },
+ * },
+ * config_prefix = "workflow",
+ * entity_keys = {
+ * "id" = "id",
+ * "label" = "label",
+ * "uuid" = "uuid",
+ * },
+ * config_export = {
+ * "id",
+ * "label",
+ * "states",
+ * "transitions",
+ * "type",
+ * "type_settings"
+ * },
+ * )
+ *
+ * @internal
+ * The workflow system is currently experimental and should only be leveraged
+ * by experimental modules and development releases of contributed modules.
+ */
+class Workflow extends ConfigEntityBase implements WorkflowInterface, EntityWithPluginCollectionInterface {
+
+ /**
+ * The Workflow ID.
+ *
+ * @var string
+ */
+ protected $id;
+
+ /**
+ * The Moderation state label.
+ *
+ * @var string
+ */
+ protected $label;
+
+ /**
+ * The states of the workflow.
+ *
+ * The array key is the machine name for the state. The structure of each
+ * array item is:
+ * @code
+ * label: {translatable label}
+ * weight: {integer value}
+ * @endcode
+ *
+ * @var array
+ */
+ protected $states = [];
+
+ /**
+ * The permitted transitions of the workflow.
+ *
+ * The array key is the machine name for the transition. The machine name is
+ * generated from the machine names of the states. The structure of each array
+ * item is:
+ * @code
+ * from:
+ * - {state machine name}
+ * - {state machine name}
+ * to: {state machine name}
+ * label: {translatable label}
+ * @endcode
+ *
+ * @var array
+ */
+ protected $transitions = [];
+
+ /**
+ * The workflow type plugin ID.
+ *
+ * @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 addState($state_id, $label) {
+ if (isset($this->states[$state_id])) {
+ throw new \InvalidArgumentException("The state '$state_id' already exists in workflow '{$this->id()}'");
+ }
+ if (preg_match('/[^a-z0-9_]+/', $state_id)) {
+ throw new \InvalidArgumentException("The state ID '$state_id' must contain only lowercase letters, numbers, and underscores");
+ }
+ $this->states[$state_id] = [
+ 'label' => $label,
+ 'weight' => $this->getNextWeight($this->states),
+ ];
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasState($state_id) {
+ return isset($this->states[$state_id]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getStates($state_ids = NULL) {
+ if ($state_ids === NULL) {
+ $state_ids = array_keys($this->states);
+ }
+ /** @var \Drupal\workflows\StateInterface[] $states */
+ $states = array_combine($state_ids, array_map([$this, 'getState'], $state_ids));
+ if (count($states) > 1) {
+ // Sort states by weight and then label.
+ $weights = $labels = [];
+ foreach ($states as $id => $state) {
+ $weights[$id] = $state->weight();
+ $labels[$id] = $state->label();
+ }
+ array_multisort(
+ $weights, SORT_NUMERIC, SORT_ASC,
+ $labels, SORT_NATURAL, SORT_ASC
+ );
+ $states = array_replace($weights, $states);
+ }
+ return $states;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getState($state_id) {
+ if (!isset($this->states[$state_id])) {
+ throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow '{$this->id()}'");
+ }
+ $state = new State(
+ $this,
+ $state_id,
+ $this->states[$state_id]['label'],
+ $this->states[$state_id]['weight']
+ );
+ return $this->getTypePlugin()->decorateState($state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setStateLabel($state_id, $label) {
+ if (!isset($this->states[$state_id])) {
+ throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow '{$this->id()}'");
+ }
+ $this->states[$state_id]['label'] = $label;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setStateWeight($state_id, $weight) {
+ if (!isset($this->states[$state_id])) {
+ throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow '{$this->id()}'");
+ }
+ $this->states[$state_id]['weight'] = $weight;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function deleteState($state_id) {
+ if (!isset($this->states[$state_id])) {
+ throw new \InvalidArgumentException("The state '$state_id' does not exist in workflow '{$this->id()}'");
+ }
+ if (count($this->states) === 1) {
+ throw new \InvalidArgumentException("The state '$state_id' can not be deleted from workflow '{$this->id()}' as it is the only state");
+ }
+
+ foreach ($this->transitions as $transition_id => $transition) {
+ $from_key = array_search($state_id, $transition['from'], TRUE);
+ if ($from_key !== FALSE) {
+ // Remove state from the from array.
+ unset($transition['from'][$from_key]);
+ }
+ if (empty($transition['from']) || $transition['to'] === $state_id) {
+ $this->deleteTransition($transition_id);
+ }
+ elseif ($from_key !== FALSE) {
+ $this->setTransitionFromStates($transition_id, $transition['from']);
+ }
+ }
+ unset($this->states[$state_id]);
+ $this->getTypePlugin()->deleteState($state_id);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getInitialState() {
+ $ordered_states = $this->getStates();
+ return reset($ordered_states);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addTransition($transition_id, $label, array $from_state_ids, $to_state_id) {
+ if (isset($this->transitions[$transition_id])) {
+ throw new \InvalidArgumentException("The transition '$transition_id' already exists in workflow '{$this->id()}'");
+ }
+ if (preg_match('/[^a-z0-9_]+/', $transition_id)) {
+ throw new \InvalidArgumentException("The transition ID '$transition_id' must contain only lowercase letters, numbers, and underscores");
+ }
+
+ if (!$this->hasState($to_state_id)) {
+ throw new \InvalidArgumentException("The state '$to_state_id' does not exist in workflow '{$this->id()}'");
+ }
+ $this->transitions[$transition_id] = [
+ 'label' => $label,
+ 'from' => [],
+ 'to' => $to_state_id,
+ // Always add to the end.
+ 'weight' => $this->getNextWeight($this->transitions),
+ ];
+
+ try {
+ $this->setTransitionFromStates($transition_id, $from_state_ids);
+ }
+ catch (\InvalidArgumentException $e) {
+ unset($this->transitions[$transition_id]);
+ throw $e;
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTransitions(array $transition_ids = NULL) {
+ if ($transition_ids === NULL) {
+ $transition_ids = array_keys($this->transitions);
+ }
+ /** @var \Drupal\workflows\TransitionInterface[] $transitions */
+ $transitions = array_combine($transition_ids, array_map([$this, 'getTransition'], $transition_ids));
+ if (count($transitions) > 1) {
+ // Sort transitions by weights and then labels.
+ $weights = $labels = [];
+ foreach ($transitions as $id => $transition) {
+ $weights[$id] = $transition->weight();
+ $labels[$id] = $transition->label();
+ }
+ array_multisort(
+ $weights, SORT_NUMERIC, SORT_ASC,
+ $labels, SORT_NATURAL, SORT_ASC
+ );
+ $transitions = array_replace($weights, $transitions);
+ }
+ return $transitions;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTransition($transition_id) {
+ if (!isset($this->transitions[$transition_id])) {
+ throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow '{$this->id()}'");
+ }
+ $transition = new Transition(
+ $this,
+ $transition_id,
+ $this->transitions[$transition_id]['label'],
+ $this->transitions[$transition_id]['from'],
+ $this->transitions[$transition_id]['to'],
+ $this->transitions[$transition_id]['weight']
+ );
+ return $this->getTypePlugin()->decorateTransition($transition);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasTransition($transition_id) {
+ return isset($this->transitions[$transition_id]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTransitionsForState($state_id, $direction = 'from') {
+ $transition_ids = array_keys(array_filter($this->transitions, function ($transition) use ($state_id, $direction) {
+ return in_array($state_id, (array) $transition[$direction], TRUE);
+ }));
+ return $this->getTransitions($transition_ids);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTransitionFromStateToState($from_state_id, $to_state_id) {
+ $transition_id = $this->getTransitionIdFromStateToState($from_state_id, $to_state_id);
+ if (empty($transition_id)) {
+ throw new \InvalidArgumentException("The transition from '$from_state_id' to '$to_state_id' does not exist in workflow '{$this->id()}'");
+ }
+ return $this->getTransition($transition_id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasTransitionFromStateToState($from_state_id, $to_state_id) {
+ return !empty($this->getTransitionIdFromStateToState($from_state_id, $to_state_id));
+ }
+
+ /**
+ * Gets the transition ID from state to state.
+ *
+ * @param string $from_state_id
+ * The state ID to transition from.
+ * @param string $to_state_id
+ * The state ID to transition to.
+ *
+ * @return string|null
+ * The transition ID, or NULL if no transition exists.
+ */
+ protected function getTransitionIdFromStateToState($from_state_id, $to_state_id) {
+ foreach ($this->transitions as $transition_id => $transition) {
+ if (in_array($from_state_id, $transition['from'], TRUE) && $transition['to'] === $to_state_id) {
+ return $transition_id;
+ }
+ }
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setTransitionLabel($transition_id, $label) {
+ if (isset($this->transitions[$transition_id])) {
+ $this->transitions[$transition_id]['label'] = $label;
+ }
+ else {
+ throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow '{$this->id()}'");
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setTransitionWeight($transition_id, $weight) {
+ if (isset($this->transitions[$transition_id])) {
+ $this->transitions[$transition_id]['weight'] = $weight;
+ }
+ else {
+ throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow '{$this->id()}'");
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setTransitionFromStates($transition_id, array $from_state_ids) {
+ if (!isset($this->transitions[$transition_id])) {
+ throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow '{$this->id()}'");
+ }
+
+ // Ensure that the states exist.
+ foreach ($from_state_ids as $from_state_id) {
+ if (!$this->hasState($from_state_id)) {
+ throw new \InvalidArgumentException("The state '$from_state_id' does not exist in workflow '{$this->id()}'");
+ }
+ if ($this->hasTransitionFromStateToState($from_state_id, $this->transitions[$transition_id]['to'])) {
+ $transition = $this->getTransitionFromStateToState($from_state_id, $this->transitions[$transition_id]['to']);
+ if ($transition_id !== $transition->id()) {
+ throw new \InvalidArgumentException("The '{$transition->id()}' transition already allows '$from_state_id' to '{$this->transitions[$transition_id]['to']}' transitions in workflow '{$this->id()}'");
+ }
+ }
+ }
+
+ // Preserve the order of the state IDs in the from value and don't save any
+ // keys.
+ $from_state_ids = array_values($from_state_ids);
+ sort($from_state_ids);
+ $this->transitions[$transition_id]['from'] = $from_state_ids;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function deleteTransition($transition_id) {
+ if (isset($this->transitions[$transition_id])) {
+ unset($this->transitions[$transition_id]);
+ $this->getTypePlugin()->deleteTransition($transition_id);
+ }
+ else {
+ throw new \InvalidArgumentException("The transition '$transition_id' does not exist in workflow '{$this->id()}'");
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getTypePlugin() {
+ return $this->getPluginCollection()->get($this->type);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getPluginCollections() {
+ return ['type_settings' => $this->getPluginCollection()];
+ }
+
+ /**
+ * Encapsulates the creation of the workflow's plugin collection.
+ *
+ * @return \Drupal\Core\Plugin\DefaultSingleLazyPluginCollection
+ * The workflow's plugin collection.
+ */
+ protected function getPluginCollection() {
+ if (!$this->pluginCollection && $this->type) {
+ $this->pluginCollection = new DefaultSingleLazyPluginCollection(\Drupal::service('plugin.manager.workflows.type'), $this->type, $this->type_settings);
+ }
+ return $this->pluginCollection;
+ }
+
+ /**
+ * Loads all workflows of the provided type.
+ *
+ * @param string $type
+ * The workflow type to load all workflows for.
+ *
+ * @return static[]
+ * An array of workflow objects of the provided workflow type, indexed by
+ * their IDs.
+ *
+ * @see \Drupal\workflows\Annotation\WorkflowType
+ */
+ public static function loadMultipleByType($type) {
+ return self::loadMultiple(\Drupal::entityQuery('workflow')->condition('type', $type)->execute());
+ }
+
+ /**
+ * Gets the weight for a new state or transition.
+ *
+ * @param array $items
+ * An array of states or transitions information where each item has a
+ * 'weight' key with a numeric value.
+ *
+ * @return int
+ * The weight for a new item in the array so that it has the highest weight.
+ */
+ protected function getNextWeight(array $items) {
+ return array_reduce($items, function ($carry, $item) {
+ return max($carry, $item['weight'] + 1);
+ }, 0);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function status() {
+ // In order for a workflow to be usable it must have at least one state.
+ return !empty($this->status) && !empty($this->states);
+ }
+
+}
diff --git a/core/modules/workflows/src/Form/WorkflowAddForm.php b/core/modules/workflows/src/Form/WorkflowAddForm.php
new file mode 100644
index 0000000..c779b8f
--- /dev/null
+++ b/core/modules/workflows/src/Form/WorkflowAddForm.php
@@ -0,0 +1,107 @@
+<?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.
+ */
+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(),
+ '#description' => $this->t('Label for the Workflow.'),
+ '#required' => TRUE,
+ ];
+
+ $form['id'] = [
+ '#type' => 'machine_name',
+ '#default_value' => $workflow->id(),
+ '#machine_name' => [
+ 'exists' => [Workflow::class, 'load'],
+ ],
+ ];
+
+ $workflow_types = array_map(function ($plugin_definition) {
+ return $plugin_definition['label'];
+ }, $this->workflowTypePluginManager->getDefinitions());
+ $form['workflow_type'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Workflow type'),
+ '#required' => TRUE,
+ '#options' => $workflow_types,
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(array $form, FormStateInterface $form_state) {
+ /* @var \Drupal\workflows\WorkflowInterface $workflow */
+ $workflow = $this->entity;
+ $workflow->save();
+ drupal_set_message($this->t('Created the %label Workflow. In order for the workflow to be enabled there needs to be at least one state.', [
+ '%label' => $workflow->label(),
+ ]));
+ $form_state->setRedirectUrl($workflow->toUrl('add-state-form'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
+ // This form can only set the workflow's ID, label and the weights for each
+ // state.
+ /** @var \Drupal\workflows\WorkflowInterface $entity */
+ $values = $form_state->getValues();
+ $entity->set('label', $values['label']);
+ $entity->set('id', $values['id']);
+ $entity->set('type', $values['workflow_type']);
+ }
+
+}
diff --git a/core/modules/content_moderation/src/Form/ModerationStateDeleteForm.php b/core/modules/workflows/src/Form/WorkflowDeleteForm.php
index 43e2b36..b9b8331 100644
--- a/core/modules/content_moderation/src/Form/ModerationStateDeleteForm.php
+++ b/core/modules/workflows/src/Form/WorkflowDeleteForm.php
@@ -1,28 +1,28 @@
<?php
-namespace Drupal\content_moderation\Form;
+namespace Drupal\workflows\Form;
use Drupal\Core\Entity\EntityConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
/**
- * Builds the form to delete Moderation state entities.
+ * Builds the form to delete Workflow entities.
*/
-class ModerationStateDeleteForm extends EntityConfirmFormBase {
+class WorkflowDeleteForm extends EntityConfirmFormBase {
/**
* {@inheritdoc}
*/
public function getQuestion() {
- return $this->t('Are you sure you want to delete %name?', array('%name' => $this->entity->label()));
+ return $this->t('Are you sure you want to delete %name?', ['%name' => $this->entity->label()]);
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
- return new Url('entity.moderation_state.collection');
+ return new Url('entity.workflow.collection');
}
/**
@@ -39,7 +39,7 @@ class ModerationStateDeleteForm extends EntityConfirmFormBase {
$this->entity->delete();
drupal_set_message($this->t(
- 'Moderation state %label deleted.',
+ 'Workflow %label deleted.',
['%label' => $this->entity->label()]
));
diff --git a/core/modules/workflows/src/Form/WorkflowEditForm.php b/core/modules/workflows/src/Form/WorkflowEditForm.php
new file mode 100644
index 0000000..d8f99f9
--- /dev/null
+++ b/core/modules/workflows/src/Form/WorkflowEditForm.php
@@ -0,0 +1,214 @@
+<?php
+
+namespace Drupal\workflows\Form;
+
+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;
+
+/**
+ * The form for editing workflows.
+ */
+class WorkflowEditForm extends EntityForm {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function form(array $form, FormStateInterface $form_state) {
+ $form = parent::form($form, $form_state);
+
+ /* @var \Drupal\workflows\WorkflowInterface $workflow */
+ $workflow = $this->entity;
+ $form['label'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Label'),
+ '#maxlength' => 255,
+ '#default_value' => $workflow->label(),
+ '#description' => $this->t('Label for the Workflow.'),
+ '#required' => TRUE,
+ ];
+
+ $form['id'] = [
+ '#type' => 'machine_name',
+ '#default_value' => $workflow->id(),
+ '#machine_name' => [
+ 'exists' => [Workflow::class, 'load'],
+ ],
+ '#disabled' => TRUE,
+ ];
+
+ $header = [
+ 'state' => $this->t('State'),
+ 'weight' => $this->t('Weight'),
+ 'operations' => $this->t('Operations')
+ ];
+ $form['states_container'] = [
+ '#type' => 'details',
+ '#title' => $this->t('States'),
+ '#open' => TRUE,
+ '#collapsible' => 'FALSE',
+ ];
+ $form['states_container']['states'] = [
+ '#type' => 'table',
+ '#header' => $header,
+ '#title' => $this->t('States'),
+ '#empty' => $this->t('There are no states yet.'),
+ '#tabledrag' => [
+ [
+ 'action' => 'order',
+ 'relationship' => 'sibling',
+ 'group' => 'state-weight',
+ ],
+ ],
+ ];
+
+ $states = $workflow->getStates();
+
+ // Warn the user if there are no states.
+ if (empty($states)) {
+ drupal_set_message(
+ $this->t(
+ 'This workflow has no states and will be disabled until there is at least one, <a href=":add-state">add a new state.</a>',
+ [':add-state' => $workflow->toUrl('add-state-form')->toString()]
+ ),
+ 'warning'
+ );
+ }
+
+ $delete_state_access = $this->entity->access('delete-state');
+ foreach ($states as $state) {
+ $links['edit'] = [
+ 'title' => $this->t('Edit'),
+ 'url' => Url::fromRoute('entity.workflow.edit_state_form', ['workflow' => $workflow->id(), 'workflow_state' => $state->id()]),
+ 'attributes' => ['aria-label' => $this->t('Edit @state state', ['@state' => $state->label()])],
+ ];
+ if ($delete_state_access) {
+ $links['delete'] = [
+ 'title' => t('Delete'),
+ 'url' => Url::fromRoute('entity.workflow.delete_state_form', [
+ 'workflow' => $workflow->id(),
+ 'workflow_state' => $state->id()
+ ]),
+ 'attributes' => ['aria-label' => $this->t('Delete @state state', ['@state' => $state->label()])],
+ ];
+ }
+ $form['states_container']['states'][$state->id()] = [
+ '#attributes' => ['class' => ['draggable']],
+ 'state' => ['#markup' => $state->label()],
+ '#weight' => $state->weight(),
+ 'weight' => [
+ '#type' => 'weight',
+ '#title' => t('Weight for @title', ['@title' => $state->label()]),
+ '#title_display' => 'invisible',
+ '#default_value' => $state->weight(),
+ '#attributes' => ['class' => ['state-weight']],
+ ],
+ 'operations' => [
+ '#type' => 'operations',
+ '#links' => $links,
+ ],
+ ];
+ }
+ $form['states_container']['state_add'] = [
+ '#markup' => $workflow->toLink($this->t('Add a new state'), 'add-state-form')->toString(),
+ ];
+
+ $header = [
+ 'label' => $this->t('Label'),
+ 'weight' => $this->t('Weight'),
+ 'from' => $this->t('From'),
+ 'to' => $this->t('To'),
+ 'operations' => $this->t('Operations')
+ ];
+ $form['transitions_container'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Transitions'),
+ '#open' => TRUE,
+ ];
+ $form['transitions_container']['transitions'] = [
+ '#type' => 'table',
+ '#header' => $header,
+ '#title' => $this->t('Transitions'),
+ '#empty' => $this->t('There are no transitions yet.'),
+ '#tabledrag' => [
+ [
+ 'action' => 'order',
+ 'relationship' => 'sibling',
+ 'group' => 'transition-weight',
+ ],
+ ],
+ ];
+ foreach ($workflow->getTransitions() as $transition) {
+ $links['edit'] = [
+ 'title' => $this->t('Edit'),
+ 'url' => Url::fromRoute('entity.workflow.edit_transition_form', ['workflow' => $workflow->id(), 'workflow_transition' => $transition->id()]),
+ 'attributes' => ['aria-label' => $this->t('Edit \'@transition\' transition', ['@transition' => $transition->label()])],
+ ];
+ $links['delete'] = [
+ 'title' => t('Delete'),
+ 'url' => Url::fromRoute('entity.workflow.delete_transition_form', ['workflow' => $workflow->id(), 'workflow_transition' => $transition->id()]),
+ 'attributes' => ['aria-label' => $this->t('Delete \'@transition\' transition', ['@transition' => $transition->label()])],
+ ];
+ $form['transitions_container']['transitions'][$transition->id()] = [
+ '#attributes' => ['class' => ['draggable']],
+ 'label' => ['#markup' => $transition->label()],
+ '#weight' => $transition->weight(),
+ 'weight' => [
+ '#type' => 'weight',
+ '#title' => t('Weight for @title', ['@title' => $transition->label()]),
+ '#title_display' => 'invisible',
+ '#default_value' => $transition->weight(),
+ '#attributes' => ['class' => ['transition-weight']],
+ ],
+ 'from' => [
+ '#theme' => 'item_list',
+ '#items' => array_map([State::class, 'labelCallback'], $transition->from()),
+ '#context' => ['list_style' => 'comma-list'],
+ ],
+ 'to' => ['#markup' => $transition->to()->label()],
+ 'operations' => [
+ '#type' => 'operations',
+ '#links' => $links,
+ ],
+ ];
+ }
+ $form['transitions_container']['transition_add'] = [
+ '#markup' => $workflow->toLink($this->t('Add a new transition'), 'add-transition-form')->toString(),
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(array $form, FormStateInterface $form_state) {
+ /* @var \Drupal\workflows\WorkflowInterface $workflow */
+ $workflow = $this->entity;
+ $workflow->save();
+ drupal_set_message($this->t('Saved the %label Workflow.', ['%label' => $workflow->label()]));
+ $form_state->setRedirectUrl($workflow->toUrl('collection'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
+ // This form can only set the workflow's ID, label and the weights for each
+ // state.
+ /** @var \Drupal\workflows\WorkflowInterface $entity */
+ $values = $form_state->getValues();
+ $entity->set('label', $values['label']);
+ $entity->set('id', $values['id']);
+ foreach ($values['states'] as $state_id => $state_values) {
+ $entity->setStateWeight($state_id, $state_values['weight']);
+ }
+ foreach ($values['transitions'] as $transition_id => $transition_values) {
+ $entity->setTransitionWeight($transition_id, $transition_values['weight']);
+ }
+ }
+
+}
diff --git a/core/modules/workflows/src/Form/WorkflowStateAddForm.php b/core/modules/workflows/src/Form/WorkflowStateAddForm.php
new file mode 100644
index 0000000..c9b46e2
--- /dev/null
+++ b/core/modules/workflows/src/Form/WorkflowStateAddForm.php
@@ -0,0 +1,115 @@
+<?php
+
+namespace Drupal\workflows\Form;
+
+use Drupal\Core\Entity\EntityForm;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Class WorkflowStateAddForm.
+ */
+class WorkflowStateAddForm extends EntityForm {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function form(array $form, FormStateInterface $form_state) {
+ $form = parent::form($form, $form_state);
+
+ /* @var \Drupal\workflows\WorkflowInterface $workflow */
+ $workflow = $this->getEntity();
+ $form['label'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Label'),
+ '#maxlength' => 255,
+ '#default_value' => '',
+ '#description' => $this->t('Label for the state.'),
+ '#required' => TRUE,
+ ];
+
+ $form['id'] = [
+ '#type' => 'machine_name',
+ '#machine_name' => [
+ 'exists' => [$this, 'exists'],
+ ],
+ ];
+
+ // Add additional form fields from the workflow type plugin.
+ $form['type_settings'] = [
+ $workflow->get('type') => $workflow->getTypePlugin()->buildStateConfigurationForm($form_state, $workflow),
+ '#tree' => TRUE,
+ ];
+
+ return $form;
+ }
+
+ /**
+ * Determines if the workflow state already exists.
+ *
+ * @param string $state_id
+ * The workflow state ID.
+ *
+ * @return bool
+ * TRUE if the workflow state exists, FALSE otherwise.
+ */
+ public function exists($state_id) {
+ /** @var \Drupal\workflows\WorkflowInterface $original_workflow */
+ $original_workflow = \Drupal::entityTypeManager()->getStorage('workflow')->loadUnchanged($this->getEntity()->id());
+ return $original_workflow->hasState($state_id);
+ }
+
+ /**
+ * Copies top-level form values to entity properties
+ *
+ * This form can only change values for a state, which is part of workflow.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity the current form should operate upon.
+ * @param array $form
+ * A nested array of form elements comprising the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
+ /** @var \Drupal\workflows\WorkflowInterface $entity */
+ $values = $form_state->getValues();
+
+ // This is fired twice so we have to check that the entity does not already
+ // have the state.
+ if (!$entity->hasState($values['id'])) {
+ $entity->addState($values['id'], $values['label']);
+ if (isset($values['type_settings'])) {
+ $configuration = $entity->getTypePlugin()->getConfiguration();
+ $configuration['states'][$values['id']] = $values['type_settings'][$entity->getTypePlugin()->getPluginId()];
+ $entity->set('type_settings', $configuration);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(array $form, FormStateInterface $form_state) {
+ /** @var \Drupal\workflows\WorkflowInterface $workflow */
+ $workflow = $this->entity;
+ $workflow->save();
+ drupal_set_message($this->t('Created %label state.', [
+ '%label' => $workflow->getState($form_state->getValue('id'))->label(),
+ ]));
+ $form_state->setRedirectUrl($workflow->toUrl('edit-form'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function actions(array $form, FormStateInterface $form_state) {
+ $actions['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Save'),
+ '#submit' => ['::submitForm', '::save'],
+ ];
+ return $actions;
+ }
+
+}
diff --git a/core/modules/workflows/src/Form/WorkflowStateDeleteForm.php b/core/modules/workflows/src/Form/WorkflowStateDeleteForm.php
new file mode 100644
index 0000000..c60045c
--- /dev/null
+++ b/core/modules/workflows/src/Form/WorkflowStateDeleteForm.php
@@ -0,0 +1,99 @@
+<?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.
+ */
+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->getState($this->stateId)->label(), '%workflow' => $this->workflow->label()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ return $this->workflow->toUrl();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfirmText() {
+ return $this->t('Delete');
+ }
+
+ /**
+ * Form constructor.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ * @param \Drupal\workflows\WorkflowInterface $workflow
+ * The workflow entity being edited.
+ * @param string|null $workflow_state
+ * The workflow state being deleted.
+ *
+ * @return array
+ * The form structure.
+ */
+ public function buildForm(array $form, FormStateInterface $form_state, WorkflowInterface $workflow = NULL, $workflow_state = NULL) {
+ if (!$workflow->hasState($workflow_state)) {
+ throw new NotFoundHttpException();
+ }
+ $this->workflow = $workflow;
+ $this->stateId = $workflow_state;
+ return parent::buildForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+
+ $workflow_label = $this->workflow->getState($this->stateId)->label();
+ $this->workflow
+ ->deleteState($this->stateId)
+ ->save();
+
+ drupal_set_message($this->t(
+ 'State %label deleted.',
+ ['%label' => $workflow_label]
+ ));
+
+ $form_state->setRedirectUrl($this->getCancelUrl());
+ }
+
+}
diff --git a/core/modules/workflows/src/Form/WorkflowStateEditForm.php b/core/modules/workflows/src/Form/WorkflowStateEditForm.php
new file mode 100644
index 0000000..6ee33f3
--- /dev/null
+++ b/core/modules/workflows/src/Form/WorkflowStateEditForm.php
@@ -0,0 +1,167 @@
+<?php
+
+namespace Drupal\workflows\Form;
+
+use Drupal\Core\Entity\EntityForm;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+
+/**
+ * Class WorkflowStateEditForm.
+ */
+class WorkflowStateEditForm extends EntityForm {
+
+ /**
+ * The ID of the state that is being edited.
+ *
+ * @var string
+ */
+ protected $stateId;
+
+ /**
+ * {@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();
+ $state = $workflow->getState($this->stateId);
+ $form['label'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Label'),
+ '#maxlength' => 255,
+ '#default_value' => $state->label(),
+ '#description' => $this->t('Label for the state.'),
+ '#required' => TRUE,
+ ];
+
+ $form['id'] = [
+ '#type' => 'machine_name',
+ '#default_value' => $this->stateId,
+ '#machine_name' => [
+ 'exists' => [$this, 'exists'],
+ ],
+ '#disabled' => TRUE,
+ ];
+
+ // Add additional form fields from the workflow type plugin.
+ $form['type_settings'] = [
+ $workflow->get('type') => $workflow->getTypePlugin()->buildStateConfigurationForm($form_state, $workflow, $state),
+ '#tree' => TRUE,
+ ];
+
+ $header = [
+ 'label' => $this->t('Transition'),
+ 'state' => $this->t('To'),
+ 'operations' => $this->t('Operations'),
+ ];
+ $form['transitions'] = [
+ '#type' => 'table',
+ '#header' => $header,
+ '#empty' => $this->t('There are no states yet.'),
+ ];
+ foreach ($state->getTransitions() as $transition) {
+ $links['edit'] = [
+ 'title' => $this->t('Edit'),
+ 'url' => Url::fromRoute('entity.workflow.edit_transition_form', [
+ 'workflow' => $workflow->id(),
+ 'workflow_transition' => $transition->id()
+ ]),
+ ];
+ $links['delete'] = [
+ 'title' => t('Delete'),
+ 'url' => Url::fromRoute('entity.workflow.delete_transition_form', [
+ 'workflow' => $workflow->id(),
+ 'workflow_transition' => $transition->id()
+ ]),
+ ];
+ $form['transitions'][$transition->id()] = [
+ 'label' => [
+ '#markup' => $transition->label(),
+ ],
+ 'state' => [
+ '#markup' => $transition->to()->label(),
+ ],
+ 'operations' => [
+ '#type' => 'operations',
+ '#links' => $links,
+ ],
+ ];
+ }
+
+ return $form;
+ }
+
+ /**
+ * Copies top-level form values to entity properties
+ *
+ * This form can only change values for a state, which is part of workflow.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity the current form should operate upon.
+ * @param array $form
+ * A nested array of form elements comprising the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
+ /** @var \Drupal\workflows\WorkflowInterface $entity */
+ $values = $form_state->getValues();
+ $entity->setStateLabel($values['id'], $values['label']);
+ if (isset($values['type_settings'])) {
+ $configuration = $entity->getTypePlugin()->getConfiguration();
+ $configuration['states'][$values['id']] = $values['type_settings'][$entity->getTypePlugin()->getPluginId()];
+ $entity->set('type_settings', $configuration);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(array $form, FormStateInterface $form_state) {
+ /** @var \Drupal\workflows\WorkflowInterface $workflow */
+ $workflow = $this->entity;
+ $workflow->save();
+ drupal_set_message($this->t('Saved %label state.', [
+ '%label' => $workflow->getState($this->stateId)->label(),
+ ]));
+ $form_state->setRedirectUrl($workflow->toUrl('edit-form'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function actions(array $form, FormStateInterface $form_state) {
+ $actions['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Save'),
+ '#submit' => ['::submitForm', '::save'],
+ ];
+
+ $actions['delete'] = [
+ '#type' => 'link',
+ '#title' => $this->t('Delete'),
+ '#access' => $this->entity->access('delete-state'),
+ '#attributes' => [
+ 'class' => ['button', 'button--danger'],
+ ],
+ '#url' => Url::fromRoute('entity.workflow.delete_state_form', [
+ 'workflow' => $this->entity->id(),
+ 'workflow_state' => $this->stateId
+ ])
+ ];
+
+ return $actions;
+ }
+
+}
diff --git a/core/modules/workflows/src/Form/WorkflowTransitionAddForm.php b/core/modules/workflows/src/Form/WorkflowTransitionAddForm.php
new file mode 100644
index 0000000..1c557d9
--- /dev/null
+++ b/core/modules/workflows/src/Form/WorkflowTransitionAddForm.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace Drupal\workflows\Form;
+
+use Drupal\Core\Entity\EntityForm;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\workflows\State;
+
+/**
+ * Class WorkflowTransitionAddForm.
+ */
+class WorkflowTransitionAddForm extends EntityForm {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function form(array $form, FormStateInterface $form_state) {
+ $form = parent::form($form, $form_state);
+
+ /* @var \Drupal\workflows\WorkflowInterface $workflow */
+ $workflow = $this->getEntity();
+ $form['label'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Label'),
+ '#maxlength' => 255,
+ '#default_value' => '',
+ '#description' => $this->t('Label for the transition.'),
+ '#required' => TRUE,
+ ];
+
+ $form['id'] = [
+ '#type' => 'machine_name',
+ '#machine_name' => [
+ 'exists' => [$this, 'exists'],
+ ],
+ ];
+
+ // @todo https://www.drupal.org/node/2830584 Add some ajax to ensure that
+ // only valid transitions are selectable.
+ $states = array_map([State::class, 'labelCallback'], $workflow->getStates());
+ $form['from'] = [
+ '#type' => 'checkboxes',
+ '#title' => $this->t('From'),
+ '#required' => TRUE,
+ '#default_value' => [],
+ '#options' => $states,
+ ];
+ $form['to'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('To'),
+ '#required' => TRUE,
+ '#default_value' => [],
+ '#options' => $states,
+ ];
+
+ // Add additional form fields from the workflow type plugin.
+ $form['type_settings'] = [
+ $workflow->get('type') => $workflow->getTypePlugin()->buildTransitionConfigurationForm($form_state, $workflow),
+ '#tree' => TRUE,
+ ];
+
+ return $form;
+ }
+
+ /**
+ * Determines if the workflow transition already exists.
+ *
+ * @param string $transition_id
+ * The workflow transition ID.
+ *
+ * @return bool
+ * TRUE if the workflow transition exists, FALSE otherwise.
+ */
+ public function exists($transition_id) {
+ /** @var \Drupal\workflows\WorkflowInterface $original_workflow */
+ $original_workflow = \Drupal::entityTypeManager()->getStorage('workflow')->loadUnchanged($this->getEntity()->id());
+ return $original_workflow->hasTransition($transition_id);
+ }
+
+ /**
+ * Copies top-level form values to entity properties
+ *
+ * This form can only change values for a state, which is part of workflow.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity the current form should operate upon.
+ * @param array $form
+ * A nested array of form elements comprising the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
+ if (!$form_state->isValidationComplete()) {
+ // Only do something once form validation is complete.
+ return;
+ }
+ /** @var \Drupal\workflows\WorkflowInterface $entity */
+ $values = $form_state->getValues();
+ $entity->addTransition($values['id'], $values['label'], array_filter($values['from']), $values['to']);
+ if (isset($values['type_settings'])) {
+ $configuration = $entity->getTypePlugin()->getConfiguration();
+ $configuration['transitions'][$values['id']] = $values['type_settings'][$entity->getTypePlugin()->getPluginId()];
+ $entity->set('type_settings', $configuration);
+ }
+ }
+
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ /** @var \Drupal\workflows\WorkflowInterface $workflow */
+ $workflow = $this->getEntity();
+ $values = $form_state->getValues();
+ foreach (array_filter($values['from']) as $from_state_id) {
+ if ($workflow->hasTransitionFromStateToState($from_state_id, $values['to'])) {
+ $form_state->setErrorByName('from][' . $from_state_id, $this->t('The transition from %from to %to already exists.', [
+ '%from' => $workflow->getState($from_state_id)->label(),
+ '%to' => $workflow->getState($values['to'])->label(),
+ ]));
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(array $form, FormStateInterface $form_state) {
+ /** @var \Drupal\workflows\WorkflowInterface $workflow */
+ $workflow = $this->entity;
+ $workflow->save();
+ drupal_set_message($this->t('Created %label transition.', [
+ '%label' => $form_state->getValue('label'),
+ ]));
+ $form_state->setRedirectUrl($workflow->toUrl('edit-form'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function actions(array $form, FormStateInterface $form_state) {
+ $actions['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Save'),
+ '#submit' => ['::submitForm', '::save'],
+ ];
+ return $actions;
+ }
+
+}
diff --git a/core/modules/workflows/src/Form/WorkflowTransitionDeleteForm.php b/core/modules/workflows/src/Form/WorkflowTransitionDeleteForm.php
new file mode 100644
index 0000000..abcb41e
--- /dev/null
+++ b/core/modules/workflows/src/Form/WorkflowTransitionDeleteForm.php
@@ -0,0 +1,102 @@
+<?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.
+ */
+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->getTransition($workflow_transition);
+ }
+ catch (\InvalidArgumentException $e) {
+ throw new NotFoundHttpException();
+ }
+ $this->workflow = $workflow;
+ return parent::buildForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $this->workflow
+ ->deleteTransition($this->transition->id())
+ ->save();
+
+ drupal_set_message($this->t('%transition transition deleted.', ['%transition' => $this->transition->label()]));
+ $form_state->setRedirectUrl($this->getCancelUrl());
+ }
+
+}
diff --git a/core/modules/workflows/src/Form/WorkflowTransitionEditForm.php b/core/modules/workflows/src/Form/WorkflowTransitionEditForm.php
new file mode 100644
index 0000000..b6f65a4
--- /dev/null
+++ b/core/modules/workflows/src/Form/WorkflowTransitionEditForm.php
@@ -0,0 +1,171 @@
+<?php
+
+namespace Drupal\workflows\Form;
+
+use Drupal\Core\Entity\EntityForm;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\workflows\State;
+
+/**
+ * Class WorkflowTransitionEditForm.
+ */
+class WorkflowTransitionEditForm extends EntityForm {
+
+ /**
+ * The ID of the transition that is being edited.
+ *
+ * @var string
+ */
+ protected $transitionId;
+
+ /**
+ * {@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();
+ $transition = $workflow->getTransition($this->transitionId);
+ $form['label'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Label'),
+ '#maxlength' => 255,
+ '#default_value' => $transition->label(),
+ '#description' => $this->t('Label for the transition.'),
+ '#required' => TRUE,
+ ];
+
+ $form['id'] = [
+ '#type' => 'value',
+ '#value' => $this->transitionId,
+ ];
+
+ // @todo https://www.drupal.org/node/2830584 Add some ajax to ensure that
+ // only valid transitions are selectable.
+ $states = array_map([State::class, 'labelCallback'], $workflow->getStates());
+ $form['from'] = [
+ '#type' => 'checkboxes',
+ '#title' => $this->t('From'),
+ '#required' => TRUE,
+ '#default_value' => array_keys($transition->from()),
+ '#options' => $states,
+ ];
+ $form['to'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('To'),
+ '#required' => TRUE,
+ '#default_value' => $transition->to()->id(),
+ '#options' => $states,
+ '#disabled' => TRUE,
+ ];
+
+ // Add additional form fields from the workflow type plugin.
+ $form['type_settings'] = [
+ $workflow->get('type') => $workflow->getTypePlugin()->buildTransitionConfigurationForm($form_state, $workflow, $transition),
+ '#tree' => TRUE,
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ /** @var \Drupal\workflows\WorkflowInterface $workflow */
+ $workflow = $this->getEntity();
+ $values = $form_state->getValues();
+ foreach (array_filter($values['from']) as $from_state_id) {
+ if ($workflow->hasTransitionFromStateToState($from_state_id, $values['to'])) {
+ $transition = $workflow->getTransitionFromStateToState($from_state_id, $values['to']);
+ if ($transition->id() !== $values['id']) {
+ $form_state->setErrorByName('from][' . $from_state_id, $this->t('The transition from %from to %to already exists.', [
+ '%from' => $workflow->getState($from_state_id)->label(),
+ '%to' => $workflow->getState($values['to'])->label(),
+ ]));
+ }
+ }
+ }
+ }
+
+ /**
+ * Copies top-level form values to entity properties
+ *
+ * This form can only change values for a state, which is part of workflow.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity the current form should operate upon.
+ * @param array $form
+ * A nested array of form elements comprising the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
+ if (!$form_state->isValidationComplete()) {
+ // Only do something once form validation is complete.
+ return;
+ }
+ /** @var \Drupal\workflows\WorkflowInterface $entity */
+ $values = $form_state->getValues();
+ $form_state->set('created_transition', FALSE);
+ $entity->setTransitionLabel($values['id'], $values['label']);
+ $entity->setTransitionFromStates($values['id'], array_filter($values['from']));
+ if (isset($values['type_settings'])) {
+ $configuration = $entity->getTypePlugin()->getConfiguration();
+ $configuration['transitions'][$values['id']] = $values['type_settings'][$entity->getTypePlugin()->getPluginId()];
+ $entity->set('type_settings', $configuration);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(array $form, FormStateInterface $form_state) {
+ /** @var \Drupal\workflows\WorkflowInterface $workflow */
+ $workflow = $this->entity;
+ $workflow->save();
+ drupal_set_message($this->t('Saved %label transition.', [
+ '%label' => $workflow->getTransition($this->transitionId)->label(),
+ ]));
+ $form_state->setRedirectUrl($workflow->toUrl('edit-form'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function actions(array $form, FormStateInterface $form_state) {
+ $actions['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Save'),
+ '#submit' => ['::submitForm', '::save'],
+ ];
+
+ $actions['delete'] = [
+ '#type' => 'link',
+ '#title' => $this->t('Delete'),
+ // Deleting a transition is editing a workflow.
+ '#access' => $this->entity->access('edit'),
+ '#attributes' => [
+ 'class' => ['button', 'button--danger'],
+ ],
+ '#url' => Url::fromRoute('entity.workflow.delete_transition_form', [
+ 'workflow' => $this->entity->id(),
+ 'workflow_transition' => $this->transitionId
+ ])
+ ];
+
+ return $actions;
+ }
+
+}
diff --git a/core/modules/workflows/src/Plugin/WorkflowTypeBase.php b/core/modules/workflows/src/Plugin/WorkflowTypeBase.php
new file mode 100644
index 0000000..1b910e9
--- /dev/null
+++ b/core/modules/workflows/src/Plugin/WorkflowTypeBase.php
@@ -0,0 +1,119 @@
+<?php
+
+namespace Drupal\workflows\Plugin;
+
+use Drupal\Component\Plugin\PluginBase;
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\workflows\StateInterface;
+use Drupal\workflows\TransitionInterface;
+use Drupal\workflows\WorkflowInterface;
+use Drupal\workflows\WorkflowTypeInterface;
+
+/**
+ * A base class for Workflow type plugins.
+ *
+ * @see \Drupal\workflows\Annotation\WorkflowType
+ *
+ * @internal
+ * The workflow system is currently experimental and should only be leveraged
+ * by experimental modules and development releases of contributed modules.
+ */
+abstract class WorkflowTypeBase extends PluginBase implements WorkflowTypeInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function label() {
+ $definition = $this->getPluginDefinition();
+ // The label can be an object.
+ // @see \Drupal\Core\StringTranslation\TranslatableMarkup
+ return $definition['label'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function checkWorkflowAccess(WorkflowInterface $entity, $operation, AccountInterface $account) {
+ return AccessResult::neutral();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function decorateState(StateInterface $state) {
+ return $state;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function deleteState($state_id) {
+ unset($this->configuration['states'][$state_id]);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function decorateTransition(TransitionInterface $transition) {
+ return $transition;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function deleteTransition($transition_id) {
+ unset($this->configuration['transitions'][$transition_id]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildStateConfigurationForm(FormStateInterface $form_state, WorkflowInterface $workflow, StateInterface $state = NULL) {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildTransitionConfigurationForm(FormStateInterface $form_state, WorkflowInterface $workflow, TransitionInterface $transition = NULL) {
+ return [];
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getConfiguration() {
+ return $this->configuration;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setConfiguration(array $configuration) {
+ $this->configuration = NestedArray::mergeDeep(
+ $this->defaultConfiguration(),
+ $configuration
+ );
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function defaultConfiguration() {
+ return [
+ 'states' => [],
+ 'transitions' => [],
+ ];
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function calculateDependencies() {
+ return [];
+ }
+
+}
diff --git a/core/modules/workflows/src/State.php b/core/modules/workflows/src/State.php
new file mode 100644
index 0000000..f8b4fae
--- /dev/null
+++ b/core/modules/workflows/src/State.php
@@ -0,0 +1,118 @@
+<?php
+
+namespace Drupal\workflows;
+
+/**
+ * A value object representing a workflow state.
+ *
+ * @internal
+ * The workflow system is currently experimental and should only be leveraged
+ * by experimental modules and development releases of contributed modules.
+ */
+class State implements StateInterface {
+
+ /**
+ * The workflow the state is attached to.
+ *
+ * @var \Drupal\workflows\WorkflowInterface
+ */
+ 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\WorkflowInterface $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(WorkflowInterface $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 list of states to labels
+ *
+ * @param \Drupal\workflows\StateInterface $state
+ *
+ * @return string
+ * The label of the state.
+ */
+ public static function labelCallback(StateInterface $state) {
+ return $state->label();
+ }
+
+}
diff --git a/core/modules/workflows/src/StateInterface.php b/core/modules/workflows/src/StateInterface.php
new file mode 100644
index 0000000..6aa16e9
--- /dev/null
+++ b/core/modules/workflows/src/StateInterface.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace Drupal\workflows;
+
+/**
+ * An interface for state value objects.
+ *
+ * @see \Drupal\workflows\WorkflowTypeInterface::decorateState()
+ *
+ * @internal
+ * The workflow system is currently experimental and should only be leveraged
+ * by experimental modules and development releases of contributed modules.
+ */
+interface StateInterface {
+
+ /**
+ * 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();
+
+}
diff --git a/core/modules/workflows/src/Transition.php b/core/modules/workflows/src/Transition.php
new file mode 100644
index 0000000..d8c21b6
--- /dev/null
+++ b/core/modules/workflows/src/Transition.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace Drupal\workflows;
+
+/**
+ * A transition value object that describes the transition between states.
+ *
+ * @internal
+ * The workflow system is currently experimental and should only be leveraged
+ * by experimental modules and development releases of contributed modules.
+ */
+class Transition implements TransitionInterface {
+
+ /**
+ * The workflow that this transition is attached to.
+ *
+ * @var \Drupal\workflows\WorkflowInterface
+ */
+ 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\WorkflowInterface $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(WorkflowInterface $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;
+ }
+
+}
diff --git a/core/modules/workflows/src/TransitionInterface.php b/core/modules/workflows/src/TransitionInterface.php
new file mode 100644
index 0000000..c178b22
--- /dev/null
+++ b/core/modules/workflows/src/TransitionInterface.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\workflows;
+
+/**
+ * A transition value object that describes the transition between two states.
+ *
+ * @internal
+ * The workflow system is currently experimental and should only be leveraged
+ * by experimental modules and development releases of contributed modules.
+ */
+interface TransitionInterface {
+
+ /**
+ * 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();
+
+}
diff --git a/core/modules/workflows/src/WorkflowAccessControlHandler.php b/core/modules/workflows/src/WorkflowAccessControlHandler.php
new file mode 100644
index 0000000..dbeedf5
--- /dev/null
+++ b/core/modules/workflows/src/WorkflowAccessControlHandler.php
@@ -0,0 +1,83 @@
+<?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 Moderation State entity.
+ *
+ * @see \Drupal\workflows\Entity\Workflow.
+ *
+ * @internal
+ * The workflow system is currently experimental and should only be leveraged
+ * by experimental modules and development releases of contributed modules.
+ */
+class WorkflowAccessControlHandler extends EntityAccessControlHandler implements EntityHandlerInterface {
+
+ /**
+ * 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) {
+ if ($operation === 'delete-state') {
+ // Deleting a state is editing a workflow, but also we should forbid
+ // access if there is only one state.
+ /** @var \Drupal\workflows\Entity\Workflow $entity */
+ $admin_access = AccessResult::allowedIf(count($entity->getStates()) > 1)->andIf(parent::checkAccess($entity, 'edit', $account))->addCacheableDependency($entity);
+ }
+ else {
+ $admin_access = parent::checkAccess($entity, $operation, $account);
+ }
+ /** @var \Drupal\workflows\WorkflowInterface $entity */
+ return $entity->getTypePlugin()->checkWorkflowAccess($entity, $operation, $account)->orIf($admin_access);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
+ $workflow_types_count = count($this->workflowTypeManager->getDefinitions());
+ $admin_access = parent::checkCreateAccess($account, $context, $entity_bundle);
+ // Allow access if there is at least one workflow type. Since workflow types
+ // are provided by modules this is cacheable until extensions change.
+ return $admin_access->andIf(AccessResult::allowedIf($workflow_types_count > 0))->addCacheTags(['config:core.extension']);
+ }
+
+}
diff --git a/core/modules/workflows/src/WorkflowInterface.php b/core/modules/workflows/src/WorkflowInterface.php
new file mode 100644
index 0000000..0efedea
--- /dev/null
+++ b/core/modules/workflows/src/WorkflowInterface.php
@@ -0,0 +1,289 @@
+<?php
+
+namespace Drupal\workflows;
+
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+
+/**
+ * Provides an interface for defining workflow entities.
+ *
+ * @internal
+ * The workflow system is currently experimental and should only be leveraged
+ * by experimental modules and development releases of contributed modules.
+ */
+interface WorkflowInterface extends ConfigEntityInterface {
+
+ /**
+ * Adds a state to the workflow.
+ *
+ * @param string $state_id
+ * The state's ID.
+ * @param string $label
+ * The state's label.
+ *
+ * @return \Drupal\workflows\WorkflowInterface
+ * The workflow entity.
+ */
+ public function addState($state_id, $label);
+
+ /**
+ * Determines if the workflow has a state with the provided ID.
+ *
+ * @param string $state_id
+ * The state's ID.
+ *
+ * @return bool
+ * TRUE if the workflow has a state with the provided ID, FALSE if not.
+ */
+ public function hasState($state_id);
+
+ /**
+ * Gets state objects for the provided state IDs.
+ *
+ * @param string[] $state_ids
+ * A list of state IDs to get. If NULL then all states will be returned.
+ *
+ * @return \Drupal\workflows\StateInterface[]
+ * An array of workflow states.
+ *
+ * @throws \InvalidArgumentException
+ * Thrown if $state_ids contains a state ID that does not exist.
+ */
+ public function getStates($state_ids = NULL);
+
+ /**
+ * Gets a workflow state.
+ *
+ * @param string $state_id
+ * The state's ID.
+ *
+ * @return \Drupal\workflows\StateInterface
+ * The workflow state.
+ *
+ * @throws \InvalidArgumentException
+ * Thrown if $state_id does not exist.
+ */
+ public function getState($state_id);
+
+ /**
+ * Sets a state's label.
+ *
+ * @param string $state_id
+ * The state ID to set the label for.
+ * @param string $label
+ * The state's label.
+ *
+ * @return \Drupal\workflows\WorkflowInterface
+ * The workflow entity.
+ */
+ public function setStateLabel($state_id, $label);
+
+ /**
+ * Sets a state's weight value.
+ *
+ * @param string $state_id
+ * The state ID to set the weight for.
+ * @param int $weight
+ * The state's weight.
+ *
+ * @return \Drupal\workflows\WorkflowInterface
+ * The workflow entity.
+ */
+ public function setStateWeight($state_id, $weight);
+
+ /**
+ * Deletes a state from the workflow.
+ *
+ * @param string $state_id
+ * The state ID to delete.
+ *
+ * @return \Drupal\workflows\WorkflowInterface
+ * The workflow entity.
+ *
+ * @throws \InvalidArgumentException
+ * Thrown if $state_id does not exist.
+ */
+ public function deleteState($state_id);
+
+ /**
+ * Gets the initial state for the workflow.
+ *
+ * @return \Drupal\workflows\StateInterface
+ * The initial state.
+ */
+ public function getInitialState();
+
+ /**
+ * Adds a transition to the workflow.
+ *
+ * @param string $id
+ * The transition ID.
+ * @param string $label.
+ * The transition's label.
+ * @param array $from_state_ids
+ * The state IDs to transition from.
+ * @param string $to_state_id
+ * The state ID to transition to.
+ *
+ * @return \Drupal\workflows\WorkflowInterface
+ * The workflow entity.
+ *
+ * @throws \InvalidArgumentException
+ * Thrown if either state does not exist.
+ */
+ public function addTransition($id, $label, array $from_state_ids, $to_state_id);
+
+ /**
+ * Gets a transition object for the provided transition ID.
+ *
+ * @param string $transition_id
+ * A transition ID.
+ *
+ * @return \Drupal\workflows\TransitionInterface
+ * The transition.
+ *
+ * @throws \InvalidArgumentException
+ * Thrown if $transition_id does not exist.
+ */
+ public function getTransition($transition_id);
+
+ /**
+ * Determines if a transition exists.
+ *
+ * @param string $transition_id
+ * The transition ID.
+ *
+ * @return bool
+ * TRUE if the transition exists, FALSE if not.
+ */
+ public function hasTransition($transition_id);
+
+ /**
+ * Gets transition objects for the provided transition IDs.
+ *
+ * @param string[] $transition_ids
+ * A list of transition IDs to get. If NULL then all transitions will be
+ * returned.
+ *
+ * @return \Drupal\workflows\TransitionInterface[]
+ * An array of transition objects.
+ *
+ * @throws \InvalidArgumentException
+ * Thrown if $transition_ids contains a transition ID that does not exist.
+ */
+ public function getTransitions(array $transition_ids = NULL);
+
+ /**
+ * Gets the transactions IDs for a state for the provided direction.
+ *
+ * @param $state_id
+ * The state to get transitions for.
+ * @param string $direction
+ * (optional) The direction of the transition. Defaults to 'from'. Possible
+ * values are: 'from' and 'to'.
+ *
+ * @return array
+ * The transactions IDs for a state for the provided direction.
+ */
+ public function getTransitionsForState($state_id, $direction = 'from');
+
+ /**
+ * Gets a transition from state to state.
+ *
+ * @param string $from_state_id
+ * The state ID to transition from.
+ * @param string $to_state_id
+ * The state ID to transition to.
+ *
+ * @return \Drupal\workflows\TransitionInterface
+ * The transitions.
+ *
+ * @throws \InvalidArgumentException
+ * Thrown if the transition does not exist.
+ */
+ public function getTransitionFromStateToState($from_state_id, $to_state_id);
+
+ /**
+ * Determines if a transition from state to state exists.
+ *
+ * @param string $from_state_id
+ * The state ID to transition from.
+ * @param string $to_state_id
+ * The state ID to transition to.
+ *
+ * @return bool
+ * TRUE if the transition exists, FALSE if not.
+ */
+ public function hasTransitionFromStateToState($from_state_id, $to_state_id);
+
+ /**
+ * Sets a transition's label.
+ *
+ * @param string $transition_id
+ * The transition ID.
+ * @param string $label
+ * The transition's label.
+ *
+ * @return \Drupal\workflows\WorkflowInterface
+ * The workflow entity.
+ *
+ * @throws \InvalidArgumentException
+ * Thrown if the transition does not exist.
+ */
+ public function setTransitionLabel($transition_id, $label);
+
+ /**
+ * Sets a transition's weight.
+ *
+ * @param string $transition_id
+ * The transition ID.
+ * @param int $weight
+ * The transition's weight.
+ *
+ * @return \Drupal\workflows\WorkflowInterface
+ * The workflow entity.
+ *
+ * @throws \InvalidArgumentException
+ * Thrown if the transition does not exist.
+ */
+ public function setTransitionWeight($transition_id, $weight);
+
+ /**
+ * Sets a transition's from states.
+ *
+ * @param string $transition_id
+ * The transition ID.
+ * @param array $from_state_ids
+ * The state IDs to transition from.
+ *
+ * @return \Drupal\workflows\WorkflowInterface
+ * The workflow entity.
+ *
+ * @throws \InvalidArgumentException
+ * Thrown if the transition does not exist or the states do not exist.
+ */
+ public function setTransitionFromStates($transition_id, array $from_state_ids);
+
+ /**
+ * Deletes a transition.
+ *
+ * @param string $transition_id
+ * The transition ID.
+ *
+ * @return \Drupal\workflows\WorkflowInterface
+ * The workflow entity.
+ *
+ * @throws \InvalidArgumentException
+ * Thrown if the transition does not exist.
+ */
+ public function deleteTransition($transition_id);
+
+ /**
+ * Gets the workflow type plugin.
+ *
+ * @return \Drupal\workflows\WorkflowTypeInterface
+ * The workflow type plugin.
+ */
+ public function getTypePlugin();
+
+}
diff --git a/core/modules/workflows/src/WorkflowListBuilder.php b/core/modules/workflows/src/WorkflowListBuilder.php
new file mode 100644
index 0000000..3e94103
--- /dev/null
+++ b/core/modules/workflows/src/WorkflowListBuilder.php
@@ -0,0 +1,101 @@
+<?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 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->getStates());
+ $row['states']['data'] = [
+ '#theme' => 'item_list',
+ '#context' => ['list_style' => 'comma-list'],
+ '#items' => $items,
+ ];
+
+ return $row + parent::buildRow($entity);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render() {
+ $build = parent::render();
+ $workflow_types_count = count($this->workflowTypeManager->getDefinitions());
+ if ($workflow_types_count === 0) {
+ $build['table']['#empty'] = $this->t('There are no workflow types available. In order to create workflows you need to install a module that provides a workflow type. For example, the Content Moderation module provides a workflow type that enables workflows for content entities.');
+ }
+ return $build;
+ }
+
+}
diff --git a/core/modules/workflows/src/WorkflowTypeInterface.php b/core/modules/workflows/src/WorkflowTypeInterface.php
new file mode 100644
index 0000000..17fddec
--- /dev/null
+++ b/core/modules/workflows/src/WorkflowTypeInterface.php
@@ -0,0 +1,120 @@
+<?php
+
+namespace Drupal\workflows;
+
+use Drupal\Component\Plugin\ConfigurablePluginInterface;
+use Drupal\Component\Plugin\DerivativeInspectionInterface;
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * An interface for Workflow type plugins.
+ *
+ * @internal
+ * The workflow system is currently experimental and should only be leveraged
+ * by experimental modules and development releases of contributed modules.
+ */
+interface WorkflowTypeInterface extends PluginInspectionInterface, DerivativeInspectionInterface, ConfigurablePluginInterface {
+
+ /**
+ * Gets the label for the workflow type.
+ *
+ * @return string
+ * The workflow type label.
+ */
+ public function label();
+
+ /**
+ * Performs access checks.
+ *
+ * @param \Drupal\workflows\WorkflowInterface $entity
+ * The workflow entity for which to check access.
+ * @param string $operation
+ * The entity operation. Usually one of 'view', 'view label', 'update' or
+ * 'delete'.
+ * @param \Drupal\Core\Session\AccountInterface $account
+ * The user for which to check access.
+ *
+ * @return \Drupal\Core\Access\AccessResultInterface
+ * The access result.
+ */
+ public function checkWorkflowAccess(WorkflowInterface $entity, $operation, AccountInterface $account);
+
+ /**
+ * Decorates states so the WorkflowType can add additional information.
+ *
+ * @param \Drupal\workflows\StateInterface $state
+ * The state object to decorate.
+ *
+ * @return \Drupal\workflows\StateInterface $state
+ * The decorated state object.
+ */
+ public function decorateState(StateInterface $state);
+
+ /**
+ * React to the removal of a state from a workflow.
+ *
+ * @param string $state_id
+ * The state ID of the state that is being removed.
+ */
+ public function deleteState($state_id);
+
+ /**
+ * Decorates transitions so the WorkflowType can add additional information.
+ * @param \Drupal\workflows\TransitionInterface $transition
+ * The transition object to decorate.
+ *
+ * @return \Drupal\workflows\TransitionInterface $transition
+ * The decorated transition object.
+ */
+ public function decorateTransition(TransitionInterface $transition);
+
+ /**
+ * React to the removal of a transition from a workflow.
+ *
+ * @param string $transition_id
+ * The transition ID of the transition that is being removed.
+ */
+ public function deleteTransition($transition_id);
+
+ /**
+ * Builds a form to be added to the Workflow state edit form.
+ *
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ * @param \Drupal\workflows\WorkflowInterface $workflow
+ * The workflow the state is attached to.
+ * @param \Drupal\workflows\StateInterface|null $state
+ * The workflow state being edited. If NULL, a new state is being added.
+ *
+ * @return array
+ * Form elements to add to a workflow state form for customisations to the
+ * workflow.
+ *
+ * @see \Drupal\workflows\Form\WorkflowStateAddForm::form()
+ * @see \Drupal\workflows\Form\WorkflowStateEditForm::form()
+ */
+ public function buildStateConfigurationForm(FormStateInterface $form_state, WorkflowInterface $workflow, StateInterface $state = NULL);
+
+ /**
+ * Builds a form to be added to the Workflow transition edit form.
+ *
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ * @param \Drupal\workflows\WorkflowInterface $workflow
+ * The workflow the state is attached to.
+ * @param \Drupal\workflows\TransitionInterface|null $transition
+ * The workflow transition being edited. If NULL, a new transition is being
+ * added.
+ *
+ * @return array
+ * Form elements to add to a workflow transition form for customisations to
+ * the workflow.
+ *
+ * @see \Drupal\workflows\Form\WorkflowTransitionAddForm::form()
+ * @see \Drupal\workflows\Form\WorkflowTransitionEditForm::form()
+ */
+ public function buildTransitionConfigurationForm(FormStateInterface $form_state, WorkflowInterface $workflow, TransitionInterface $transition = NULL);
+
+}
diff --git a/core/modules/workflows/src/WorkflowTypeManager.php b/core/modules/workflows/src/WorkflowTypeManager.php
new file mode 100644
index 0000000..b2eafa8
--- /dev/null
+++ b/core/modules/workflows/src/WorkflowTypeManager.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\workflows;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\workflows\Annotation\WorkflowType;
+
+/**
+ * Provides a Workflow type plugin manager.
+ *
+ * @see \Drupal\workflows\Annotation\WorkflowType
+ * @see \Drupal\workflows\WorkflowTypeInterface
+ * @see plugin_api
+ *
+ * @internal
+ * The workflow system is currently experimental and should only be leveraged
+ * by experimental modules and development releases of contributed modules.
+ */
+class WorkflowTypeManager extends DefaultPluginManager {
+
+ /**
+ * 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);
+ $this->alterInfo('workflow_type_info');
+ $this->setCacheBackend($cache_backend, 'workflow_type_info');
+ }
+
+}
diff --git a/core/modules/workflows/tests/modules/workflow_type_test/config/schema/workflow_type_test.schema.yml b/core/modules/workflows/tests/modules/workflow_type_test/config/schema/workflow_type_test.schema.yml
new file mode 100644
index 0000000..4f12fdd
--- /dev/null
+++ b/core/modules/workflows/tests/modules/workflow_type_test/config/schema/workflow_type_test.schema.yml
@@ -0,0 +1,33 @@
+workflow.type_settings.workflow_type_test:
+ type: mapping
+ label: 'Workflow test type settings'
+ mapping:
+ states:
+ type: sequence
+ sequence:
+ type: ignore
+
+workflow.type_settings.workflow_type_complex_test:
+ type: mapping
+ label: 'Workflow complex test type settings'
+ mapping:
+ states:
+ type: sequence
+ label: 'Additional state configuration'
+ sequence:
+ type: mapping
+ label: 'States'
+ mapping:
+ extra:
+ type: string
+ label: 'Extra information'
+ transitions:
+ type: sequence
+ label: 'Additional transition configuration'
+ sequence:
+ type: mapping
+ label: 'Transitions'
+ mapping:
+ extra:
+ type: string
+ label: 'Extra information'
diff --git a/core/modules/workflows/tests/modules/workflow_type_test/src/DecoratedState.php b/core/modules/workflows/tests/modules/workflow_type_test/src/DecoratedState.php
new file mode 100644
index 0000000..793e899
--- /dev/null
+++ b/core/modules/workflows/tests/modules/workflow_type_test/src/DecoratedState.php
@@ -0,0 +1,90 @@
+<?php
+
+namespace Drupal\workflow_type_test;
+
+use Drupal\workflows\StateInterface;
+
+/**
+ * A value object representing a workflow state.
+ */
+class DecoratedState implements StateInterface {
+
+ /**
+ * The vanilla state object from the Workflow module.
+ *
+ * @var \Drupal\workflows\StateInterface
+ */
+ protected $state;
+
+ /**
+ * Extra information added to state.
+ *
+ * @var string
+ */
+ protected $extra;
+
+ /**
+ * DecoratedState constructor.
+ *
+ * @param \Drupal\workflows\StateInterface $state
+ * The vanilla state object from the Workflow module.
+ * @param string $extra
+ * (optional) Extra information stored on the state. Defaults to ''.
+ */
+ public function __construct(StateInterface $state, $extra = '') {
+ $this->state = $state;
+ $this->extra = $extra;
+ }
+
+ /**
+ * Gets the extra information stored on the state.
+ *
+ * @return string
+ */
+ public function getExtra() {
+ return $this->extra;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function id() {
+ return $this->state->id();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function label() {
+ return $this->state->label();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function weight() {
+ return $this->state->weight();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function canTransitionTo($to_state_id) {
+ return $this->state->canTransitionTo($to_state_id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTransitionTo($to_state_id) {
+ return $this->state->getTransitionTo($to_state_id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTransitions() {
+ return $this->state->getTransitions();
+ }
+
+}
diff --git a/core/modules/workflows/tests/modules/workflow_type_test/src/DecoratedTransition.php b/core/modules/workflows/tests/modules/workflow_type_test/src/DecoratedTransition.php
new file mode 100644
index 0000000..d7690d5
--- /dev/null
+++ b/core/modules/workflows/tests/modules/workflow_type_test/src/DecoratedTransition.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace Drupal\workflow_type_test;
+
+use Drupal\workflows\TransitionInterface;
+
+/**
+ * A value object representing a workflow transition.
+ */
+class DecoratedTransition implements TransitionInterface {
+
+ /**
+ * The vanilla transition object from the Workflow module.
+ *
+ * @var \Drupal\workflows\TransitionInterface
+ */
+ protected $transition;
+
+ /**
+ * Extra information added to transition.
+ *
+ * @var string
+ */
+ protected $extra;
+
+ /**
+ * DecoratedTransition constructor.
+ *
+ * @param \Drupal\workflows\TransitionInterface $transition
+ * The vanilla transition object from the Workflow module.
+ * @param string $extra
+ * (optional) Extra information stored on the transition. Defaults to ''.
+ */
+ public function __construct(TransitionInterface $transition, $extra = '') {
+ $this->transition = $transition;
+ $this->extra = $extra;
+ }
+
+ /**
+ * Gets the extra information stored on the transition.
+ *
+ * @return string
+ */
+ public function getExtra() {
+ return $this->extra;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function id() {
+ return $this->transition->id();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function label() {
+ return $this->transition->label();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function from() {
+ return $this->transition->from();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function to() {
+ return $this->transition->to();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function weight() {
+ return $this->transition->weight();
+ }
+
+}
diff --git a/core/modules/workflows/tests/modules/workflow_type_test/src/Plugin/WorkflowType/ComplexTestType.php b/core/modules/workflows/tests/modules/workflow_type_test/src/Plugin/WorkflowType/ComplexTestType.php
new file mode 100644
index 0000000..8440c1c
--- /dev/null
+++ b/core/modules/workflows/tests/modules/workflow_type_test/src/Plugin/WorkflowType/ComplexTestType.php
@@ -0,0 +1,82 @@
+<?php
+
+namespace Drupal\workflow_type_test\Plugin\WorkflowType;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\workflows\Plugin\WorkflowTypeBase;
+use Drupal\workflows\StateInterface;
+use Drupal\workflows\TransitionInterface;
+use Drupal\workflows\WorkflowInterface;
+use Drupal\workflow_type_test\DecoratedState;
+use Drupal\workflow_type_test\DecoratedTransition;
+
+/**
+ * Test workflow type.
+ *
+ * @WorkflowType(
+ * id = "workflow_type_complex_test",
+ * label = @Translation("Workflow Type Complex Test"),
+ * )
+ */
+class ComplexTestType extends WorkflowTypeBase {
+
+ use StringTranslationTrait;
+
+ /**
+ * {@inheritDoc}
+ */
+ public function decorateState(StateInterface $state) {
+ if (isset($this->configuration['states'][$state->id()])) {
+ $state = new DecoratedState($state, $this->configuration['states'][$state->id()]['extra']);
+ }
+ else {
+ $state = new DecoratedState($state);
+ }
+ return $state;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function decorateTransition(TransitionInterface $transition) {
+ if (isset($this->configuration['transitions'][$transition->id()])) {
+ $transition = new DecoratedTransition($transition, $this->configuration['transitions'][$transition->id()]['extra']);
+ }
+ else {
+ $transition = new DecoratedTransition($transition);
+ }
+ return $transition;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildStateConfigurationForm(FormStateInterface $form_state, WorkflowInterface $workflow, StateInterface $state = NULL) {
+ /** @var \Drupal\workflow_type_test\DecoratedState $state */
+ $form = [];
+ $form['extra'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Extra'),
+ '#description' => $this->t('Extra information added to state'),
+ '#default_value' => isset($state) ? $state->getExtra() : FALSE,
+ ];
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildTransitionConfigurationForm(FormStateInterface $form_state, WorkflowInterface $workflow, TransitionInterface $transition = NULL) {
+ /** @var \Drupal\workflow_type_test\DecoratedTransition $transition */
+ $form = [];
+ $form['extra'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Extra'),
+ '#description' => $this->t('Extra information added to transition'),
+ '#default_value' => isset($transition) ? $transition->getExtra() : FALSE,
+ ];
+ return $form;
+ }
+
+}
diff --git a/core/modules/workflows/tests/modules/workflow_type_test/src/Plugin/WorkflowType/TestType.php b/core/modules/workflows/tests/modules/workflow_type_test/src/Plugin/WorkflowType/TestType.php
new file mode 100644
index 0000000..78ebe03
--- /dev/null
+++ b/core/modules/workflows/tests/modules/workflow_type_test/src/Plugin/WorkflowType/TestType.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace Drupal\workflow_type_test\Plugin\WorkflowType;
+
+use Drupal\workflows\Plugin\WorkflowTypeBase;
+
+/**
+ * Test workflow type.
+ *
+ * @WorkflowType(
+ * id = "workflow_type_test",
+ * label = @Translation("Workflow Type Test"),
+ * )
+ */
+class TestType extends WorkflowTypeBase {
+}
diff --git a/core/modules/workflows/tests/modules/workflow_type_test/workflow_type_test.info.yml b/core/modules/workflows/tests/modules/workflow_type_test/workflow_type_test.info.yml
new file mode 100644
index 0000000..80a8c89
--- /dev/null
+++ b/core/modules/workflows/tests/modules/workflow_type_test/workflow_type_test.info.yml
@@ -0,0 +1,8 @@
+name: 'Workflow Type Test'
+type: module
+description: 'Provides a workflow type plugin for testing.'
+package: Testing
+version: VERSION
+core: 8.x
+dependencies:
+ - workflows
diff --git a/core/modules/workflows/tests/src/Functional/WorkflowUiNoTypeTest.php b/core/modules/workflows/tests/src/Functional/WorkflowUiNoTypeTest.php
new file mode 100644
index 0000000..c0c7de6
--- /dev/null
+++ b/core/modules/workflows/tests/src/Functional/WorkflowUiNoTypeTest.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\Tests\workflows\Functional;
+
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests workflow UI when there are no types.
+ *
+ * @group workflows
+ */
+class WorkflowUiNoTypeTest extends BrowserTestBase {
+
+ /**
+ * Modules to enable.
+ *
+ * @var array
+ */
+ public static $modules = ['workflows', 'block'];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+ // We're testing local actions.
+ $this->drupalPlaceBlock('local_actions_block');
+ }
+
+ /**
+ * Tests the creation of a workflow through the UI.
+ */
+ public function testWorkflowUiWithNoType() {
+ $this->drupalLogin($this->createUser(['access administration pages', 'administer workflows']));
+ $this->drupalGet('admin/config/workflow/workflows/add');
+ // There are no workflow types so this should be a 403.
+ $this->assertSession()->statusCodeEquals(403);
+
+ $this->drupalGet('admin/config/workflow/workflows');
+ $this->assertSession()->pageTextContains('There are no workflow types available. In order to create workflows you need to install a module that provides a workflow type. For example, the Content Moderation module provides a workflow type that enables workflows for content entities.');
+ $this->assertSession()->pageTextNotContains('Add workflow');
+
+ $this->container->get('module_installer')->install(['workflow_type_test']);
+ // The render cache needs to be cleared because although the cache tags are
+ // correctly set the render cache does not pick it up.
+ \Drupal::cache('render')->deleteAll();
+
+ $this->drupalGet('admin/config/workflow/workflows');
+ $this->assertSession()->pageTextNotContains('There are no workflow types available. In order to create workflows you need to install a module that provides a workflow type. For example, the Content Moderation module provides a workflow type that enables workflows for content entities.');
+ $this->assertSession()->linkExists('Add workflow');
+ $this->assertSession()->pageTextContains('There is no Workflow yet.');
+ }
+
+}
diff --git a/core/modules/workflows/tests/src/Functional/WorkflowUiTest.php b/core/modules/workflows/tests/src/Functional/WorkflowUiTest.php
new file mode 100644
index 0000000..ed5eb6f
--- /dev/null
+++ b/core/modules/workflows/tests/src/Functional/WorkflowUiTest.php
@@ -0,0 +1,262 @@
+<?php
+
+namespace Drupal\Tests\workflows\Functional;
+
+use Drupal\Core\Url;
+use Drupal\Tests\BrowserTestBase;
+use Drupal\workflows\Entity\Workflow;
+
+/**
+ * Tests workflow creation UI.
+ *
+ * @group workflows
+ */
+class WorkflowUiTest extends BrowserTestBase {
+
+ /**
+ * Modules to enable.
+ *
+ * @var array
+ */
+ public static $modules = ['workflows', 'workflow_type_test', 'block'];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+ // We're testing local actions.
+ $this->drupalPlaceBlock('local_actions_block');
+ }
+
+ /**
+ * Tests route access/permissions.
+ */
+ public function testAccess() {
+ // Create a minimal workflow for testing.
+ $workflow = Workflow::create(['id' => 'test', 'type' => 'workflow_type_test']);
+ $workflow
+ ->addState('draft', 'Draft')
+ ->addState('published', 'Published')
+ ->addTransition('publish', 'Publish', ['draft', 'published'], 'published')
+ ->save();
+
+ $paths = [
+ 'admin/config/workflow/workflows',
+ 'admin/config/workflow/workflows/add',
+ 'admin/config/workflow/workflows/manage/test',
+ 'admin/config/workflow/workflows/manage/test/delete',
+ 'admin/config/workflow/workflows/manage/test/add_state',
+ 'admin/config/workflow/workflows/manage/test/state/published',
+ 'admin/config/workflow/workflows/manage/test/state/published/delete',
+ 'admin/config/workflow/workflows/manage/test/add_transition',
+ 'admin/config/workflow/workflows/manage/test/transition/publish',
+ 'admin/config/workflow/workflows/manage/test/transition/publish/delete',
+ ];
+
+ foreach ($paths as $path) {
+ $this->drupalGet($path);
+ // No access.
+ $this->assertSession()->statusCodeEquals(403);
+ }
+ $this->drupalLogin($this->createUser(['administer workflows']));
+ foreach ($paths as $path) {
+ $this->drupalGet($path);
+ // User has access.
+ $this->assertSession()->statusCodeEquals(200);
+ }
+
+ // Delete one of the states and ensure the other test cannot be deleted.
+ $this->drupalGet('admin/config/workflow/workflows/manage/test/state/published/delete');
+ $this->submitForm([], 'Delete');
+ $this->drupalGet('admin/config/workflow/workflows/manage/test/state/published/delete');
+ $this->assertSession()->statusCodeEquals(403);
+ }
+
+ /**
+ * Tests the creation of a workflow through the UI.
+ */
+ public function testWorkflowCreation() {
+ $workflow_storage = $this->container->get('entity_type.manager')->getStorage('workflow');
+ /** @var \Drupal\workflows\WorkflowInterface $workflow */
+ $this->drupalLogin($this->createUser(['access administration pages', 'administer workflows']));
+ $this->drupalGet('admin/config/workflow');
+ $this->assertSession()->linkByHrefExists('admin/config/workflow/workflows');
+ $this->clickLink('Workflows');
+ $this->assertSession()->pageTextContains('There is no Workflow yet.');
+ $this->clickLink('Add workflow');
+ $this->submitForm(['label' => 'Test', 'id' => 'test', 'workflow_type' => 'workflow_type_test'], 'Save');
+ $this->assertSession()->pageTextContains('Created the Test Workflow.');
+ $this->assertSession()->addressEquals('admin/config/workflow/workflows/manage/test/add_state');
+ $this->drupalGet('/admin/config/workflow/workflows/manage/test');
+ $this->assertSession()->pageTextContains('This workflow has no states and will be disabled until there is at least one, add a new state.');
+ $this->assertSession()->pageTextContains('There are no states yet.');
+ $this->clickLink('Add a new state');
+ $this->submitForm(['label' => 'Published', 'id' => 'published'], 'Save');
+ $this->assertSession()->pageTextContains('Created Published state.');
+ $workflow = $workflow_storage->loadUnchanged('test');
+ $this->assertFalse($workflow->getState('published')->canTransitionTo('published'), 'No default transition from published to published exists.');
+
+ $this->clickLink('Add a new state');
+ // Don't create a draft to draft transition by default.
+ $this->submitForm(['label' => 'Draft', 'id' => 'draft'], 'Save');
+ $this->assertSession()->pageTextContains('Created Draft state.');
+ $workflow = $workflow_storage->loadUnchanged('test');
+ $this->assertFalse($workflow->getState('draft')->canTransitionTo('draft'), 'Can not transition from draft to draft');
+
+ $this->clickLink('Add a new transition');
+ $this->submitForm(['id' => 'publish', 'label' => 'Publish', 'from[draft]' => 'draft', 'to' => 'published'], 'Save');
+ $this->assertSession()->pageTextContains('Created Publish transition.');
+ $workflow = $workflow_storage->loadUnchanged('test');
+ $this->assertTrue($workflow->getState('draft')->canTransitionTo('published'), 'Can transition from draft to published');
+
+ $this->clickLink('Add a new transition');
+ $this->submitForm(['id' => 'create_new_draft', 'label' => 'Create new draft', 'from[draft]' => 'draft', 'to' => 'draft'], 'Save');
+ $this->assertSession()->pageTextContains('Created Create new draft transition.');
+ $workflow = $workflow_storage->loadUnchanged('test');
+ $this->assertTrue($workflow->getState('draft')->canTransitionTo('draft'), 'Can transition from draft to draft');
+
+ // The fist state to edit on the page should be published.
+ $this->clickLink('Edit');
+ $this->assertSession()->fieldValueEquals('label', 'Published');
+ // Change the label.
+ $this->submitForm(['label' => 'Live'], 'Save');
+ $this->assertSession()->pageTextContains('Saved Live state.');
+
+ // Allow published to draft.
+ $this->clickLink('Edit', 3);
+ $this->submitForm(['from[published]' => 'published'], 'Save');
+ $this->assertSession()->pageTextContains('Saved Create new draft transition.');
+ $workflow = $workflow_storage->loadUnchanged('test');
+ $this->assertTrue($workflow->getState('published')->canTransitionTo('draft'), 'Can transition from published to draft');
+
+ // Try creating a duplicate transition.
+ $this->clickLink('Add a new transition');
+ $this->submitForm(['id' => 'create_new_draft', 'label' => 'Create new draft', 'from[published]' => 'published', 'to' => 'draft'], 'Save');
+ $this->assertSession()->pageTextContains('The machine-readable name is already in use. It must be unique.');
+ // Try creating a transition which duplicates the states of another.
+ $this->submitForm(['id' => 'create_new_draft2', 'label' => 'Create new draft again', 'from[published]' => 'published', 'to' => 'draft'], 'Save');
+ $this->assertSession()->pageTextContains('The transition from Live to Draft already exists.');
+
+ // Create a new transition.
+ $this->submitForm(['id' => 'save_and_publish', 'label' => 'Save and publish', 'from[published]' => 'published', 'to' => 'published'], 'Save');
+ $this->assertSession()->pageTextContains('Created Save and publish transition.');
+ // Edit the new transition and try to add an existing transition.
+ $this->clickLink('Edit', 4);
+ $this->submitForm(['from[draft]' => 'draft'], 'Save');
+ $this->assertSession()->pageTextContains('The transition from Draft to Live already exists.');
+
+ // Delete the transition.
+ $workflow = $workflow_storage->loadUnchanged('test');
+ $this->assertTrue($workflow->hasTransitionFromStateToState('published', 'published'), 'Can transition from published to published');
+ $this->clickLink('Delete');
+ $this->assertSession()->pageTextContains('Are you sure you want to delete Save and publish from Test?');
+ $this->submitForm([], 'Delete');
+ $workflow = $workflow_storage->loadUnchanged('test');
+ $this->assertFalse($workflow->hasTransitionFromStateToState('published', 'published'), 'Cannot transition from published to published');
+
+ // Try creating a duplicate state.
+ $this->drupalGet('admin/config/workflow/workflows/manage/test');
+ $this->clickLink('Add a new state');
+ $this->submitForm(['label' => 'Draft', 'id' => 'draft'], 'Save');
+ $this->assertSession()->pageTextContains('The machine-readable name is already in use. It must be unique.');
+
+ // Ensure that weight changes the state ordering.
+ $workflow = $workflow_storage->loadUnchanged('test');
+ $this->assertEquals('published', $workflow->getInitialState()->id());
+ $this->drupalGet('admin/config/workflow/workflows/manage/test');
+ $this->submitForm(['states[draft][weight]' => '-1'], 'Save');
+ $workflow = $workflow_storage->loadUnchanged('test');
+ $this->assertEquals('draft', $workflow->getInitialState()->id());
+
+ // This will take us to the list of workflows, so we need to edit the
+ // workflow again.
+ $this->clickLink('Edit');
+
+ // Ensure that weight changes the transition ordering.
+ $this->assertEquals(['publish', 'create_new_draft'], array_keys($workflow->getTransitions()));
+ $this->drupalGet('admin/config/workflow/workflows/manage/test');
+ $this->submitForm(['transitions[create_new_draft][weight]' => '-1'], 'Save');
+ $workflow = $workflow_storage->loadUnchanged('test');
+ $this->assertEquals(['create_new_draft', 'publish'], array_keys($workflow->getTransitions()));
+
+ // This will take us to the list of workflows, so we need to edit the
+ // workflow again.
+ $this->clickLink('Edit');
+
+ // Ensure that a delete link for the published state exists before deleting
+ // the draft state.
+ $published_delete_link = Url::fromRoute('entity.workflow.delete_state_form', [
+ 'workflow' => $workflow->id(),
+ 'workflow_state' => 'published'
+ ])->toString();
+ $this->assertSession()->linkByHrefExists($published_delete_link);
+
+ // Delete the Draft state.
+ $this->clickLink('Delete');
+ $this->assertSession()->pageTextContains('Are you sure you want to delete Draft from Test?');
+ $this->submitForm([], 'Delete');
+ $this->assertSession()->pageTextContains('State Draft deleted.');
+ $workflow = $workflow_storage->loadUnchanged('test');
+ $this->assertFalse($workflow->hasState('draft'), 'Draft state deleted');
+ $this->assertTrue($workflow->hasState('published'), 'Workflow still has published state');
+
+ // The last state cannot be deleted so the only delete link on the page will
+ // be for the workflow.
+ $this->assertSession()->linkByHrefNotExists($published_delete_link);
+ $this->clickLink('Delete');
+ $this->assertSession()->pageTextContains('Are you sure you want to delete Test?');
+ $this->submitForm([], 'Delete');
+ $this->assertSession()->pageTextContains('Workflow Test deleted.');
+ $this->assertSession()->pageTextContains('There is no Workflow yet.');
+ $this->assertNull($workflow_storage->loadUnchanged('test'), 'The test workflow has been deleted');
+ }
+
+ /**
+ * Tests that workflow types can add form fields to states and transitions.
+ */
+ public function testWorkflowDecoration() {
+ // Create a minimal workflow for testing.
+ $workflow = Workflow::create(['id' => 'test', 'type' => 'workflow_type_complex_test']);
+ $workflow
+ ->addState('published', 'Published')
+ ->addTransition('publish', 'Publish', ['published'], 'published')
+ ->save();
+
+ $this->assertEquals('', $workflow->getState('published')->getExtra());
+ $this->assertEquals('', $workflow->getTransition('publish')->getExtra());
+
+ $this->drupalLogin($this->createUser(['administer workflows']));
+
+ // Add additional state information when editing.
+ $this->drupalGet('admin/config/workflow/workflows/manage/test/state/published');
+ $this->assertSession()->pageTextContains('Extra information added to state');
+ $this->submitForm(['type_settings[workflow_type_complex_test][extra]' => 'Extra state information'], 'Save');
+
+ // Add additional transition information when editing.
+ $this->drupalGet('admin/config/workflow/workflows/manage/test/transition/publish');
+ $this->assertSession()->pageTextContains('Extra information added to transition');
+ $this->submitForm(['type_settings[workflow_type_complex_test][extra]' => 'Extra transition information'], 'Save');
+
+ $workflow_storage = $this->container->get('entity_type.manager')->getStorage('workflow');
+ /** @var \Drupal\workflows\WorkflowInterface $workflow */
+ $workflow = $workflow_storage->loadUnchanged('test');
+ $this->assertEquals('Extra state information', $workflow->getState('published')->getExtra());
+ $this->assertEquals('Extra transition information', $workflow->getTransition('publish')->getExtra());
+
+ // Add additional state information when adding.
+ $this->drupalGet('admin/config/workflow/workflows/manage/test/add_state');
+ $this->assertSession()->pageTextContains('Extra information added to state');
+ $this->submitForm(['label' => 'Draft', 'id' => 'draft', 'type_settings[workflow_type_complex_test][extra]' => 'Extra state information on add'], 'Save');
+
+ // Add additional transition information when adding.
+ $this->drupalGet('admin/config/workflow/workflows/manage/test/add_transition');
+ $this->assertSession()->pageTextContains('Extra information added to transition');
+ $this->submitForm(['id' => 'draft_published', 'label' => 'Publish', 'from[draft]' => 'draft', 'to' => 'published', 'type_settings[workflow_type_complex_test][extra]' => 'Extra transition information on add'], 'Save');
+
+ $workflow = $workflow_storage->loadUnchanged('test');
+ $this->assertEquals('Extra state information on add', $workflow->getState('draft')->getExtra());
+ $this->assertEquals('Extra transition information on add', $workflow->getTransition('draft_published')->getExtra());
+ }
+
+}
diff --git a/core/modules/workflows/tests/src/Kernel/ComplexWorkflowTypeTest.php b/core/modules/workflows/tests/src/Kernel/ComplexWorkflowTypeTest.php
new file mode 100644
index 0000000..2ff1c2a
--- /dev/null
+++ b/core/modules/workflows/tests/src/Kernel/ComplexWorkflowTypeTest.php
@@ -0,0 +1,55 @@
+<?php
+
+namespace Drupal\Tests\workflows\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\workflows\Entity\Workflow;
+use Drupal\workflow_type_test\DecoratedState;
+use Drupal\workflow_type_test\DecoratedTransition;
+
+/**
+ * Workflow entity tests that require modules or storage.
+ *
+ * @coversDefaultClass \Drupal\workflows\Entity\Workflow
+ *
+ * @group workflows
+ */
+class ComplexWorkflowTypeTest extends KernelTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $modules = ['workflows', 'workflow_type_test'];
+
+ /**
+ * Tests a workflow type that decorates transitions and states.
+ *
+ * @covers ::getState
+ * @covers ::getTransition
+ */
+ public function testComplexType() {
+ $workflow = new Workflow(['id' => 'test', 'type' => 'workflow_type_complex_test'], 'workflow');
+ $workflow
+ ->addState('draft', 'Draft')
+ ->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft');
+ $this->assertInstanceOf(DecoratedState::class, $workflow->getState('draft'));
+ $this->assertInstanceOf(DecoratedTransition::class, $workflow->getTransition('create_new_draft'));
+ }
+
+ /**
+ * @covers ::loadMultipleByType
+ */
+ public function testLoadMultipleByType() {
+ $workflow1 = new Workflow(['id' => 'test1', 'type' => 'workflow_type_complex_test'], 'workflow');
+ $workflow1->save();
+ $workflow2 = new Workflow(['id' => 'test2', 'type' => 'workflow_type_complex_test'], 'workflow');
+ $workflow2->save();
+ $workflow3 = new Workflow(['id' => 'test3', 'type' => 'workflow_type_test'], 'workflow');
+ $workflow3->save();
+
+ $this->assertEquals(['test1', 'test2'], array_keys(Workflow::loadMultipleByType('workflow_type_complex_test')));
+ $this->assertEquals(['test3'], array_keys(Workflow::loadMultipleByType('workflow_type_test')));
+ $this->assertEquals([], Workflow::loadMultipleByType('a_type_that_does_not_exist'));
+ }
+
+}
diff --git a/core/modules/workflows/tests/src/Unit/StateTest.php b/core/modules/workflows/tests/src/Unit/StateTest.php
new file mode 100644
index 0000000..82feca3
--- /dev/null
+++ b/core/modules/workflows/tests/src/Unit/StateTest.php
@@ -0,0 +1,131 @@
+<?php
+
+namespace Drupal\Tests\workflows\Unit;
+
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Tests\UnitTestCase;
+use Drupal\workflows\Entity\Workflow;
+use Drupal\workflows\State;
+use Drupal\workflows\WorkflowInterface;
+use Drupal\workflows\WorkflowTypeInterface;
+use Drupal\workflows\WorkflowTypeManager;
+use Prophecy\Argument;
+
+/**
+ * @coversDefaultClass \Drupal\workflows\State
+ *
+ * @group workflows
+ */
+class StateTest extends UnitTestCase {
+
+ /**
+ * Sets up the Workflow Type manager so that workflow entities can be used.
+ */
+ protected function setUp() {
+ parent::setUp();
+ // Create a container so that the plugin manager and workflow type can be
+ // mocked.
+ $container = new ContainerBuilder();
+ $workflow_type = $this->prophesize(WorkflowTypeInterface::class);
+ $workflow_type->decorateState(Argument::any())->willReturnArgument(0);
+ $workflow_type->decorateTransition(Argument::any())->willReturnArgument(0);
+ $workflow_type->deleteState(Argument::any())->willReturn(NULL);
+ $workflow_type->deleteTransition(Argument::any())->willReturn(NULL);
+ $workflow_manager = $this->prophesize(WorkflowTypeManager::class);
+ $workflow_manager->createInstance('test_type', Argument::any())->willReturn($workflow_type->reveal());
+ $container->set('plugin.manager.workflows.type', $workflow_manager->reveal());
+ \Drupal::setContainer($container);
+ }
+
+ /**
+ * @covers ::__construct
+ * @covers ::id
+ * @covers ::label
+ * @covers ::weight
+ */
+ public function testGetters() {
+ $state = new State(
+ $this->prophesize(WorkflowInterface::class)->reveal(),
+ 'draft',
+ 'Draft',
+ 3
+ );
+ $this->assertEquals('draft', $state->id());
+ $this->assertEquals('Draft', $state->label());
+ $this->assertEquals(3, $state->weight());
+ }
+
+ /**
+ * @covers ::canTransitionTo
+ */
+ public function testCanTransitionTo() {
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow
+ ->addState('draft', 'Draft')
+ ->addState('published', 'Published')
+ ->addTransition('publish', 'Publish', ['draft'], 'published');
+ $state = $workflow->getState('draft');
+ $this->assertTrue($state->canTransitionTo('published'));
+ $this->assertFalse($state->canTransitionTo('some_other_state'));
+
+ $workflow->deleteTransition('publish');
+ $this->assertFalse($state->canTransitionTo('published'));
+ }
+
+ /**
+ * @covers ::getTransitionTo
+ */
+ public function testGetTransitionTo() {
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow
+ ->addState('draft', 'Draft')
+ ->addState('published', 'Published')
+ ->addTransition('publish', 'Publish', ['draft'], 'published');
+ $state = $workflow->getState('draft');
+ $transition = $state->getTransitionTo('published');
+ $this->assertEquals('Publish', $transition->label());
+ }
+
+ /**
+ * @covers ::getTransitionTo
+ */
+ public function testGetTransitionToException() {
+ $this->setExpectedException(\InvalidArgumentException::class, "Can not transition to 'published' state");
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow->addState('draft', 'Draft');
+ $state = $workflow->getState('draft');
+ $state->getTransitionTo('published');
+ }
+
+ /**
+ * @covers ::getTransitions
+ */
+ public function testGetTransitions() {
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow
+ ->addState('draft', 'Draft')
+ ->addState('published', 'Published')
+ ->addState('archived', 'Archived')
+ ->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft')
+ ->addTransition('publish', 'Publish', ['draft'], 'published')
+ ->addTransition('archive', 'Archive', ['published'], 'archived');
+ $state = $workflow->getState('draft');
+ $transitions = $state->getTransitions();
+ $this->assertCount(2, $transitions);
+ $this->assertEquals('Create new draft', $transitions['create_new_draft']->label());
+ $this->assertEquals('Publish', $transitions['publish']->label());
+ }
+
+ /**
+ * @covers ::labelCallback
+ */
+ public function testLabelCallback() {
+ $workflow = $this->prophesize(WorkflowInterface::class)->reveal();
+ $states = [
+ new State($workflow, 'draft', 'Draft'),
+ new State($workflow, 'published', 'Published'),
+ ];
+ $this->assertEquals(['Draft', 'Published'], array_map([State::class, 'labelCallback'], $states));
+ }
+
+}
diff --git a/core/modules/workflows/tests/src/Unit/TransitionTest.php b/core/modules/workflows/tests/src/Unit/TransitionTest.php
new file mode 100644
index 0000000..3202e8a
--- /dev/null
+++ b/core/modules/workflows/tests/src/Unit/TransitionTest.php
@@ -0,0 +1,71 @@
+<?php
+
+namespace Drupal\Tests\workflows\Unit;
+
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Tests\UnitTestCase;
+use Drupal\workflows\Entity\Workflow;
+use Drupal\workflows\Transition;
+use Drupal\workflows\WorkflowInterface;
+use Drupal\workflows\WorkflowTypeInterface;
+use Drupal\workflows\WorkflowTypeManager;
+use Prophecy\Argument;
+
+/**
+ * @coversDefaultClass \Drupal\workflows\Transition
+ *
+ * @group workflows
+ */
+class TransitionTest extends UnitTestCase {
+
+ /**
+ * Sets up the Workflow Type manager so that workflow entities can be used.
+ */
+ protected function setUp() {
+ parent::setUp();
+ // Create a container so that the plugin manager and workflow type can be
+ // mocked.
+ $container = new ContainerBuilder();
+ $workflow_type = $this->prophesize(WorkflowTypeInterface::class);
+ $workflow_type->decorateState(Argument::any())->willReturnArgument(0);
+ $workflow_type->decorateTransition(Argument::any())->willReturnArgument(0);
+ $workflow_manager = $this->prophesize(WorkflowTypeManager::class);
+ $workflow_manager->createInstance('test_type', Argument::any())->willReturn($workflow_type->reveal());
+ $container->set('plugin.manager.workflows.type', $workflow_manager->reveal());
+ \Drupal::setContainer($container);
+ }
+
+ /**
+ * @covers ::__construct
+ * @covers ::id
+ * @covers ::label
+ */
+ public function testGetters() {
+ $state = new Transition(
+ $this->prophesize(WorkflowInterface::class)->reveal(),
+ 'draft_published',
+ 'Publish',
+ ['draft'],
+ 'published'
+ );
+ $this->assertEquals('draft_published', $state->id());
+ $this->assertEquals('Publish', $state->label());
+ }
+
+ /**
+ * @covers ::from
+ * @covers ::to
+ */
+ public function testFromAndTo() {
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow
+ ->addState('draft', 'Draft')
+ ->addState('published', 'Published')
+ ->addTransition('publish', 'Publish', ['draft'], 'published');
+ $state = $workflow->getState('draft');
+ $transition = $state->getTransitionTo('published');
+ $this->assertEquals($state, $transition->from()['draft']);
+ $this->assertEquals($workflow->getState('published'), $transition->to());
+ }
+
+}
diff --git a/core/modules/workflows/tests/src/Unit/WorkflowTest.php b/core/modules/workflows/tests/src/Unit/WorkflowTest.php
new file mode 100644
index 0000000..49e8671
--- /dev/null
+++ b/core/modules/workflows/tests/src/Unit/WorkflowTest.php
@@ -0,0 +1,654 @@
+<?php
+
+namespace Drupal\Tests\workflows\Unit;
+
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Tests\UnitTestCase;
+use Drupal\workflows\Entity\Workflow;
+use Drupal\workflows\State;
+use Drupal\workflows\Transition;
+use Drupal\workflows\WorkflowTypeInterface;
+use Drupal\workflows\WorkflowTypeManager;
+use Prophecy\Argument;
+
+/**
+ * @coversDefaultClass \Drupal\workflows\Entity\Workflow
+ *
+ * @group workflows
+ */
+class WorkflowTest extends UnitTestCase {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+ // Create a container so that the plugin manager and workflow type can be
+ // mocked.
+ $container = new ContainerBuilder();
+ $workflow_type = $this->prophesize(WorkflowTypeInterface::class);
+ $workflow_type->decorateState(Argument::any())->willReturnArgument(0);
+ $workflow_type->decorateTransition(Argument::any())->willReturnArgument(0);
+ $workflow_manager = $this->prophesize(WorkflowTypeManager::class);
+ $workflow_manager->createInstance('test_type', Argument::any())->willReturn($workflow_type->reveal());
+ $container->set('plugin.manager.workflows.type', $workflow_manager->reveal());
+ \Drupal::setContainer($container);
+ }
+
+ /**
+ * @covers ::addState
+ * @covers ::hasState
+ */
+ public function testAddAndHasState() {
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $this->assertFalse($workflow->hasState('draft'));
+
+ // By default states are ordered in the order added.
+ $workflow->addState('draft', 'Draft');
+ $this->assertTrue($workflow->hasState('draft'));
+ $this->assertFalse($workflow->hasState('published'));
+ $this->assertEquals(0, $workflow->getState('draft')->weight());
+ // Adding a state does not set up a transition to itself.
+ $this->assertFalse($workflow->hasTransitionFromStateToState('draft', 'draft'));
+
+ // New states are added with a new weight 1 more than the current highest
+ // weight.
+ $workflow->addState('published', 'Published');
+ $this->assertEquals(1, $workflow->getState('published')->weight());
+ }
+
+ /**
+ * @covers ::addState
+ */
+ public function testAddStateException() {
+ $this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' already exists in workflow 'test'");
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow->addState('draft', 'Draft');
+ $workflow->addState('draft', 'Draft');
+ }
+
+ /**
+ * @covers ::addState
+ */
+ public function testAddStateInvalidIdException() {
+ $this->setExpectedException(\InvalidArgumentException::class, "The state ID 'draft-draft' must contain only lowercase letters, numbers, and underscores");
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow->addState('draft-draft', 'Draft');
+ }
+
+ /**
+ * @covers ::getStates
+ */
+ public function testGetStates() {
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+
+ // Getting states works when there are none.
+ $this->assertArrayEquals([], array_keys($workflow->getStates()));
+ $this->assertArrayEquals([], array_keys($workflow->getStates([])));
+
+ // By default states are ordered in the order added.
+ $workflow
+ ->addState('draft', 'Draft')
+ ->addState('published', 'Published')
+ ->addState('archived', 'Archived');
+
+ // Ensure we're returning state objects.
+ $this->assertInstanceOf(State::class, $workflow->getStates()['draft']);
+
+ // Passing in no IDs returns all states.
+ $this->assertArrayEquals(['draft', 'published', 'archived'], array_keys($workflow->getStates()));
+
+ // The order of states is by weight.
+ $workflow->setStateWeight('published', -1);
+ $this->assertArrayEquals(['published', 'draft', 'archived'], array_keys($workflow->getStates()));
+
+ // The label is also used for sorting if weights are equal.
+ $workflow->setStateWeight('archived', 0);
+ $this->assertArrayEquals(['published', 'archived', 'draft'], array_keys($workflow->getStates()));
+
+ // You can limit the states returned by passing in states IDs.
+ $this->assertArrayEquals(['archived', 'draft'], array_keys($workflow->getStates(['draft', 'archived'])));
+
+ // An empty array does not load all states.
+ $this->assertArrayEquals([], array_keys($workflow->getStates([])));
+ }
+
+ /**
+ * @covers ::getStates
+ */
+ public function testGetStatesException() {
+ $this->setExpectedException(\InvalidArgumentException::class, "The state 'state_that_does_not_exist' does not exist in workflow 'test'");
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow->getStates(['state_that_does_not_exist']);
+ }
+
+ /**
+ * @covers ::getState
+ */
+ public function testGetState() {
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ // By default states are ordered in the order added.
+ $workflow
+ ->addState('draft', 'Draft')
+ ->addState('published', 'Published')
+ ->addState('archived', 'Archived')
+ ->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft')
+ ->addTransition('publish', 'Publish', ['draft'], 'published');
+
+ // Ensure we're returning state objects and they are set up correctly
+ $this->assertInstanceOf(State::class, $workflow->getState('draft'));
+ $this->assertEquals('archived', $workflow->getState('archived')->id());
+ $this->assertEquals('Archived', $workflow->getState('archived')->label());
+
+ $draft = $workflow->getState('draft');
+ $this->assertTrue($draft->canTransitionTo('draft'));
+ $this->assertTrue($draft->canTransitionTo('published'));
+ $this->assertFalse($draft->canTransitionTo('archived'));
+ $this->assertEquals('Publish', $draft->getTransitionTo('published')->label());
+ $this->assertEquals(0, $draft->weight());
+ $this->assertEquals(1, $workflow->getState('published')->weight());
+ $this->assertEquals(2, $workflow->getState('archived')->weight());
+ }
+
+ /**
+ * @covers ::getState
+ */
+ public function testGetStateException() {
+ $this->setExpectedException(\InvalidArgumentException::class, "The state 'state_that_does_not_exist' does not exist in workflow 'test'");
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow->getState('state_that_does_not_exist');
+ }
+
+ /**
+ * @covers ::setStateLabel
+ */
+ public function testSetStateLabel() {
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow->addState('draft', 'Draft');
+ $this->assertEquals('Draft', $workflow->getState('draft')->label());
+ $workflow->setStateLabel('draft', 'Unpublished');
+ $this->assertEquals('Unpublished', $workflow->getState('draft')->label());
+ }
+
+ /**
+ * @covers ::setStateLabel
+ */
+ public function testSetStateLabelException() {
+ $this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' does not exist in workflow 'test'");
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow->setStateLabel('draft', 'Draft');
+ }
+
+ /**
+ * @covers ::setStateWeight
+ */
+ public function testSetStateWeight() {
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow->addState('draft', 'Draft');
+ $this->assertEquals(0, $workflow->getState('draft')->weight());
+ $workflow->setStateWeight('draft', -10);
+ $this->assertEquals(-10, $workflow->getState('draft')->weight());
+ }
+
+ /**
+ * @covers ::setStateWeight
+ */
+ public function testSetStateWeightException() {
+ $this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' does not exist in workflow 'test'");
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow->setStateWeight('draft', 10);
+ }
+
+ /**
+ * @covers ::deleteState
+ */
+ public function testDeleteState() {
+ // Create a container so that the plugin manager and workflow type can be
+ // mocked and test that
+ // \Drupal\workflows\WorkflowTypeInterface::deleteState() is called
+ // correctly.
+ $container = new ContainerBuilder();
+ $workflow_type = $this->prophesize(WorkflowTypeInterface::class);
+ $workflow_type->decorateState(Argument::any())->willReturnArgument(0);
+ $workflow_type->decorateTransition(Argument::any())->willReturnArgument(0);
+ $workflow_type->deleteState('draft')->shouldBeCalled();
+ $workflow_type->deleteTransition('create_new_draft')->shouldBeCalled();
+ $workflow_manager = $this->prophesize(WorkflowTypeManager::class);
+ $workflow_manager->createInstance('test_type', Argument::any())->willReturn($workflow_type->reveal());
+ $container->set('plugin.manager.workflows.type', $workflow_manager->reveal());
+ \Drupal::setContainer($container);
+
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow
+ ->addState('draft', 'Draft')
+ ->addState('published', 'Published')
+ ->addTransition('publish', 'Publish', ['draft', 'published'], 'published')
+ ->addTransition('create_new_draft', 'Create new draft', ['draft', 'published'], 'draft');
+ $this->assertCount(2, $workflow->getStates());
+ $this->assertCount(2, $workflow->getState('published')->getTransitions());
+ $workflow->deleteState('draft');
+ $this->assertFalse($workflow->hasState('draft'));
+ $this->assertCount(1, $workflow->getStates());
+ $this->assertCount(1, $workflow->getState('published')->getTransitions());
+ }
+
+ /**
+ * @covers ::deleteState
+ */
+ public function testDeleteStateException() {
+ $this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' does not exist in workflow 'test'");
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow->deleteState('draft');
+ }
+
+ /**
+ * @covers ::deleteState
+ */
+ public function testDeleteOnlyStateException() {
+ $this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' can not be deleted from workflow 'test' as it is the only state");
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow->addState('draft', 'Draft');
+ $workflow->deleteState('draft');
+ }
+
+ /**
+ * @covers ::getInitialState
+ */
+ public function testGetInitialState() {
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+
+ // By default states are ordered in the order added.
+ $workflow
+ ->addState('draft', 'Draft')
+ ->addState('published', 'Published')
+ ->addState('archived', 'Archived');
+
+ $this->assertEquals('draft', $workflow->getInitialState()->id());
+
+ // Make published the first state.
+ $workflow->setStateWeight('published', -1);
+ $this->assertEquals('published', $workflow->getInitialState()->id());
+ }
+
+ /**
+ * @covers ::addTransition
+ * @covers ::hasTransition
+ */
+ public function testAddTransition() {
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+
+ // By default states are ordered in the order added.
+ $workflow
+ ->addState('draft', 'Draft')
+ ->addState('published', 'Published');
+
+ $this->assertFalse($workflow->getState('draft')->canTransitionTo('published'));
+ $workflow->addTransition('publish', 'Publish', ['draft'], 'published');
+ $this->assertTrue($workflow->getState('draft')->canTransitionTo('published'));
+ $this->assertEquals(0, $workflow->getTransition('publish')->weight());
+ $this->assertTrue($workflow->hasTransition('publish'));
+ $this->assertFalse($workflow->hasTransition('draft'));
+
+ $workflow->addTransition('save_publish', 'Save', ['published'], 'published');
+ $this->assertEquals(1, $workflow->getTransition('save_publish')->weight());
+ }
+
+ /**
+ * @covers ::addTransition
+ */
+ public function testAddTransitionDuplicateException() {
+ $this->setExpectedException(\InvalidArgumentException::class, "The transition 'publish' already exists in workflow 'test'");
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow->addState('published', 'Published');
+ $workflow->addTransition('publish', 'Publish', ['published'], 'published');
+ $workflow->addTransition('publish', 'Publish', ['published'], 'published');
+ }
+
+ /**
+ * @covers ::addTransition
+ */
+ public function testAddTransitionInvalidIdException() {
+ $this->setExpectedException(\InvalidArgumentException::class, "The transition ID 'publish-publish' must contain only lowercase letters, numbers, and underscores");
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow->addState('published', 'Published');
+ $workflow->addTransition('publish-publish', 'Publish', ['published'], 'published');
+ }
+
+ /**
+ * @covers ::addTransition
+ */
+ public function testAddTransitionMissingFromException() {
+ $this->setExpectedException(\InvalidArgumentException::class, "The state 'draft' does not exist in workflow 'test'");
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow->addState('published', 'Published');
+ $workflow->addTransition('publish', 'Publish', ['draft'], 'published');
+ }
+
+ /**
+ * @covers ::addTransition
+ */
+ public function testAddTransitionDuplicateTransitionStatesException() {
+ $this->setExpectedException(\InvalidArgumentException::class, "The 'publish' transition already allows 'draft' to 'published' transitions in workflow 'test'");
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow
+ ->addState('draft', 'Draft')
+ ->addState('published', 'Published');
+ $workflow->addTransition('publish', 'Publish', ['draft', 'published'], 'published');
+ $workflow->addTransition('draft_to_published', 'Publish a draft', ['draft'], 'published');
+ }
+
+ /**
+ * @covers ::addTransition
+ */
+ public function testAddTransitionConsistentAfterFromCatch() {
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow->addState('published', 'Published');
+ try {
+ $workflow->addTransition('publish', 'Publish', ['draft'], 'published');
+ }
+ catch (\InvalidArgumentException $e) {
+ }
+ // Ensure that the workflow is not left in an inconsistent state after an
+ // exception is thrown from Workflow::setTransitionFromStates() whilst
+ // calling Workflow::addTransition().
+ $this->assertFalse($workflow->hasTransition('publish'));
+ }
+
+ /**
+ * @covers ::addTransition
+ */
+ public function testAddTransitionMissingToException() {
+ $this->setExpectedException(\InvalidArgumentException::class, "The state 'published' does not exist in workflow 'test'");
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow->addState('draft', 'Draft');
+ $workflow->addTransition('publish', 'Publish', ['draft'], 'published');
+ }
+
+ /**
+ * @covers ::getTransitions
+ * @covers ::setTransitionWeight
+ */
+ public function testGetTransitions() {
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+
+ // Getting transitions works when there are none.
+ $this->assertArrayEquals([], array_keys($workflow->getTransitions()));
+ $this->assertArrayEquals([], array_keys($workflow->getTransitions([])));
+
+ // By default states are ordered in the order added.
+ $workflow
+ ->addState('a', 'A')
+ ->addState('b', 'B')
+ ->addTransition('a_a', 'A to A', ['a'], 'a')
+ ->addTransition('a_b', 'A to B', ['a'], 'b');
+
+ // Ensure we're returning transition objects.
+ $this->assertInstanceOf(Transition::class, $workflow->getTransitions()['a_a']);
+
+ // Passing in no IDs returns all transitions.
+ $this->assertArrayEquals(['a_a', 'a_b'], array_keys($workflow->getTransitions()));
+
+ // The order of states is by weight.
+ $workflow->setTransitionWeight('a_b', -1);
+ $this->assertArrayEquals(['a_b', 'a_a'], array_keys($workflow->getTransitions()));
+
+ // If all weights are equal it will fallback to labels.
+ $workflow->setTransitionWeight('a_b', 0);
+ $this->assertArrayEquals(['a_a', 'a_b'], array_keys($workflow->getTransitions()));
+ $workflow->setTransitionLabel('a_b', 'A B');
+ $this->assertArrayEquals(['a_b', 'a_a'], array_keys($workflow->getTransitions()));
+
+ // You can limit the states returned by passing in states IDs.
+ $this->assertArrayEquals(['a_a'], array_keys($workflow->getTransitions(['a_a'])));
+
+ // An empty array does not load all states.
+ $this->assertArrayEquals([], array_keys($workflow->getTransitions([])));
+ }
+
+
+ /**
+ * @covers ::getTransition
+ */
+ public function testGetTransition() {
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ // By default states are ordered in the order added.
+ $workflow
+ ->addState('draft', 'Draft')
+ ->addState('published', 'Published')
+ ->addState('archived', 'Archived')
+ ->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft')
+ ->addTransition('publish', 'Publish', ['draft'], 'published');
+
+ // Ensure we're returning state objects and they are set up correctly
+ $this->assertInstanceOf(Transition::class, $workflow->getTransition('create_new_draft'));
+ $this->assertEquals('publish', $workflow->getTransition('publish')->id());
+ $this->assertEquals('Publish', $workflow->getTransition('publish')->label());
+
+ $transition = $workflow->getTransition('publish');
+ $this->assertEquals($workflow->getState('draft'), $transition->from()['draft']);
+ $this->assertEquals($workflow->getState('published'), $transition->to());
+ }
+
+ /**
+ * @covers ::getTransition
+ */
+ public function testGetTransitionException() {
+ $this->setExpectedException(\InvalidArgumentException::class, "The transition 'transition_that_does_not_exist' does not exist in workflow 'test'");
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow->getTransition('transition_that_does_not_exist');
+ }
+
+ /**
+ * @covers ::getTransitionsForState
+ */
+ public function testGetTransitionsForState() {
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ // By default states are ordered in the order added.
+ $workflow
+ ->addState('draft', 'Draft')
+ ->addState('published', 'Published')
+ ->addState('archived', 'Archived')
+ ->addTransition('create_new_draft', 'Create new draft', ['archived', 'draft'], 'draft')
+ ->addTransition('publish', 'Publish', ['draft', 'published'], 'published')
+ ->addTransition('archive', 'Archive', ['published'], 'archived');
+
+ $this->assertEquals(['create_new_draft', 'publish'], array_keys($workflow->getTransitionsForState('draft')));
+ $this->assertEquals(['create_new_draft'], array_keys($workflow->getTransitionsForState('draft', 'to')));
+ $this->assertEquals(['publish', 'archive'], array_keys($workflow->getTransitionsForState('published')));
+ $this->assertEquals(['publish'], array_keys($workflow->getTransitionsForState('published', 'to')));
+ $this->assertEquals(['create_new_draft'], array_keys($workflow->getTransitionsForState('archived', 'from')));
+ $this->assertEquals(['archive'], array_keys($workflow->getTransitionsForState('archived', 'to')));
+ }
+
+
+ /**
+ * @covers ::getTransitionFromStateToState
+ * @covers ::hasTransitionFromStateToState
+ */
+ public function testGetTransitionFromStateToState() {
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ // By default states are ordered in the order added.
+ $workflow
+ ->addState('draft', 'Draft')
+ ->addState('published', 'Published')
+ ->addState('archived', 'Archived')
+ ->addTransition('create_new_draft', 'Create new draft', ['archived', 'draft'], 'draft')
+ ->addTransition('publish', 'Publish', ['draft', 'published'], 'published')
+ ->addTransition('archive', 'Archive', ['published'], 'archived');
+
+ $this->assertTrue($workflow->hasTransitionFromStateToState('draft', 'published'));
+ $this->assertFalse($workflow->hasTransitionFromStateToState('archived', 'archived'));
+ $transition = $workflow->getTransitionFromStateToState('published', 'archived');
+ $this->assertEquals('Archive', $transition->label());
+ }
+
+ /**
+ * @covers ::getTransitionFromStateToState
+ */
+ public function testGetTransitionFromStateToStateException() {
+ $this->setExpectedException(\InvalidArgumentException::class, "The transition from 'archived' to 'archived' does not exist in workflow 'test'");
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ // By default states are ordered in the order added.
+ $workflow
+ ->addState('draft', 'Draft')
+ ->addState('published', 'Published')
+ ->addState('archived', 'Archived')
+ ->addTransition('create_new_draft', 'Create new draft', ['archived', 'draft'], 'draft')
+ ->addTransition('publish', 'Publish', ['draft', 'published'], 'published')
+ ->addTransition('archive', 'Archive', ['published'], 'archived');
+
+ $workflow->getTransitionFromStateToState('archived', 'archived');
+ }
+
+ /**
+ * @covers ::setTransitionLabel
+ */
+ public function testSetTransitionLabel() {
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow
+ ->addState('draft', 'Draft')
+ ->addState('published', 'Published')
+ ->addTransition('publish', 'Publish', ['draft'], 'published');
+ $this->assertEquals('Publish', $workflow->getTransition('publish')->label());
+ $workflow->setTransitionLabel('publish', 'Publish!');
+ $this->assertEquals('Publish!', $workflow->getTransition('publish')->label());
+ }
+
+ /**
+ * @covers ::setTransitionLabel
+ */
+ public function testSetTransitionLabelException() {
+ $this->setExpectedException(\InvalidArgumentException::class, "The transition 'draft-published' does not exist in workflow 'test'");
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow->addState('published', 'Published');
+ $workflow->setTransitionLabel('draft-published', 'Publish');
+ }
+
+ /**
+ * @covers ::setTransitionWeight
+ */
+ public function testSetTransitionWeight() {
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow
+ ->addState('draft', 'Draft')
+ ->addState('published', 'Published')
+ ->addTransition('publish', 'Publish', ['draft'], 'published');
+ $this->assertEquals(0, $workflow->getTransition('publish')->weight());
+ $workflow->setTransitionWeight('publish', 10);
+ $this->assertEquals(10, $workflow->getTransition('publish')->weight());
+ }
+
+ /**
+ * @covers ::setTransitionWeight
+ */
+ public function testSetTransitionWeightException() {
+ $this->setExpectedException(\InvalidArgumentException::class, "The transition 'draft-published' does not exist in workflow 'test'");
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow->addState('published', 'Published');
+ $workflow->setTransitionWeight('draft-published', 10);
+ }
+
+ /**
+ * @covers ::setTransitionFromStates
+ */
+ public function testSetTransitionFromStates() {
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow
+ ->addState('draft', 'Draft')
+ ->addState('published', 'Published')
+ ->addState('archived', 'Archived')
+ ->addTransition('test', 'Test', ['draft'], 'draft');
+
+ $this->assertTrue($workflow->hasTransitionFromStateToState('draft', 'draft'));
+ $this->assertFalse($workflow->hasTransitionFromStateToState('published', 'draft'));
+ $this->assertFalse($workflow->hasTransitionFromStateToState('archived', 'draft'));
+ $workflow->setTransitionFromStates('test', ['draft', 'published', 'archived']);
+ $this->assertTrue($workflow->hasTransitionFromStateToState('draft', 'draft'));
+ $this->assertTrue($workflow->hasTransitionFromStateToState('published', 'draft'));
+ $this->assertTrue($workflow->hasTransitionFromStateToState('archived', 'draft'));
+ $workflow->setTransitionFromStates('test', ['published', 'archived']);
+ $this->assertFalse($workflow->hasTransitionFromStateToState('draft', 'draft'));
+ $this->assertTrue($workflow->hasTransitionFromStateToState('published', 'draft'));
+ $this->assertTrue($workflow->hasTransitionFromStateToState('archived', 'draft'));
+ }
+
+ /**
+ * @covers ::setTransitionFromStates
+ */
+ public function testSetTransitionFromStatesMissingTransition() {
+ $this->setExpectedException(\InvalidArgumentException::class, "The transition 'test' does not exist in workflow 'test'");
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow
+ ->addState('draft', 'Draft')
+ ->addState('published', 'Published')
+ ->addState('archived', 'Archived')
+ ->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft');
+
+ $workflow->setTransitionFromStates('test', ['draft', 'published', 'archived']);
+ }
+
+ /**
+ * @covers ::setTransitionFromStates
+ */
+ public function testSetTransitionFromStatesMissingState() {
+ $this->setExpectedException(\InvalidArgumentException::class, "The state 'published' does not exist in workflow 'test'");
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow
+ ->addState('draft', 'Draft')
+ ->addState('archived', 'Archived')
+ ->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft');
+
+ $workflow->setTransitionFromStates('create_new_draft', ['draft', 'published', 'archived']);
+ }
+
+ /**
+ * @covers ::deleteTransition
+ */
+ public function testDeleteTransition() {
+ // Create a container so that the plugin manager and workflow type can be
+ // mocked and test that
+ // \Drupal\workflows\WorkflowTypeInterface::deleteState() is called
+ // correctly.
+ $container = new ContainerBuilder();
+ $workflow_type = $this->prophesize(WorkflowTypeInterface::class);
+ $workflow_type->decorateState(Argument::any())->willReturnArgument(0);
+ $workflow_type->decorateTransition(Argument::any())->willReturnArgument(0);
+ $workflow_type->deleteTransition('publish')->shouldBeCalled();
+ $workflow_manager = $this->prophesize(WorkflowTypeManager::class);
+ $workflow_manager->createInstance('test_type', Argument::any())->willReturn($workflow_type->reveal());
+ $container->set('plugin.manager.workflows.type', $workflow_manager->reveal());
+ \Drupal::setContainer($container);
+
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow
+ ->addState('draft', 'Draft')
+ ->addState('published', 'Published')
+ ->addTransition('create_new_draft', 'Create new draft', ['draft'], 'draft')
+ ->addTransition('publish', 'Publish', ['draft'], 'published');
+ $this->assertTrue($workflow->getState('draft')->canTransitionTo('published'));
+ $workflow->deleteTransition('publish');
+ $this->assertFalse($workflow->getState('draft')->canTransitionTo('published'));
+ $this->assertTrue($workflow->getState('draft')->canTransitionTo('draft'));
+ }
+
+ /**
+ * @covers ::deleteTransition
+ */
+ public function testDeleteTransitionException() {
+ $this->setExpectedException(\InvalidArgumentException::class, "The transition 'draft-published' does not exist in workflow 'test'");
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $workflow->addState('published', 'Published');
+ $workflow->deleteTransition('draft-published');
+ }
+
+ /**
+ * @covers ::status
+ */
+ public function testStatus() {
+ $workflow = new Workflow(['id' => 'test', 'type' => 'test_type'], 'workflow');
+ $this->assertFalse($workflow->status());
+ $workflow->addState('published', 'Published');
+ $this->assertTrue($workflow->status());
+ }
+
+}
diff --git a/core/modules/workflows/workflows.info.yml b/core/modules/workflows/workflows.info.yml
new file mode 100644
index 0000000..692d086
--- /dev/null
+++ b/core/modules/workflows/workflows.info.yml
@@ -0,0 +1,7 @@
+name: 'Workflows'
+type: module
+description: 'Provides UI and API for managing workflows. This module can be used with the Content moderation module to add highly customisable workflows to content.'
+version: VERSION
+core: 8.x
+package: Core (Experimental)
+configure: workflows.overview
diff --git a/core/modules/workflows/workflows.links.action.yml b/core/modules/workflows/workflows.links.action.yml
new file mode 100644
index 0000000..e3a80f7
--- /dev/null
+++ b/core/modules/workflows/workflows.links.action.yml
@@ -0,0 +1,5 @@
+entity.workflow.add_form:
+ route_name: 'entity.workflow.add_form'
+ title: 'Add workflow'
+ appears_on:
+ - entity.workflow.collection
diff --git a/core/modules/workflows/workflows.links.menu.yml b/core/modules/workflows/workflows.links.menu.yml
new file mode 100644
index 0000000..a6ac512
--- /dev/null
+++ b/core/modules/workflows/workflows.links.menu.yml
@@ -0,0 +1,7 @@
+# Workflow menu items definition
+entity.workflow.collection:
+ title: 'Workflows'
+ route_name: entity.workflow.collection
+ description: 'Configure workflows.'
+ parent: system.admin_config_workflow
+
diff --git a/core/modules/workflows/workflows.module b/core/modules/workflows/workflows.module
new file mode 100644
index 0000000..26f72b4
--- /dev/null
+++ b/core/modules/workflows/workflows.module
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * @file
+ * Provides hook implementations for the Workflow UI module.
+ */
+
+use Drupal\Core\Routing\RouteMatchInterface;
+
+use Drupal\workflows\Form\WorkflowAddForm;
+use Drupal\workflows\Form\WorkflowEditForm;
+use Drupal\workflows\Form\WorkflowDeleteForm;
+use Drupal\workflows\Form\WorkflowStateAddForm;
+use Drupal\workflows\Form\WorkflowStateEditForm;
+use Drupal\workflows\Form\WorkflowStateDeleteForm;
+use Drupal\workflows\Form\WorkflowTransitionAddForm;
+use Drupal\workflows\Form\WorkflowTransitionEditForm;
+use Drupal\workflows\Form\WorkflowTransitionDeleteForm;
+use Drupal\workflows\WorkflowListBuilder;
+
+/**
+ * Implements hook_help().
+ */
+function workflows_help($route_name, RouteMatchInterface $route_match) {
+ switch ($route_name) {
+ // Main module help for the Workflow UI module.
+ case 'help.page.workflows':
+ $output = '';
+ $output .= '<h3>' . t('About') . '</h3>';
+ $output .= '<p>' . t('The Workflows module provides a UI and an API for creating workflows content. This lets site admins define workflows and their states, and then define transitions between those states. For more information, see the <a href=":workflow">online documentation for the Workflows module</a>.', [':workflow' => 'https://www.drupal.org/documentation/modules/workflows']) . '</p>';
+ return $output;
+ }
+}
+
+/**
+ * Implements hook_entity_type_build().
+ */
+function workflows_entity_type_build(array &$entity_types) {
+ /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */
+ $entity_types['workflow']
+ ->setFormClass('add', WorkflowAddForm::class)
+ ->setFormClass('edit', WorkflowEditForm::class)
+ ->setFormClass('delete', WorkflowDeleteForm::class)
+ ->setFormClass('add-state', WorkflowStateAddForm::class)
+ ->setFormClass('edit-state', WorkflowStateEditForm::class)
+ ->setFormClass('delete-state', WorkflowStateDeleteForm::class)
+ ->setFormClass('add-transition', WorkflowTransitionAddForm::class)
+ ->setFormClass('edit-transition', WorkflowTransitionEditForm::class)
+ ->setFormClass('delete-transition', WorkflowTransitionDeleteForm::class)
+ ->setListBuilderClass(WorkflowListBuilder::class)
+ ->set('admin_permission', 'administer workflows')
+ ->setLinkTemplate('add-form', '/admin/config/workflow/workflows/add')
+ ->setLinkTemplate('edit-form', '/admin/config/workflow/workflows/manage/{workflow}')
+ ->setLinkTemplate('delete-form', '/admin/config/workflow/workflows/manage/{workflow}/delete')
+ ->setLinkTemplate('add-state-form', '/admin/config/workflow/workflows/manage/{workflow}/add_state')
+ ->setLinkTemplate('add-transition-form', '/admin/config/workflow/workflows/manage/{workflow}/add_transition')
+ ->setLinkTemplate('collection', '/admin/config/workflow/workflows');
+}
diff --git a/core/modules/workflows/workflows.permissions.yml b/core/modules/workflows/workflows.permissions.yml
new file mode 100644
index 0000000..88573b6
--- /dev/null
+++ b/core/modules/workflows/workflows.permissions.yml
@@ -0,0 +1,4 @@
+'administer workflows':
+ title: 'Administer workflows'
+ description: 'Create and edit workflows.'
+ 'restrict access': TRUE
diff --git a/core/modules/workflows/workflows.routing.yml b/core/modules/workflows/workflows.routing.yml
new file mode 100644
index 0000000..02269cd
--- /dev/null
+++ b/core/modules/workflows/workflows.routing.yml
@@ -0,0 +1,47 @@
+entity.workflow.add_state_form:
+ path: '/admin/config/workflow/workflows/manage/{workflow}/add_state'
+ defaults:
+ _entity_form: 'workflow.add-state'
+ _title: 'Add state'
+ requirements:
+ _entity_access: 'workflow.edit'
+
+entity.workflow.edit_state_form:
+ path: '/admin/config/workflow/workflows/manage/{workflow}/state/{workflow_state}'
+ defaults:
+ _entity_form: 'workflow.edit-state'
+ _title: 'Edit state'
+ requirements:
+ _entity_access: 'workflow.edit'
+
+entity.workflow.delete_state_form:
+ path: '/admin/config/workflow/workflows/manage/{workflow}/state/{workflow_state}/delete'
+ defaults:
+ _form: '\Drupal\workflows\Form\WorkflowStateDeleteForm'
+ _title: 'Delete state'
+ requirements:
+ _entity_access: 'workflow.delete-state'
+
+entity.workflow.add_transition_form:
+ path: '/admin/config/workflow/workflows/manage/{workflow}/add_transition'
+ defaults:
+ _entity_form: 'workflow.add-transition'
+ _title: 'Add state'
+ requirements:
+ _entity_access: 'workflow.edit'
+
+entity.workflow.edit_transition_form:
+ path: '/admin/config/workflow/workflows/manage/{workflow}/transition/{workflow_transition}'
+ defaults:
+ _entity_form: 'workflow.edit-transition'
+ _title: 'Edit state'
+ requirements:
+ _entity_access: 'workflow.edit'
+
+entity.workflow.delete_transition_form:
+ path: '/admin/config/workflow/workflows/manage/{workflow}/transition/{workflow_transition}/delete'
+ defaults:
+ _form: '\Drupal\workflows\Form\WorkflowTransitionDeleteForm'
+ _title: 'Delete state'
+ requirements:
+ _entity_access: 'workflow.edit' \ No newline at end of file
diff --git a/core/modules/workflows/workflows.services.yml b/core/modules/workflows/workflows.services.yml
new file mode 100644
index 0000000..772bab7
--- /dev/null
+++ b/core/modules/workflows/workflows.services.yml
@@ -0,0 +1,6 @@
+services:
+ plugin.manager.workflows.type:
+ class: Drupal\workflows\WorkflowTypeManager
+ parent: default_plugin_manager
+ tags:
+ - { name: plugin_manager_cache_clear } \ No newline at end of file