diff --git a/core/modules/content_moderation/config/optional/views.view.moderated_content.yml b/core/modules/content_moderation/config/optional/views.view.moderated_content.yml new file mode 100644 index 0000000..8250643 --- /dev/null +++ b/core/modules/content_moderation/config/optional/views.view.moderated_content.yml @@ -0,0 +1,817 @@ +langcode: en +status: true +dependencies: + module: + - content_moderation + - node + - user +id: moderated_content +label: 'Moderated content' +module: views +description: 'Find and manage content.' +tag: '' +base_table: node_field_revision +base_field: vid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'view any unpublished content' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Filter + reset_button: true + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: full + options: + items_per_page: 50 + offset: 0 + id: 0 + total_pages: null + tags: + previous: '‹ Previous' + next: 'Next ›' + first: '« First' + last: 'Last »' + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + quantity: 9 + style: + type: table + options: + grouping: { } + row_class: '' + default_row_class: true + override: true + sticky: true + caption: '' + summary: '' + description: '' + columns: + title: title + type: type + name: name + moderation_state: moderation_state + changed: changed + info: + title: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + type: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + name: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + moderation_state: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + changed: + sortable: true + default_sort_order: desc + align: '' + separator: '' + empty_column: false + responsive: '' + default: changed + empty_table: true + row: + type: fields + fields: + title: + id: title + table: node_field_revision + field: title + relationship: none + group_type: group + admin_label: '' + label: Title + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: false + ellipsis: false + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: string + settings: + link_to_entity: true + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: node + entity_field: title + plugin_id: field + type: + id: type + table: node_field_data + field: type + relationship: nid + group_type: group + admin_label: '' + label: 'Content type' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: target_id + type: entity_reference_label + settings: + link: false + group_column: target_id + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: node + entity_field: type + plugin_id: field + name: + id: name + table: users_field_data + field: name + relationship: uid + group_type: group + admin_label: '' + label: Author + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: user_name + settings: + link_to_entity: true + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: user + entity_field: name + plugin_id: field + moderation_state: + id: moderation_state + table: node_field_revision + field: moderation_state + relationship: none + group_type: group + admin_label: '' + label: 'Moderation state' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: content_moderation_state + settings: { } + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: node + plugin_id: field + changed: + id: changed + table: node_field_revision + field: changed + relationship: none + group_type: group + admin_label: '' + label: Updated + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: false + ellipsis: false + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: timestamp + settings: + date_format: short + custom_date_format: '' + timezone: '' + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: node + entity_field: changed + plugin_id: field + operations: + id: operations + table: node_revision + field: operations + relationship: none + group_type: group + admin_label: '' + label: Operations + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + destination: true + entity_type: node + plugin_id: entity_operations + filters: + latest_revision: + id: latest_revision + table: node_revision + field: latest_revision + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: '' + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: node + plugin_id: latest_revision + title: + id: title + table: node_field_revision + field: title + relationship: none + group_type: group + admin_label: '' + operator: contains + value: '' + group: 1 + exposed: true + expose: + operator_id: title_op + label: Title + description: '' + use_operator: false + operator: title_op + identifier: title + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: node + entity_field: title + plugin_id: string + type: + id: type + table: node_field_data + field: type + relationship: nid + group_type: group + admin_label: '' + operator: in + value: { } + group: 1 + exposed: true + expose: + operator_id: type_op + label: 'Content type' + description: '' + use_operator: false + operator: type_op + identifier: type + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: node + entity_field: type + plugin_id: bundle + moderation_state: + id: moderation_state + table: node_field_revision + field: moderation_state + relationship: none + group_type: group + admin_label: '' + operator: in + value: + editorial-draft: editorial-draft + editorial-archived: editorial-archived + group: 1 + exposed: true + expose: + operator_id: moderation_state_op + label: 'Moderation state' + description: '' + use_operator: false + operator: moderation_state_op + identifier: moderation_state + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + reduce: true + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: node + plugin_id: moderation_state_filter + langcode: + id: langcode + table: node_field_revision + field: langcode + relationship: none + group_type: group + admin_label: '' + operator: in + value: { } + group: 1 + exposed: true + expose: + operator_id: langcode_op + label: Language + description: '' + use_operator: false + operator: langcode_op + identifier: langcode + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: node + entity_field: langcode + plugin_id: language + moderation_state_1: + id: moderation_state_1 + table: node_field_revision + field: moderation_state + relationship: none + group_type: group + admin_label: '' + operator: 'not in' + value: + editorial-published: editorial-published + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: node + plugin_id: moderation_state_filter + sorts: { } + title: 'Moderated content' + header: { } + footer: { } + empty: + area_text_custom: + id: area_text_custom + table: views + field: area_text_custom + relationship: none + group_type: group + admin_label: '' + empty: true + tokenize: false + content: 'No moderated content available. Only pending versions of content, such as drafts, are listed here.' + plugin_id: text_custom + relationships: + nid: + id: nid + table: node_field_revision + field: nid + relationship: none + group_type: group + admin_label: 'Get the actual content from a content revision.' + required: false + entity_type: node + entity_field: nid + plugin_id: standard + uid: + id: uid + table: node_field_revision + field: uid + relationship: none + group_type: group + admin_label: User + required: false + entity_type: node + entity_field: uid + plugin_id: standard + arguments: { } + display_extenders: { } + filter_groups: + operator: AND + groups: + 1: AND + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } + moderated_content: + display_plugin: page + id: moderated_content + display_title: 'Moderated content' + position: 1 + display_options: + display_extenders: { } + path: admin/content/moderated + display_description: '' + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/core/modules/content_moderation/content_moderation.links.task.yml b/core/modules/content_moderation/content_moderation.links.task.yml index f92e92e..95105d3 100644 --- a/core/modules/content_moderation/content_moderation.links.task.yml +++ b/core/modules/content_moderation/content_moderation.links.task.yml @@ -1,3 +1,14 @@ content_moderation.workflows: deriver: 'Drupal\content_moderation\Plugin\Derivative\DynamicLocalTasks' weight: 100 + +content_moderation.content: + title: 'Overview' + route_name: system.admin_content + parent_id: system.admin_content + +content_moderation.moderated_content: + title: 'Moderated content' + route_name: content_moderation.admin_moderated_content + parent_id: system.admin_content + weight: 1 diff --git a/core/modules/content_moderation/content_moderation.routing.yml b/core/modules/content_moderation/content_moderation.routing.yml index 2ee2c47..545c575 100644 --- a/core/modules/content_moderation/content_moderation.routing.yml +++ b/core/modules/content_moderation/content_moderation.routing.yml @@ -1,3 +1,11 @@ +content_moderation.admin_moderated_content: + path: '/admin/content/moderated' + defaults: + _controller: '\Drupal\content_moderation\Controller\ModeratedContentController::nodeListing' + _title: 'Moderated content' + requirements: + _permission: 'view any unpublished content' + content_moderation.workflow_type_edit_form: path: '/admin/config/workflow/workflows/manage/{workflow}/type/{entity_type_id}' defaults: diff --git a/core/modules/content_moderation/src/Controller/ModeratedContentController.php b/core/modules/content_moderation/src/Controller/ModeratedContentController.php new file mode 100644 index 0000000..cdd00dd --- /dev/null +++ b/core/modules/content_moderation/src/Controller/ModeratedContentController.php @@ -0,0 +1,25 @@ +entityTypeManager()->getDefinition('node'); + + return $this->entityTypeManager()->createHandlerInstance(ModeratedNodeListBuilder::class, $entity_type)->render(); + } + +} diff --git a/core/modules/content_moderation/src/ModeratedNodeListBuilder.php b/core/modules/content_moderation/src/ModeratedNodeListBuilder.php new file mode 100644 index 0000000..84d6c22 --- /dev/null +++ b/core/modules/content_moderation/src/ModeratedNodeListBuilder.php @@ -0,0 +1,124 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@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('date.formatter'), + $container->get('redirect.destination'), + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function load() { + $entities = []; + foreach ($this->getEntityRevisionIds() as $entity_id => $revision_id) { + $entities[$entity_id] = $this->storage->loadRevision($revision_id); + } + return $entities; + } + + /** + * Loads entity revision IDs using a pager sorted by the entity revision ID. + * + * @return array + * An array of entity revision IDs. + */ + protected function getEntityRevisionIds() { + $query = $this->entityTypeManager->getStorage('content_moderation_state')->getAggregateQuery() + ->aggregate('content_entity_id', 'MAX') + ->groupBy('content_entity_revision_id') + ->condition('content_entity_type_id', $this->entityTypeId) + ->condition('moderation_state', 'published', '<>') + ->sort('content_entity_revision_id', 'DESC'); + + // Only add the pager if a limit is specified. + if ($this->limit) { + $query->pager($this->limit); + } + + $result = $query->execute(); + + return $result ? array_column($result, 'content_entity_revision_id', 'content_entity_id_max') : []; + } + + /** + * {@inheritdoc} + */ + public function buildHeader() { + $header = parent::buildHeader(); + $header['status'] = $this->t('Moderation state'); + + return $header; + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $row = parent::buildRow($entity); + $row['status'] = $entity->moderation_state->value; + + return $row; + } + + /** + * {@inheritdoc} + */ + public function render() { + $build = parent::render(); + $build['table']['#empty'] = $this->t('There is no moderated @label yet. Only pending versions of @label, such as drafts, are listed here.', ['@label' => $this->entityType->getLabel()]); + + return $build; + } + +} diff --git a/core/modules/content_moderation/src/ViewsData.php b/core/modules/content_moderation/src/ViewsData.php index 0325db0..90c5a90 100644 --- a/core/modules/content_moderation/src/ViewsData.php +++ b/core/modules/content_moderation/src/ViewsData.php @@ -78,7 +78,7 @@ public function getViewsData() { ], ], ], - 'field' => ['default_formatter' => 'content_moderation_state'], + 'field' => ['id' => 'field', 'default_formatter' => 'content_moderation_state', 'field_name' => 'moderation_state'], 'filter' => ['id' => 'moderation_state_filter', 'allow empty' => TRUE], ]; @@ -98,7 +98,7 @@ public function getViewsData() { ], ], ], - 'field' => ['default_formatter' => 'content_moderation_state'], + 'field' => ['id' => 'field', 'default_formatter' => 'content_moderation_state', 'field_name' => 'moderation_state'], 'filter' => ['id' => 'moderation_state_filter', 'allow empty' => TRUE], ]; } diff --git a/core/modules/content_moderation/tests/src/Functional/ModeratedContentViewTest.php b/core/modules/content_moderation/tests/src/Functional/ModeratedContentViewTest.php new file mode 100644 index 0000000..7891b60 --- /dev/null +++ b/core/modules/content_moderation/tests/src/Functional/ModeratedContentViewTest.php @@ -0,0 +1,129 @@ +drupalCreateContentType(['type' => 'page', 'name' => 'Basic page'])->save(); + $this->drupalCreateContentType(['type' => 'article', 'name' => 'Article'])->save(); + $this->drupalCreateContentType(['type' => 'unmoderated_type', 'name' => 'Unmoderated type'])->save(); + + $workflow = Workflow::load('editorial'); + $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'page'); + $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'article'); + $workflow->save(); + + $this->adminUser = $this->drupalCreateUser(['access administration pages', 'view any unpublished content', 'administer nodes', 'bypass node access']); + } + + /** + * Tests the moderated content page. + */ + public function testModeratedContentPage() { + $assert_sesison = $this->assertSession(); + $this->drupalLogin($this->adminUser); + + // Use an explicit changed time to ensure the expected order in the content + // admin listing. We want these to appear in the table in the same order as + // they appear in the following code, and the 'moderated_content' view has a + // table style configuration with a default sort on the 'changed' field + // descending. + $time = \Drupal::time()->getRequestTime(); + $excluded_nodes['published_page'] = $this->drupalCreateNode(['type' => 'page', 'changed' => $time--, 'moderation_state' => 'published']); + $excluded_nodes['published_article'] = $this->drupalCreateNode(['type' => 'article', 'changed' => $time--, 'moderation_state' => 'published']); + + $excluded_nodes['unmoderated_type'] = $this->drupalCreateNode(['type' => 'unmoderated_type', 'changed' => $time--]); + $excluded_nodes['unmoderated_type']->setNewRevision(TRUE); + $excluded_nodes['unmoderated_type']->isDefaultRevision(FALSE); + $excluded_nodes['unmoderated_type']->changed->value = $time--; + $excluded_nodes['unmoderated_type']->save(); + + $nodes['published_then_draft_article'] = $this->drupalCreateNode(['type' => 'article', 'changed' => $time--, 'moderation_state' => 'published', 'title' => 'first article - published']); + $nodes['published_then_draft_article']->setNewRevision(TRUE); + $nodes['published_then_draft_article']->setTitle('first article - draft'); + $nodes['published_then_draft_article']->moderation_state->value = 'draft'; + $nodes['published_then_draft_article']->changed->value = $time--; + $nodes['published_then_draft_article']->save(); + + $nodes['published_then_archived_article'] = $this->drupalCreateNode(['type' => 'article', 'changed' => $time--, 'moderation_state' => 'published']); + $nodes['published_then_archived_article']->setNewRevision(TRUE); + $nodes['published_then_archived_article']->moderation_state->value = 'archived'; + $nodes['published_then_archived_article']->changed->value = $time--; + $nodes['published_then_archived_article']->save(); + + $nodes['draft_article'] = $this->drupalCreateNode(['type' => 'article', 'changed' => $time--, 'moderation_state' => 'draft']); + $nodes['draft_page_1'] = $this->drupalCreateNode(['type' => 'page', 'changed' => $time--, 'moderation_state' => 'draft']); + $nodes['draft_page_2'] = $this->drupalCreateNode(['type' => 'page', 'changed' => $time, 'moderation_state' => 'draft']); + + // Verify view, edit, and delete links for any content. + $this->drupalGet('admin/content/moderated'); + $assert_sesison->statusCodeEquals(200); + + // Check that nodes with pending revisions appear in the view. + $node_type_labels = $this->xpath('//td[contains(@class, "views-field-type")]'); + $delta = 0; + foreach ($nodes as $node) { + $assert_sesison->linkByHrefExists('node/' . $node->id()); + $assert_sesison->linkByHrefExists('node/' . $node->id() . '/edit'); + $assert_sesison->linkByHrefExists('node/' . $node->id() . '/delete'); + // Verify that we can see the content type label. + $this->assertEquals($node->type->entity->label(), trim($node_type_labels[$delta]->getText())); + $delta++; + } + + // Check that nodes that are not moderated or do not have a pending revision + // do not appear in the view. + foreach ($excluded_nodes as $node) { + $assert_sesison->linkByHrefNotExists('node/' . $node->id()); + } + + // Check that the latest revision is displayed. + $assert_sesison->pageTextContains('first article - draft'); + $assert_sesison->pageTextNotContains('first article - published'); + + // Verify filtering by moderation state. + $this->drupalGet('admin/content/moderated', ['query' => ['moderation_state' => 'editorial-draft']]); + + $assert_sesison->linkByHrefExists('node/' . $nodes['published_then_draft_article']->id() . '/edit'); + $assert_sesison->linkByHrefExists('node/' . $nodes['draft_article']->id() . '/edit'); + $assert_sesison->linkByHrefExists('node/' . $nodes['draft_page_1']->id() . '/edit'); + $assert_sesison->linkByHrefExists('node/' . $nodes['draft_page_1']->id() . '/edit'); + $assert_sesison->linkByHrefNotExists('node/' . $nodes['published_then_archived_article']->id() . '/edit'); + + // Verify filtering by moderation state and content type. + $this->drupalGet('admin/content/moderated', ['query' => ['moderation_state' => 'editorial-draft', 'type' => 'page']]); + + $assert_sesison->linkByHrefExists('node/' . $nodes['draft_page_1']->id() . '/edit'); + $assert_sesison->linkByHrefExists('node/' . $nodes['draft_page_2']->id() . '/edit'); + $assert_sesison->linkByHrefNotExists('node/' . $nodes['published_then_draft_article']->id() . '/edit'); + $assert_sesison->linkByHrefNotExists('node/' . $nodes['published_then_archived_article']->id() . '/edit'); + $assert_sesison->linkByHrefNotExists('node/' . $nodes['draft_article']->id() . '/edit'); + } + +} diff --git a/core/modules/views/src/EntityViewsData.php b/core/modules/views/src/EntityViewsData.php index 49b0af8..152101a 100644 --- a/core/modules/views/src/EntityViewsData.php +++ b/core/modules/views/src/EntityViewsData.php @@ -183,6 +183,13 @@ public function getViewsData() { 'id' => 'entity_operations', ], ]; + $data[$revision_table]['operations'] = [ + 'field' => [ + 'title' => $this->t('Operations links'), + 'help' => $this->t('Provides links to perform entity operations.'), + 'id' => 'entity_operations', + ], + ]; } if ($this->entityType->hasViewBuilderClass()) {