Browse Source

HeartbeatStream
Converting entity from Config to Content

logicp 8 years ago
parent
commit
dde209c0fc

+ 1 - 1
config/schema/heartbeat_stream.schema.yml → config/schema/heartbeat_stream.schema.yml.bak

@@ -1,5 +1,5 @@
 heartbeat8.heartbeat_stream.*:
-  type: config_entity
+  type: content_entity
   label: 'Heartbeat stream config'
   mapping:
     id:

+ 5 - 0
heartbeat8.links.action.yml

@@ -15,3 +15,8 @@ entity.heartbeat_stream.add_form:
   appears_on:
     - entity.heartbeat_stream.collection
 
+entity.heartbeat_stream.add_form:
+  route_name: entity.heartbeat_stream.add_form
+  title: 'Add Heartbeat stream'
+  appears_on:
+    - entity.heartbeat_stream.collection

+ 14 - 0
heartbeat8.links.menu.yml

@@ -25,3 +25,17 @@ entity.heartbeat_stream.collection:
   parent: system.admin_structure
   weight: 100
 
+
+# Heartbeat stream menu items definition
+entity.heartbeat_stream.collection:
+  title: 'Heartbeat stream list'
+  route_name: entity.heartbeat_stream.collection
+  description: 'List Heartbeat stream entities'
+  parent: system.admin_structure
+  weight: 100
+
+heartbeat_stream.admin.structure.settings:
+  title: Heartbeat stream settings
+  description: 'Configure Heartbeat stream entities'
+  route_name: heartbeat_stream.settings
+  parent: system.admin_structure

+ 27 - 0
heartbeat8.links.task.yml

@@ -21,3 +21,30 @@ entity.heartbeat.delete_form:
   title: Delete
   weight: 10
 
+# Heartbeat stream routing definition
+heartbeat_stream.settings_tab:
+  route_name: heartbeat_stream.settings
+  title: 'Settings'
+  base_route: heartbeat_stream.settings
+
+entity.heartbeat_stream.canonical:
+  route_name: entity.heartbeat_stream.canonical
+  base_route: entity.heartbeat_stream.canonical
+  title: 'View'
+
+entity.heartbeat_stream.edit_form:
+  route_name: entity.heartbeat_stream.edit_form
+  base_route: entity.heartbeat_stream.canonical
+  title: 'Edit'
+
+entity.heartbeat_stream.version_history:
+  route_name: entity.heartbeat_stream.version_history
+  base_route: entity.heartbeat_stream.canonical
+  title: 'Revisions'
+
+entity.heartbeat_stream.delete_form:
+  route_name:  entity.heartbeat_stream.delete_form
+  base_route:  entity.heartbeat_stream.canonical
+  title: Delete
+  weight: 10
+

+ 33 - 0
heartbeat8.permissions.yml

@@ -31,3 +31,36 @@ revert all heartbeat revisions:
 delete all heartbeat revisions:
   title: 'Delete all revisions'
   description: 'Role requires permission to <em>view Heartbeat revisions</em> and <em>delete rights</em> for heartbeat entities in question or <em>administer heartbeat entities</em>.'
+add heartbeat stream entities:
+  title: 'Create new Heartbeat stream entities'
+
+administer heartbeat stream entities:
+  title: 'Administer Heartbeat stream entities'
+  description: 'Allow to access the administration form to configure Heartbeat stream entities.'
+  restrict access: true
+
+delete heartbeat stream entities:
+  title: 'Delete Heartbeat stream entities'
+
+edit heartbeat stream entities:
+  title: 'Edit Heartbeat stream entities'
+
+access heartbeat stream overview:
+  title: 'Access the Heartbeat stream overview page'
+
+view published heartbeat stream entities:
+  title: 'View published Heartbeat stream entities'
+
+view unpublished heartbeat stream entities:
+  title: 'View unpublished Heartbeat stream entities'
+
+view all heartbeat stream revisions:
+  title: 'View all Heartbeat stream revisions'
+
+revert all heartbeat stream revisions:
+  title: 'Revert all Heartbeat stream revisions'
+  description: 'Role requires permission <em>view Heartbeat stream revisions</em> and <em>edit rights</em> for heartbeat stream entities in question or <em>administer heartbeat stream entities</em>.'
+
+delete all heartbeat stream revisions:
+  title: 'Delete all revisions'
+  description: 'Role requires permission to <em>view Heartbeat stream revisions</em> and <em>delete rights</em> for heartbeat stream entities in question or <em>administer heartbeat stream entities</em>.'

+ 2 - 0
heartbeat8.services.yml

@@ -0,0 +1,2 @@
+#services:
+#  heartbeat8

+ 30 - 0
heartbeat_stream.page.inc

@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * @file
+ * Contains heartbeat_stream.page.inc.
+ *
+ * Page callback for Heartbeat stream entities.
+ */
+
+use Drupal\Core\Render\Element;
+
+/**
+ * Prepares variables for Heartbeat stream templates.
+ *
+ * Default template: heartbeat_stream.html.twig.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - elements: An associative array containing the user information and any
+ *   - attributes: HTML attributes for the containing element.
+ */
+function template_preprocess_heartbeat_stream(array &$variables) {
+  // Fetch HeartbeatStream Entity Object.
+  $heartbeat_stream = $variables['elements']['#heartbeat_stream'];
+
+  // Helpful $content variable for templates.
+  foreach (Element::children($variables['elements']) as $key) {
+    $variables['content'][$key] = $variables['elements'][$key];
+  }
+}

+ 163 - 0
src/Controller/HeartbeatStreamController.php

@@ -0,0 +1,163 @@
+<?php
+
+namespace Drupal\heartbeat8\Controller;
+
+use Drupal\Component\Utility\Xss;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\Url;
+use Drupal\heartbeat8\Entity\HeartbeatStreamInterface;
+
+/**
+ * Class HeartbeatStreamController.
+ *
+ *  Returns responses for Heartbeat stream routes.
+ *
+ * @package Drupal\heartbeat8\Controller
+ */
+class HeartbeatStreamController extends ControllerBase implements ContainerInjectionInterface {
+
+  /**
+   * Displays a Heartbeat stream  revision.
+   *
+   * @param int $heartbeat_stream_revision
+   *   The Heartbeat stream  revision ID.
+   *
+   * @return array
+   *   An array suitable for drupal_render().
+   */
+  public function revisionShow($heartbeat_stream_revision) {
+    $heartbeat_stream = $this->entityManager()->getStorage('heartbeat_stream')->loadRevision($heartbeat_stream_revision);
+    $view_builder = $this->entityManager()->getViewBuilder('heartbeat_stream');
+
+    return $view_builder->view($heartbeat_stream);
+  }
+
+  /**
+   * Page title callback for a Heartbeat stream  revision.
+   *
+   * @param int $heartbeat_stream_revision
+   *   The Heartbeat stream  revision ID.
+   *
+   * @return string
+   *   The page title.
+   */
+  public function revisionPageTitle($heartbeat_stream_revision) {
+    $heartbeat_stream = $this->entityManager()->getStorage('heartbeat_stream')->loadRevision($heartbeat_stream_revision);
+    return $this->t('Revision of %title from %date', array('%title' => $heartbeat_stream->label(), '%date' => format_date($heartbeat_stream->getRevisionCreationTime())));
+  }
+
+  /**
+   * Generates an overview table of older revisions of a Heartbeat stream .
+   *
+   * @param \Drupal\heartbeat8\Entity\HeartbeatStreamInterface $heartbeat_stream
+   *   A Heartbeat stream  object.
+   *
+   * @return array
+   *   An array as expected by drupal_render().
+   */
+  public function revisionOverview(HeartbeatStreamInterface $heartbeat_stream) {
+    $account = $this->currentUser();
+    $langcode = $heartbeat_stream->language()->getId();
+    $langname = $heartbeat_stream->language()->getName();
+    $languages = $heartbeat_stream->getTranslationLanguages();
+    $has_translations = (count($languages) > 1);
+    $heartbeat_stream_storage = $this->entityManager()->getStorage('heartbeat_stream');
+
+    $build['#title'] = $has_translations ? $this->t('@langname revisions for %title', ['@langname' => $langname, '%title' => $heartbeat_stream->label()]) : $this->t('Revisions for %title', ['%title' => $heartbeat_stream->label()]);
+    $header = array($this->t('Revision'), $this->t('Operations'));
+
+    $revert_permission = (($account->hasPermission("revert all heartbeat stream revisions") || $account->hasPermission('administer heartbeat stream entities')));
+    $delete_permission = (($account->hasPermission("delete all heartbeat stream revisions") || $account->hasPermission('administer heartbeat stream entities')));
+
+    $rows = array();
+
+    $vids = $heartbeat_stream_storage->revisionIds($heartbeat_stream);
+
+    $latest_revision = TRUE;
+
+    foreach (array_reverse($vids) as $vid) {
+      /** @var \Drupal\heartbeat8\HeartbeatStreamInterface $revision */
+      $revision = $heartbeat_stream_storage->loadRevision($vid);
+      // Only show revisions that are affected by the language that is being
+      // displayed.
+      if ($revision->hasTranslation($langcode) && $revision->getTranslation($langcode)->isRevisionTranslationAffected()) {
+        $username = [
+          '#theme' => 'username',
+          '#account' => $revision->getRevisionUser(),
+        ];
+
+        // Use revision link to link to revisions that are not active.
+        $date = \Drupal::service('date.formatter')->format($revision->revision_timestamp->value, 'short');
+        if ($vid != $heartbeat_stream->getRevisionId()) {
+          $link = $this->l($date, new Url('entity.heartbeat_stream.revision', ['heartbeat_stream' => $heartbeat_stream->id(), 'heartbeat_stream_revision' => $vid]));
+        }
+        else {
+          $link = $heartbeat_stream->link($date);
+        }
+
+        $row = [];
+        $column = [
+          'data' => [
+            '#type' => 'inline_template',
+            '#template' => '{% trans %}{{ date }} by {{ username }}{% endtrans %}{% if message %}<p class="revision-log">{{ message }}</p>{% endif %}',
+            '#context' => [
+              'date' => $link,
+              'username' => \Drupal::service('renderer')->renderPlain($username),
+              'message' => ['#markup' => $revision->revision_log_message->value, '#allowed_tags' => Xss::getHtmlTagList()],
+            ],
+          ],
+        ];
+        $row[] = $column;
+
+        if ($latest_revision) {
+          $row[] = [
+            'data' => [
+              '#prefix' => '<em>',
+              '#markup' => $this->t('Current revision'),
+              '#suffix' => '</em>',
+            ],
+          ];
+          foreach ($row as &$current) {
+            $current['class'] = ['revision-current'];
+          }
+          $latest_revision = FALSE;
+        }
+        else {
+          $links = [];
+          if ($revert_permission) {
+            $links['revert'] = [
+              'title' => $this->t('Revert'),
+              'url' => Url::fromRoute('entity.heartbeat_stream.revision_revert', ['heartbeat_stream' => $heartbeat_stream->id(), 'heartbeat_stream_revision' => $vid]),
+            ];
+          }
+
+          if ($delete_permission) {
+            $links['delete'] = [
+              'title' => $this->t('Delete'),
+              'url' => Url::fromRoute('entity.heartbeat_stream.revision_delete', ['heartbeat_stream' => $heartbeat_stream->id(), 'heartbeat_stream_revision' => $vid]),
+            ];
+          }
+
+          $row[] = [
+            'data' => [
+              '#type' => 'operations',
+              '#links' => $links,
+            ],
+          ];
+        }
+
+        $rows[] = $row;
+      }
+    }
+
+    $build['heartbeat_stream_revisions_table'] = array(
+      '#theme' => 'table',
+      '#rows' => $rows,
+      '#header' => $header,
+    );
+
+    return $build;
+  }
+
+}

+ 190 - 168
src/Entity/HeartbeatStream.php

@@ -2,263 +2,285 @@
 
 namespace Drupal\heartbeat8\Entity;
 
-use Drupal\Core\Config\Entity\ConfigEntityBase;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Field\BaseFieldDefinition;
+use Drupal\Core\Entity\RevisionableContentEntityBase;
+use Drupal\Core\Entity\EntityChangedTrait;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\user\UserInterface;
 
 /**
  * Defines the Heartbeat stream entity.
  *
- * @ConfigEntityType(
+ * @ingroup heartbeat8
+ *
+ * @ContentEntityType(
  *   id = "heartbeat_stream",
  *   label = @Translation("Heartbeat stream"),
  *   handlers = {
+ *     "storage" = "Drupal\heartbeat8\HeartbeatStreamStorage",
+ *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
  *     "list_builder" = "Drupal\heartbeat8\HeartbeatStreamListBuilder",
+ *     "views_data" = "Drupal\heartbeat8\Entity\HeartbeatStreamViewsData",
+ *
  *     "form" = {
+ *       "default" = "Drupal\heartbeat8\Form\HeartbeatStreamForm",
  *       "add" = "Drupal\heartbeat8\Form\HeartbeatStreamForm",
  *       "edit" = "Drupal\heartbeat8\Form\HeartbeatStreamForm",
- *       "delete" = "Drupal\heartbeat8\Form\HeartbeatStreamDeleteForm"
+ *       "delete" = "Drupal\heartbeat8\Form\HeartbeatStreamDeleteForm",
  *     },
+ *     "access" = "Drupal\heartbeat8\HeartbeatStreamAccessControlHandler",
  *     "route_provider" = {
  *       "html" = "Drupal\heartbeat8\HeartbeatStreamHtmlRouteProvider",
  *     },
  *   },
- *   config_prefix = "heartbeat_stream",
- *   admin_permission = "administer site configuration",
+ *   base_table = "heartbeat_stream",
+ *   revision_table = "heartbeat_stream_revision",
+ *   revision_data_table = "heartbeat_stream_field_revision",
+ *   admin_permission = "administer heartbeat stream entities",
  *   entity_keys = {
  *     "id" = "id",
- *     "label" = "label",
- *     "uuid" = "uuid"
+ *     "revision" = "vid",
+ *     "label" = "name",
+ *     "uuid" = "uuid",
+ *     "uid" = "user_id",
+ *     "langcode" = "langcode",
+ *     "status" = "status",
  *   },
  *   links = {
- *     "canonical" = "/admin/structure/heartbeat_stream/{heartbeat_stream}",
- *     "add-form" = "/admin/structure/heartbeat_stream/add",
- *     "edit-form" = "/admin/structure/heartbeat_stream/{heartbeat_stream}/edit",
- *     "delete-form" = "/admin/structure/heartbeat_stream/{heartbeat_stream}/delete",
- *     "collection" = "/admin/structure/heartbeat_stream"
- *   }
+ *     "canonical" = "/admin/structure/heartbeatstream/heartbeat_stream/{heartbeat_stream}",
+ *     "add-form" = "/admin/structure/heartbeatstream/heartbeat_stream/add",
+ *     "edit-form" = "/admin/structure/heartbeatstream/heartbeat_stream/{heartbeat_stream}/edit",
+ *     "delete-form" = "/admin/structure/heartbeatstream/heartbeat_stream/{heartbeat_stream}/delete",
+ *     "version-history" = "/admin/structure/heartbeatstream/heartbeat_stream/{heartbeat_stream}/revisions",
+ *     "revision" = "/admin/structure/heartbeatstream/heartbeat_stream/{heartbeat_stream}/revisions/{heartbeat_stream_revision}/view",
+ *     "revision_delete" = "/admin/structure/heartbeatstream/heartbeat_stream/{heartbeat_stream}/revisions/{heartbeat_stream_revision}/delete",
+ *     "collection" = "/admin/structure/heartbeatstream/heartbeat_stream",
+ *   },
+ *   field_ui_base_route = "heartbeat_stream.settings"
  * )
  */
-class HeartbeatStream extends ConfigEntityBase implements HeartbeatStreamInterface {
+class HeartbeatStream extends RevisionableContentEntityBase implements HeartbeatStreamInterface {
 
-  /**
-   * The Heartbeat stream ID.
-   *
-   * @var string
-   */
-  protected $id;
+  use EntityChangedTrait;
 
   /**
-   * The Heartbeat stream label.
-   *
-   * @var string
+   * {@inheritdoc}
    */
-  protected $label;
-
-
-  // Class name used.
-  protected $name;
-
-  // Class to variable for ease of read/write.
-  protected $class;
-
-  // Real class to load for cloned streams.
-  protected $real_class;
-
-  // The path to the class.
-  protected $path;
-
-  // Human readable name.
-  protected $title;
-
-  // Module where query builder is located.
-  protected $module;
-
-  // Extra variables.
-  //TODO variables might be put into config api
-  protected $variables;
-
-  // Indicates whether this stream has a block display or not.
-  protected $has_block = TRUE;
-
-  // Max number of items in block display.
-  protected $block_items_max = 25;
-
-  // Number to indicate how a block-pager should be shown.
-  protected $block_show_pager = 0;
-
-  // View mode for the block.
-  protected $block_view_mode = 'default';
-
-  // Maximum number of items in the page display.
-  protected $page_items_max = 50;
-
-  // Boolean to indicate of a page-pager should be shown.
-  protected $page_show_pager = 0;
-
-  // Boolean to indicate if the pager is ajax-driven.
-  protected $page_pager_ajax = 0;
-
-  // View mode for the page.
-  protected $page_view_mode = 'default';
-
-  // Setting for the number of grouped items maximum.
-  protected $show_message_times = 1;
-
-  // Setting for the number of grouped items maximum in a grouped message.
-  protected $show_message_times_grouped = 0;
-
-  // Denied message templates.
-  protected $messages_denied = array();
-
-  // Limit the number of messages by maximum messages to load.
-  protected $num_load_max = 100;
-
-  // Limit the timespan to group messages.
-  protected $grouping_seconds = 7200;
-
-  // Boolean for to skip the viewing user, defaults to false.
-  protected $skip_active_user = FALSE;
-
-  // Timestamp used to poll for newer messages.
-  protected $poll_messages = 0;
-
-  // How to notify there are newer messages.
-  protected $poll_messages_type = 0;
-
-  // Stream path is the path to the stream page (optional).
-  protected $stream_path;
-
-  // Stream user path is the path to a stream on the profile page (optional).
-  protected $stream_profile_path;
-
-  // Settings variable
-  protected $settings;
+  public static function preCreate(EntityStorageInterface $storage_controller, array &$values) {
+    parent::preCreate($storage_controller, $values);
+    $values += array(
+      'user_id' => \Drupal::currentUser()->id(),
+    );
+  }
 
   /**
-   * @return mixed
+   * {@inheritdoc}
    */
-  public function getName()
-  {
-    return $this->name;
+  public function preSave(EntityStorageInterface $storage) {
+    parent::preSave($storage);
+
+    foreach (array_keys($this->getTranslationLanguages()) as $langcode) {
+      $translation = $this->getTranslation($langcode);
+
+      // If no owner has been set explicitly, make the anonymous user the owner.
+      if (!$translation->getOwner()) {
+        $translation->setOwnerId(0);
+      }
+    }
+
+    // If no revision author has been set explicitly, make the heartbeat_stream owner the
+    // revision author.
+    if (!$this->getRevisionUser()) {
+      $this->setRevisionUserId($this->getOwnerId());
+    }
   }
 
   /**
-   * @param mixed $name
+   * {@inheritdoc}
    */
-  public function setName($name)
-  {
-    $this->name = $name;
+  public function getName() {
+    return $this->get('name')->value;
   }
 
   /**
-   * @return mixed
+   * {@inheritdoc}
    */
-  public function getClass()
-  {
-    return $this->class;
+  public function setName($name) {
+    $this->set('name', $name);
+    return $this;
   }
 
   /**
-   * @param mixed $class
+   * {@inheritdoc}
    */
-  public function setClass($class)
-  {
-    $this->class = $class;
+  public function getCreatedTime() {
+    return $this->get('created')->value;
   }
 
   /**
-   * @return mixed
+   * {@inheritdoc}
    */
-  public function getRealClass()
-  {
-    return $this->real_class;
+  public function setCreatedTime($timestamp) {
+    $this->set('created', $timestamp);
+    return $this;
   }
 
   /**
-   * @param mixed $real_class
+   * {@inheritdoc}
    */
-  public function setRealClass($real_class)
-  {
-    $this->real_class = $real_class;
+  public function getOwner() {
+    return $this->get('user_id')->entity;
   }
 
   /**
-   * @return mixed
+   * {@inheritdoc}
    */
-  public function getPath()
-  {
-    return $this->path;
+  public function getOwnerId() {
+    return $this->get('user_id')->target_id;
   }
 
   /**
-   * @param mixed $path
+   * {@inheritdoc}
    */
-  public function setPath($path)
-  {
-    $this->path = $path;
+  public function setOwnerId($uid) {
+    $this->set('user_id', $uid);
+    return $this;
   }
 
   /**
-   * @return mixed
+   * {@inheritdoc}
    */
-  public function getTitle()
-  {
-    return $this->title;
+  public function setOwner(UserInterface $account) {
+    $this->set('user_id', $account->id());
+    return $this;
   }
 
   /**
-   * @param mixed $title
+   * {@inheritdoc}
    */
-  public function setTitle($title)
-  {
-    $this->title = $title;
+  public function isPublished() {
+    return (bool) $this->getEntityKey('status');
   }
 
   /**
-   * @return mixed
+   * {@inheritdoc}
    */
-  public function getModule()
-  {
-    return $this->module;
+  public function setPublished($published) {
+    $this->set('status', $published ? TRUE : FALSE);
+    return $this;
   }
 
   /**
-   * @param mixed $module
+   * {@inheritdoc}
    */
-  public function setModule($module)
-  {
-    $this->module = $module;
+  public function getRevisionCreationTime() {
+    return $this->get('revision_timestamp')->value;
   }
 
   /**
-   * @return mixed
+   * {@inheritdoc}
    */
-  public function getVariables()
-  {
-    return $this->variables;
+  public function setRevisionCreationTime($timestamp) {
+    $this->set('revision_timestamp', $timestamp);
+    return $this;
   }
 
   /**
-   * @param mixed $variables
+   * {@inheritdoc}
    */
-  public function setVariables($variables)
-  {
-    $this->variables = $variables;
+  public function getRevisionUser() {
+    return $this->get('revision_uid')->entity;
   }
 
   /**
-   * @return mixed
+   * {@inheritdoc}
    */
-  public function getSettings()
-  {
-    return $this->settings;
+  public function setRevisionUserId($uid) {
+    $this->set('revision_uid', $uid);
+    return $this;
   }
 
   /**
-   * @param mixed $settings
+   * {@inheritdoc}
    */
-  public function setSettings($settings)
-  {
-    $this->settings = $settings;
+  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+    $fields = parent::baseFieldDefinitions($entity_type);
+
+    $fields['user_id'] = BaseFieldDefinition::create('entity_reference')
+      ->setLabel(t('Authored by'))
+      ->setDescription(t('The user ID of author of the Heartbeat stream entity.'))
+      ->setRevisionable(TRUE)
+      ->setSetting('target_type', 'user')
+      ->setSetting('handler', 'default')
+      ->setTranslatable(TRUE)
+      ->setDisplayOptions('view', array(
+        'label' => 'hidden',
+        'type' => 'author',
+        'weight' => 0,
+      ))
+      ->setDisplayOptions('form', array(
+        'type' => 'entity_reference_autocomplete',
+        'weight' => 5,
+        'settings' => array(
+          'match_operator' => 'CONTAINS',
+          'size' => '60',
+          'autocomplete_type' => 'tags',
+          'placeholder' => '',
+        ),
+      ))
+      ->setDisplayConfigurable('form', TRUE)
+      ->setDisplayConfigurable('view', TRUE);
+
+    $fields['name'] = BaseFieldDefinition::create('string')
+      ->setLabel(t('Name'))
+      ->setDescription(t('The name of the Heartbeat stream entity.'))
+      ->setRevisionable(TRUE)
+      ->setSettings(array(
+        'max_length' => 50,
+        'text_processing' => 0,
+      ))
+      ->setDefaultValue('')
+      ->setDisplayOptions('view', array(
+        'label' => 'above',
+        'type' => 'string',
+        'weight' => -4,
+      ))
+      ->setDisplayOptions('form', array(
+        'type' => 'string_textfield',
+        'weight' => -4,
+      ))
+      ->setDisplayConfigurable('form', TRUE)
+      ->setDisplayConfigurable('view', TRUE);
+
+    $fields['status'] = BaseFieldDefinition::create('boolean')
+      ->setLabel(t('Publishing status'))
+      ->setDescription(t('A boolean indicating whether the Heartbeat stream is published.'))
+      ->setRevisionable(TRUE)
+      ->setDefaultValue(TRUE);
+
+    $fields['created'] = BaseFieldDefinition::create('created')
+      ->setLabel(t('Created'))
+      ->setDescription(t('The time that the entity was created.'));
+
+    $fields['changed'] = BaseFieldDefinition::create('changed')
+      ->setLabel(t('Changed'))
+      ->setDescription(t('The time that the entity was last edited.'));
+
+    $fields['revision_timestamp'] = BaseFieldDefinition::create('created')
+      ->setLabel(t('Revision timestamp'))
+      ->setDescription(t('The time that the current revision was created.'))
+      ->setQueryable(FALSE)
+      ->setRevisionable(TRUE);
+
+    $fields['revision_uid'] = BaseFieldDefinition::create('entity_reference')
+      ->setLabel(t('Revision user ID'))
+      ->setDescription(t('The user ID of the author of the current revision.'))
+      ->setSetting('target_type', 'user')
+      ->setQueryable(FALSE)
+      ->setRevisionable(TRUE);
+
+    return $fields;
   }
 
 }

+ 264 - 0
src/Entity/HeartbeatStream.php.bak

@@ -0,0 +1,264 @@
+<?php
+
+namespace Drupal\heartbeat8\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityBase;
+
+/**
+ * Defines the Heartbeat stream entity.
+ *
+ * @ConfigEntityType(
+ *   id = "heartbeat_stream",
+ *   label = @Translation("Heartbeat stream"),
+ *   handlers = {
+ *     "list_builder" = "Drupal\heartbeat8\HeartbeatStreamListBuilder",
+ *     "form" = {
+ *       "add" = "Drupal\heartbeat8\Form\HeartbeatStreamForm",
+ *       "edit" = "Drupal\heartbeat8\Form\HeartbeatStreamForm",
+ *       "delete" = "Drupal\heartbeat8\Form\HeartbeatStreamDeleteForm"
+ *     },
+ *     "route_provider" = {
+ *       "html" = "Drupal\heartbeat8\HeartbeatStreamHtmlRouteProvider",
+ *     },
+ *   },
+ *   config_prefix = "heartbeat_stream",
+ *   admin_permission = "administer site configuration",
+ *   entity_keys = {
+ *     "id" = "id",
+ *     "label" = "label",
+ *     "uuid" = "uuid"
+ *   },
+ *   links = {
+ *     "canonical" = "/admin/structure/heartbeat_stream/{heartbeat_stream}",
+ *     "add-form" = "/admin/structure/heartbeat_stream/add",
+ *     "edit-form" = "/admin/structure/heartbeat_stream/{heartbeat_stream}/edit",
+ *     "delete-form" = "/admin/structure/heartbeat_stream/{heartbeat_stream}/delete",
+ *     "collection" = "/admin/structure/heartbeat_stream"
+ *   }
+ * )
+ */
+class HeartbeatStream extends ConfigEntityBase implements HeartbeatStreamInterface {
+
+  /**
+   * The Heartbeat stream ID.
+   *
+   * @var string
+   */
+  protected $id;
+
+  /**
+   * The Heartbeat stream label.
+   *
+   * @var string
+   */
+  protected $label;
+
+
+  // Class name used.
+  protected $name;
+
+  // Class to variable for ease of read/write.
+  protected $class;
+
+  // Real class to load for cloned streams.
+  protected $real_class;
+
+  // The path to the class.
+  protected $path;
+
+  // Human readable name.
+  protected $title;
+
+  // Module where query builder is located.
+  protected $module;
+
+  // Extra variables.
+  //TODO variables might be put into config api
+  protected $variables;
+
+  // Indicates whether this stream has a block display or not.
+  protected $has_block = TRUE;
+
+  // Max number of items in block display.
+  protected $block_items_max = 25;
+
+  // Number to indicate how a block-pager should be shown.
+  protected $block_show_pager = 0;
+
+  // View mode for the block.
+  protected $block_view_mode = 'default';
+
+  // Maximum number of items in the page display.
+  protected $page_items_max = 50;
+
+  // Boolean to indicate of a page-pager should be shown.
+  protected $page_show_pager = 0;
+
+  // Boolean to indicate if the pager is ajax-driven.
+  protected $page_pager_ajax = 0;
+
+  // View mode for the page.
+  protected $page_view_mode = 'default';
+
+  // Setting for the number of grouped items maximum.
+  protected $show_message_times = 1;
+
+  // Setting for the number of grouped items maximum in a grouped message.
+  protected $show_message_times_grouped = 0;
+
+  // Denied message templates.
+  protected $messages_denied = array();
+
+  // Limit the number of messages by maximum messages to load.
+  protected $num_load_max = 100;
+
+  // Limit the timespan to group messages.
+  protected $grouping_seconds = 7200;
+
+  // Boolean for to skip the viewing user, defaults to false.
+  protected $skip_active_user = FALSE;
+
+  // Timestamp used to poll for newer messages.
+  protected $poll_messages = 0;
+
+  // How to notify there are newer messages.
+  protected $poll_messages_type = 0;
+
+  // Stream path is the path to the stream page (optional).
+  protected $stream_path;
+
+  // Stream user path is the path to a stream on the profile page (optional).
+  protected $stream_profile_path;
+
+  // Settings variable
+  protected $settings;
+
+  /**
+   * @return mixed
+   */
+  public function getName()
+  {
+    return $this->name;
+  }
+
+  /**
+   * @param mixed $name
+   */
+  public function setName($name)
+  {
+    $this->name = $name;
+  }
+
+  /**
+   * @return mixed
+   */
+  public function getClass()
+  {
+    return $this->class;
+  }
+
+  /**
+   * @param mixed $class
+   */
+  public function setClass($class)
+  {
+    $this->class = $class;
+  }
+
+  /**
+   * @return mixed
+   */
+  public function getRealClass()
+  {
+    return $this->real_class;
+  }
+
+  /**
+   * @param mixed $real_class
+   */
+  public function setRealClass($real_class)
+  {
+    $this->real_class = $real_class;
+  }
+
+  /**
+   * @return mixed
+   */
+  public function getPath()
+  {
+    return $this->path;
+  }
+
+  /**
+   * @param mixed $path
+   */
+  public function setPath($path)
+  {
+    $this->path = $path;
+  }
+
+  /**
+   * @return mixed
+   */
+  public function getTitle()
+  {
+    return $this->title;
+  }
+
+  /**
+   * @param mixed $title
+   */
+  public function setTitle($title)
+  {
+    $this->title = $title;
+  }
+
+  /**
+   * @return mixed
+   */
+  public function getModule()
+  {
+    return $this->module;
+  }
+
+  /**
+   * @param mixed $module
+   */
+  public function setModule($module)
+  {
+    $this->module = $module;
+  }
+
+  /**
+   * @return mixed
+   */
+  public function getVariables()
+  {
+    return $this->variables;
+  }
+
+  /**
+   * @param mixed $variables
+   */
+  public function setVariables($variables)
+  {
+    $this->variables = $variables;
+  }
+
+  /**
+   * @return mixed
+   */
+  public function getSettings()
+  {
+    return $this->settings;
+  }
+
+  /**
+   * @param mixed $settings
+   */
+  public function setSettings($settings)
+  {
+    $this->settings = $settings;
+  }
+
+}

+ 76 - 66
src/Entity/HeartbeatStreamInterface.php

@@ -2,107 +2,117 @@
 
 namespace Drupal\heartbeat8\Entity;
 
-use Drupal\Core\Config\Entity\ConfigEntityInterface;
+use Drupal\Core\Entity\RevisionLogInterface;
+use Drupal\Core\Entity\RevisionableInterface;
+use Drupal\Component\Utility\Xss;
+use Drupal\Core\Url;
+use Drupal\Core\Entity\EntityChangedInterface;
+use Drupal\user\EntityOwnerInterface;
 
 /**
  * Provides an interface for defining Heartbeat stream entities.
+ *
+ * @ingroup heartbeat8
  */
-interface HeartbeatStreamInterface extends ConfigEntityInterface {
+interface HeartbeatStreamInterface extends RevisionableInterface, RevisionLogInterface, EntityChangedInterface, EntityOwnerInterface {
+
+  // Add get/set methods for your configuration properties here.
 
   /**
-   * @return mixed
+   * Gets the Heartbeat stream name.
+   *
+   * @return string
+   *   Name of the Heartbeat stream.
    */
-
   public function getName();
 
   /**
-   * @param mixed $name
+   * Sets the Heartbeat stream name.
+   *
+   * @param string $name
+   *   The Heartbeat stream name.
+   *
+   * @return \Drupal\heartbeat8\Entity\HeartbeatStreamInterface
+   *   The called Heartbeat stream entity.
    */
-
   public function setName($name);
 
   /**
-   * @return mixed
-   */
-
-  public function getClass();
-
-  /**
-   * @param mixed $class
-   */
-
-  public function setClass($class);
-
-  /**
-   * @return mixed
+   * Gets the Heartbeat stream creation timestamp.
+   *
+   * @return int
+   *   Creation timestamp of the Heartbeat stream.
    */
-
-  public function getRealClass();
+  public function getCreatedTime();
 
   /**
-   * @param mixed $real_class
+   * Sets the Heartbeat stream creation timestamp.
+   *
+   * @param int $timestamp
+   *   The Heartbeat stream creation timestamp.
+   *
+   * @return \Drupal\heartbeat8\Entity\HeartbeatStreamInterface
+   *   The called Heartbeat stream entity.
    */
-
-  public function setRealClass($real_class);
+  public function setCreatedTime($timestamp);
 
   /**
-   * @return mixed
+   * Returns the Heartbeat stream published status indicator.
+   *
+   * Unpublished Heartbeat stream are only visible to restricted users.
+   *
+   * @return bool
+   *   TRUE if the Heartbeat stream is published.
    */
-
-  public function getPath();
+  public function isPublished();
 
   /**
-   * @param mixed $path
+   * Sets the published status of a Heartbeat stream.
+   *
+   * @param bool $published
+   *   TRUE to set this Heartbeat stream to published, FALSE to set it to unpublished.
+   *
+   * @return \Drupal\heartbeat8\Entity\HeartbeatStreamInterface
+   *   The called Heartbeat stream entity.
    */
-
-  public function setPath($path);
+  public function setPublished($published);
 
   /**
-   * @return mixed
+   * Gets the Heartbeat stream revision creation timestamp.
+   *
+   * @return int
+   *   The UNIX timestamp of when this revision was created.
    */
-
-  public function getTitle();
+  public function getRevisionCreationTime();
 
   /**
-   * @param mixed $title
+   * Sets the Heartbeat stream revision creation timestamp.
+   *
+   * @param int $timestamp
+   *   The UNIX timestamp of when this revision was created.
+   *
+   * @return \Drupal\heartbeat8\Entity\HeartbeatStreamInterface
+   *   The called Heartbeat stream entity.
    */
-
-  public function setTitle($title);
+  public function setRevisionCreationTime($timestamp);
 
   /**
-   * @return mixed
+   * Gets the Heartbeat stream revision author.
+   *
+   * @return \Drupal\user\UserInterface
+   *   The user entity for the revision author.
    */
-
-  public function getModule();
+  public function getRevisionUser();
 
   /**
-   * @param mixed $module
+   * Sets the Heartbeat stream revision author.
+   *
+   * @param int $uid
+   *   The user ID of the revision author.
+   *
+   * @return \Drupal\heartbeat8\Entity\HeartbeatStreamInterface
+   *   The called Heartbeat stream entity.
    */
-
-  public function setModule($module);
-
-  /**
-   * @return mixed
-   */
-
-  public function getVariables();
-
-  /**
-   * @param mixed $variables
-   */
-
-  public function setVariables($variables);
-
-  /**
-   * @return mixed
-   */
-
-  public function getSettings();
-
-  /**
-   * @param mixed $settings
-   */
-
-  public function setSettings($settings);
+  public function setRevisionUserId($uid);
 
 }

+ 108 - 0
src/Entity/HeartbeatStreamInterface.php.bak

@@ -0,0 +1,108 @@
+<?php
+
+namespace Drupal\heartbeat8\Entity;
+
+use Drupal\Core\Config\Entity\ConfigEntityInterface;
+
+/**
+ * Provides an interface for defining Heartbeat stream entities.
+ */
+interface HeartbeatStreamInterface extends ConfigEntityInterface {
+
+  /**
+   * @return mixed
+   */
+
+  public function getName();
+
+  /**
+   * @param mixed $name
+   */
+
+  public function setName($name);
+
+  /**
+   * @return mixed
+   */
+
+  public function getClass();
+
+  /**
+   * @param mixed $class
+   */
+
+  public function setClass($class);
+
+  /**
+   * @return mixed
+   */
+
+  public function getRealClass();
+
+  /**
+   * @param mixed $real_class
+   */
+
+  public function setRealClass($real_class);
+
+  /**
+   * @return mixed
+   */
+
+  public function getPath();
+
+  /**
+   * @param mixed $path
+   */
+
+  public function setPath($path);
+
+  /**
+   * @return mixed
+   */
+
+  public function getTitle();
+
+  /**
+   * @param mixed $title
+   */
+
+  public function setTitle($title);
+
+  /**
+   * @return mixed
+   */
+
+  public function getModule();
+
+  /**
+   * @param mixed $module
+   */
+
+  public function setModule($module);
+
+  /**
+   * @return mixed
+   */
+
+  public function getVariables();
+
+  /**
+   * @param mixed $variables
+   */
+
+  public function setVariables($variables);
+
+  /**
+   * @return mixed
+   */
+
+  public function getSettings();
+
+  /**
+   * @param mixed $settings
+   */
+
+  public function setSettings($settings);
+
+}

+ 24 - 0
src/Entity/HeartbeatStreamViewsData.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace Drupal\heartbeat8\Entity;
+
+use Drupal\views\EntityViewsData;
+
+/**
+ * Provides Views data for Heartbeat stream entities.
+ */
+class HeartbeatStreamViewsData extends EntityViewsData {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getViewsData() {
+    $data = parent::getViewsData();
+
+    // Additional information for Views integration, such as table joins, can be
+    // put here.
+
+    return $data;
+  }
+
+}

+ 5 - 43
src/Form/HeartbeatStreamDeleteForm.php

@@ -2,52 +2,14 @@
 
 namespace Drupal\heartbeat8\Form;
 
-use Drupal\Core\Entity\EntityConfirmFormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Url;
+use Drupal\Core\Entity\ContentEntityDeleteForm;
 
 /**
- * Builds the form to delete Heartbeat stream entities.
+ * Provides a form for deleting Heartbeat stream entities.
+ *
+ * @ingroup heartbeat8
  */
-class HeartbeatStreamDeleteForm extends EntityConfirmFormBase {
+class HeartbeatStreamDeleteForm extends ContentEntityDeleteForm {
 
-  /**
-   * {@inheritdoc}
-   */
-  public function getQuestion() {
-    return $this->t('Are you sure you want to delete %name?', ['%name' => $this->entity->label()]);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getCancelUrl() {
-    return new Url('entity.heartbeat_stream.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('content @type: deleted @label.',
-        [
-          '@type' => $this->entity->bundle(),
-          '@label' => $this->entity->label(),
-        ]
-        )
-    );
-
-    $form_state->setRedirectUrl($this->getCancelUrl());
-  }
 
 }

+ 34 - 30
src/Form/HeartbeatStreamForm.php

@@ -2,42 +2,33 @@
 
 namespace Drupal\heartbeat8\Form;
 
-use Drupal\Core\Entity\EntityForm;
+use Drupal\Core\Entity\ContentEntityForm;
 use Drupal\Core\Form\FormStateInterface;
 
 /**
- * Class HeartbeatStreamForm.
+ * Form controller for Heartbeat stream edit forms.
  *
- * @package Drupal\heartbeat8\Form
+ * @ingroup heartbeat8
  */
-class HeartbeatStreamForm extends EntityForm {
+class HeartbeatStreamForm extends ContentEntityForm {
 
   /**
    * {@inheritdoc}
    */
-  public function form(array $form, FormStateInterface $form_state) {
-    $form = parent::form($form, $form_state);
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    /* @var $entity \Drupal\heartbeat8\Entity\HeartbeatStream */
+    $form = parent::buildForm($form, $form_state);
 
-    $heartbeat_stream = $this->entity;
-    $form['label'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Label'),
-      '#maxlength' => 255,
-      '#default_value' => $heartbeat_stream->label(),
-      '#description' => $this->t("Label for the Heartbeat stream."),
-      '#required' => TRUE,
-    ];
-
-    $form['id'] = [
-      '#type' => 'machine_name',
-      '#default_value' => $heartbeat_stream->id(),
-      '#machine_name' => [
-        'exists' => '\Drupal\heartbeat8\Entity\HeartbeatStream::load',
-      ],
-      '#disabled' => !$heartbeat_stream->isNew(),
-    ];
+    if (!$this->entity->isNew()) {
+      $form['new_revision'] = array(
+        '#type' => 'checkbox',
+        '#title' => $this->t('Create new revision'),
+        '#default_value' => FALSE,
+        '#weight' => 10,
+      );
+    }
 
-    /* You will need additional form elements for your custom properties. */
+    $entity = $this->entity;
 
     return $form;
   }
@@ -46,22 +37,35 @@ class HeartbeatStreamForm extends EntityForm {
    * {@inheritdoc}
    */
   public function save(array $form, FormStateInterface $form_state) {
-    $heartbeat_stream = $this->entity;
-    $status = $heartbeat_stream->save();
+    $entity = &$this->entity;
+
+    // Save as a new revision if requested to do so.
+    if (!$form_state->isValueEmpty('new_revision') && $form_state->getValue('new_revision') != FALSE) {
+      $entity->setNewRevision();
+
+      // If a new revision is created, save the current user as revision author.
+      $entity->setRevisionCreationTime(REQUEST_TIME);
+      $entity->setRevisionUserId(\Drupal::currentUser()->id());
+    }
+    else {
+      $entity->setNewRevision(FALSE);
+    }
+
+    $status = parent::save($form, $form_state);
 
     switch ($status) {
       case SAVED_NEW:
         drupal_set_message($this->t('Created the %label Heartbeat stream.', [
-          '%label' => $heartbeat_stream->label(),
+          '%label' => $entity->label(),
         ]));
         break;
 
       default:
         drupal_set_message($this->t('Saved the %label Heartbeat stream.', [
-          '%label' => $heartbeat_stream->label(),
+          '%label' => $entity->label(),
         ]));
     }
-    $form_state->setRedirectUrl($heartbeat_stream->toUrl('collection'));
+    $form_state->setRedirect('entity.heartbeat_stream.canonical', ['heartbeat_stream' => $entity->id()]);
   }
 
 }

+ 123 - 0
src/Form/HeartbeatStreamRevisionDeleteForm.php

@@ -0,0 +1,123 @@
+<?php
+
+namespace Drupal\heartbeat8\Form;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a form for deleting a Heartbeat stream revision.
+ *
+ * @ingroup heartbeat8
+ */
+class HeartbeatStreamRevisionDeleteForm extends ConfirmFormBase {
+
+
+  /**
+   * The Heartbeat stream revision.
+   *
+   * @var \Drupal\heartbeat8\Entity\HeartbeatStreamInterface
+   */
+  protected $revision;
+
+  /**
+   * The Heartbeat stream storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $HeartbeatStreamStorage;
+
+  /**
+   * The database connection.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $connection;
+
+  /**
+   * Constructs a new HeartbeatStreamRevisionDeleteForm.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage
+   *   The entity storage.
+   * @param \Drupal\Core\Database\Connection $connection
+   *   The database connection.
+   */
+  public function __construct(EntityStorageInterface $entity_storage, Connection $connection) {
+    $this->HeartbeatStreamStorage = $entity_storage;
+    $this->connection = $connection;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    $entity_manager = $container->get('entity.manager');
+    return new static(
+      $entity_manager->getStorage('heartbeat_stream'),
+      $container->get('database')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'heartbeat_stream_revision_delete_confirm';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return t('Are you sure you want to delete the revision from %revision-date?', array('%revision-date' => format_date($this->revision->getRevisionCreationTime())));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    return new Url('entity.heartbeat_stream.version_history', array('heartbeat_stream' => $this->revision->id()));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return t('Delete');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $heartbeat_stream_revision = NULL) {
+    $this->revision = $this->HeartbeatStreamStorage->loadRevision($heartbeat_stream_revision);
+    $form = parent::buildForm($form, $form_state);
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $this->HeartbeatStreamStorage->deleteRevision($this->revision->getRevisionId());
+
+    $this->logger('content')->notice('Heartbeat stream: deleted %title revision %revision.', array('%title' => $this->revision->label(), '%revision' => $this->revision->getRevisionId()));
+    drupal_set_message(t('Revision from %revision-date of Heartbeat stream %title has been deleted.', array('%revision-date' => format_date($this->revision->getRevisionCreationTime()), '%title' => $this->revision->label())));
+    $form_state->setRedirect(
+      'entity.heartbeat_stream.canonical',
+       array('heartbeat_stream' => $this->revision->id())
+    );
+    if ($this->connection->query('SELECT COUNT(DISTINCT vid) FROM {heartbeat_stream_field_revision} WHERE id = :id', array(':id' => $this->revision->id()))->fetchField() > 1) {
+      $form_state->setRedirect(
+        'entity.heartbeat_stream.version_history',
+         array('heartbeat_stream' => $this->revision->id())
+      );
+    }
+  }
+
+}

+ 149 - 0
src/Form/HeartbeatStreamRevisionRevertForm.php

@@ -0,0 +1,149 @@
+<?php
+
+namespace Drupal\heartbeat8\Form;
+
+use Drupal\Core\Datetime\DateFormatterInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\heartbeat8\Entity\HeartbeatStreamInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a form for reverting a Heartbeat stream revision.
+ *
+ * @ingroup heartbeat8
+ */
+class HeartbeatStreamRevisionRevertForm extends ConfirmFormBase {
+
+
+  /**
+   * The Heartbeat stream revision.
+   *
+   * @var \Drupal\heartbeat8\Entity\HeartbeatStreamInterface
+   */
+  protected $revision;
+
+  /**
+   * The Heartbeat stream storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $HeartbeatStreamStorage;
+
+  /**
+   * The date formatter service.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatterInterface
+   */
+  protected $dateFormatter;
+
+  /**
+   * Constructs a new HeartbeatStreamRevisionRevertForm.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage
+   *   The Heartbeat stream storage.
+   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
+   *   The date formatter service.
+   */
+  public function __construct(EntityStorageInterface $entity_storage, DateFormatterInterface $date_formatter) {
+    $this->HeartbeatStreamStorage = $entity_storage;
+    $this->dateFormatter = $date_formatter;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity.manager')->getStorage('heartbeat_stream'),
+      $container->get('date.formatter')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'heartbeat_stream_revision_revert_confirm';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return t('Are you sure you want to revert to the revision from %revision-date?', ['%revision-date' => $this->dateFormatter->format($this->revision->getRevisionCreationTime())]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCancelUrl() {
+    return new Url('entity.heartbeat_stream.version_history', array('heartbeat_stream' => $this->revision->id()));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfirmText() {
+    return t('Revert');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    return '';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $heartbeat_stream_revision = NULL) {
+    $this->revision = $this->HeartbeatStreamStorage->loadRevision($heartbeat_stream_revision);
+    $form = parent::buildForm($form, $form_state);
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    // The revision timestamp will be updated when the revision is saved. Keep
+    // the original one for the confirmation message.
+    $original_revision_timestamp = $this->revision->getRevisionCreationTime();
+
+    $this->revision = $this->prepareRevertedRevision($this->revision, $form_state);
+    $this->revision->revision_log = t('Copy of the revision from %date.', ['%date' => $this->dateFormatter->format($original_revision_timestamp)]);
+    $this->revision->save();
+
+    $this->logger('content')->notice('Heartbeat stream: reverted %title revision %revision.', ['%title' => $this->revision->label(), '%revision' => $this->revision->getRevisionId()]);
+    drupal_set_message(t('Heartbeat stream %title has been reverted to the revision from %revision-date.', ['%title' => $this->revision->label(), '%revision-date' => $this->dateFormatter->format($original_revision_timestamp)]));
+    $form_state->setRedirect(
+      'entity.heartbeat_stream.version_history',
+      array('heartbeat_stream' => $this->revision->id())
+    );
+  }
+
+  /**
+   * Prepares a revision to be reverted.
+   *
+   * @param \Drupal\heartbeat8\Entity\HeartbeatStreamInterface $revision
+   *   The revision to be reverted.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return \Drupal\heartbeat8\Entity\HeartbeatStreamInterface
+   *   The prepared revision ready to be stored.
+   */
+  protected function prepareRevertedRevision(HeartbeatStreamInterface $revision, FormStateInterface $form_state) {
+    $revision->setNewRevision();
+    $revision->isDefaultRevision(TRUE);
+    $revision->setRevisionCreationTime(REQUEST_TIME);
+
+    return $revision;
+  }
+
+}

+ 115 - 0
src/Form/HeartbeatStreamRevisionRevertTranslationForm.php

@@ -0,0 +1,115 @@
+<?php
+
+namespace Drupal\heartbeat8\Form;
+
+use Drupal\Core\Datetime\DateFormatterInterface;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Language\LanguageManagerInterface;
+use Drupal\heartbeat8\Entity\HeartbeatStreamInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a form for reverting a Heartbeat stream revision for a single translation.
+ *
+ * @ingroup heartbeat8
+ */
+class HeartbeatStreamRevisionRevertTranslationForm extends HeartbeatStreamRevisionRevertForm {
+
+
+  /**
+   * The language to be reverted.
+   *
+   * @var string
+   */
+  protected $langcode;
+
+  /**
+   * The language manager.
+   *
+   * @var \Drupal\Core\Language\LanguageManagerInterface
+   */
+  protected $languageManager;
+
+  /**
+   * Constructs a new HeartbeatStreamRevisionRevertTranslationForm.
+   *
+   * @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage
+   *   The Heartbeat stream storage.
+   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
+   *   The date formatter service.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The language manager.
+   */
+  public function __construct(EntityStorageInterface $entity_storage, DateFormatterInterface $date_formatter, LanguageManagerInterface $language_manager) {
+    parent::__construct($entity_storage, $date_formatter);
+    $this->languageManager = $language_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity.manager')->getStorage('heartbeat_stream'),
+      $container->get('date.formatter'),
+      $container->get('language_manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'heartbeat_stream_revision_revert_translation_confirm';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getQuestion() {
+    return t('Are you sure you want to revert @language translation to the revision from %revision-date?', ['@language' => $this->languageManager->getLanguageName($this->langcode), '%revision-date' => $this->dateFormatter->format($this->revision->getRevisionCreationTime())]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state, $heartbeat_stream_revision = NULL, $langcode = NULL) {
+    $this->langcode = $langcode;
+    $form = parent::buildForm($form, $form_state, $heartbeat_stream_revision);
+
+    $form['revert_untranslated_fields'] = array(
+      '#type' => 'checkbox',
+      '#title' => $this->t('Revert content shared among translations'),
+      '#default_value' => FALSE,
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function prepareRevertedRevision(HeartbeatStreamInterface $revision, FormStateInterface $form_state) {
+    $revert_untranslated_fields = $form_state->getValue('revert_untranslated_fields');
+
+    /** @var \Drupal\heartbeat8\Entity\HeartbeatStreamInterface $default_revision */
+    $latest_revision = $this->HeartbeatStreamStorage->load($revision->id());
+    $latest_revision_translation = $latest_revision->getTranslation($this->langcode);
+
+    $revision_translation = $revision->getTranslation($this->langcode);
+
+    foreach ($latest_revision_translation->getFieldDefinitions() as $field_name => $definition) {
+      if ($definition->isTranslatable() || $revert_untranslated_fields) {
+        $latest_revision_translation->set($field_name, $revision_translation->get($field_name)->getValue());
+      }
+    }
+
+    $latest_revision_translation->setNewRevision();
+    $latest_revision_translation->isDefaultRevision(TRUE);
+    $revision->setRevisionCreationTime(REQUEST_TIME);
+
+    return $latest_revision_translation;
+  }
+
+}

+ 55 - 0
src/Form/HeartbeatStreamSettingsForm.php

@@ -0,0 +1,55 @@
+<?php
+
+namespace Drupal\heartbeat8\Form;
+
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Class HeartbeatStreamSettingsForm.
+ *
+ * @package Drupal\heartbeat8\Form
+ *
+ * @ingroup heartbeat8
+ */
+class HeartbeatStreamSettingsForm extends FormBase {
+
+  /**
+   * Returns a unique string identifying the form.
+   *
+   * @return string
+   *   The unique string identifying the form.
+   */
+  public function getFormId() {
+    return 'HeartbeatStream_settings';
+  }
+
+  /**
+   * Form submission handler.
+   *
+   * @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.
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    // Empty implementation of the abstract submit class.
+  }
+
+  /**
+   * Defines the settings form for Heartbeat stream entities.
+   *
+   * @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.
+   *
+   * @return array
+   *   Form definition array.
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['HeartbeatStream_settings']['#markup'] = 'Settings form for Heartbeat stream entities. Manage field settings here.';
+    return $form;
+  }
+
+}

+ 47 - 0
src/HeartbeatStreamAccessControlHandler.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace Drupal\heartbeat8;
+
+use Drupal\Core\Entity\EntityAccessControlHandler;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Access\AccessResult;
+
+/**
+ * Access controller for the Heartbeat stream entity.
+ *
+ * @see \Drupal\heartbeat8\Entity\HeartbeatStream.
+ */
+class HeartbeatStreamAccessControlHandler extends EntityAccessControlHandler {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+    /** @var \Drupal\heartbeat8\Entity\HeartbeatStreamInterface $entity */
+    switch ($operation) {
+      case 'view':
+        if (!$entity->isPublished()) {
+          return AccessResult::allowedIfHasPermission($account, 'view unpublished heartbeat stream entities');
+        }
+        return AccessResult::allowedIfHasPermission($account, 'view published heartbeat stream entities');
+
+      case 'update':
+        return AccessResult::allowedIfHasPermission($account, 'edit heartbeat stream entities');
+
+      case 'delete':
+        return AccessResult::allowedIfHasPermission($account, 'delete heartbeat stream entities');
+    }
+
+    // Unknown operation, no opinion.
+    return AccessResult::neutral();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
+    return AccessResult::allowedIfHasPermission($account, 'add heartbeat stream entities');
+  }
+
+}

+ 141 - 3
src/HeartbeatStreamHtmlRouteProvider.php

@@ -26,6 +26,26 @@ class HeartbeatStreamHtmlRouteProvider extends AdminHtmlRouteProvider {
       $collection->add("entity.{$entity_type_id}.collection", $collection_route);
     }
 
+    if ($history_route = $this->getHistoryRoute($entity_type)) {
+      $collection->add("entity.{$entity_type_id}.version_history", $history_route);
+    }
+
+    if ($revision_route = $this->getRevisionRoute($entity_type)) {
+      $collection->add("entity.{$entity_type_id}.revision", $revision_route);
+    }
+
+    if ($revert_route = $this->getRevisionRevertRoute($entity_type)) {
+      $collection->add("entity.{$entity_type_id}.revision_revert", $revert_route);
+    }
+
+    if ($delete_route = $this->getRevisionDeleteRoute($entity_type)) {
+      $collection->add("entity.{$entity_type_id}.revision_delete", $delete_route);
+    }
+
+    if ($settings_form_route = $this->getSettingsFormRoute($entity_type)) {
+      $collection->add("$entity_type_id.settings", $settings_form_route);
+    }
+
     return $collection;
   }
 
@@ -45,9 +65,127 @@ class HeartbeatStreamHtmlRouteProvider extends AdminHtmlRouteProvider {
       $route
         ->setDefaults([
           '_entity_list' => $entity_type_id,
-          // Make sure this is not a TranslatableMarkup object as the
-          // TitleResolver translates this string again.
-          '_title' => (string) $entity_type->getLabel(),
+          '_title' => "{$entity_type->getLabel()} list",
+        ])
+        ->setRequirement('_permission', 'access heartbeat stream overview')
+        ->setOption('_admin_route', TRUE);
+
+      return $route;
+    }
+  }
+
+  /**
+   * Gets the version history route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getHistoryRoute(EntityTypeInterface $entity_type) {
+    if ($entity_type->hasLinkTemplate('version-history')) {
+      $route = new Route($entity_type->getLinkTemplate('version-history'));
+      $route
+        ->setDefaults([
+          '_title' => "{$entity_type->getLabel()} revisions",
+          '_controller' => '\Drupal\heartbeat8\Controller\HeartbeatStreamController::revisionOverview',
+        ])
+        ->setRequirement('_permission', 'access heartbeat stream revisions')
+        ->setOption('_admin_route', TRUE);
+
+      return $route;
+    }
+  }
+
+  /**
+   * Gets the revision route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getRevisionRoute(EntityTypeInterface $entity_type) {
+    if ($entity_type->hasLinkTemplate('revision')) {
+      $route = new Route($entity_type->getLinkTemplate('revision'));
+      $route
+        ->setDefaults([
+          '_controller' => '\Drupal\heartbeat8\Controller\HeartbeatStreamController::revisionShow',
+          '_title_callback' => '\Drupal\heartbeat8\Controller\HeartbeatStreamController::revisionPageTitle',
+        ])
+        ->setRequirement('_permission', 'access heartbeat stream revisions')
+        ->setOption('_admin_route', TRUE);
+
+      return $route;
+    }
+  }
+
+  /**
+   * Gets the revision revert route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getRevisionRevertRoute(EntityTypeInterface $entity_type) {
+    if ($entity_type->hasLinkTemplate('revision_revert')) {
+      $route = new Route($entity_type->getLinkTemplate('revision_revert'));
+      $route
+        ->setDefaults([
+          '_form' => '\Drupal\heartbeat8\Form\HeartbeatStreamRevisionRevertForm',
+          '_title' => 'Revert to earlier revision',
+        ])
+        ->setRequirement('_permission', 'revert all heartbeat stream revisions')
+        ->setOption('_admin_route', TRUE);
+
+      return $route;
+    }
+  }
+
+  /**
+   * Gets the revision delete route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getRevisionDeleteRoute(EntityTypeInterface $entity_type) {
+    if ($entity_type->hasLinkTemplate('revision_delete')) {
+      $route = new Route($entity_type->getLinkTemplate('revision_delete'));
+      $route
+        ->setDefaults([
+          '_form' => '\Drupal\heartbeat8\Form\HeartbeatStreamRevisionDeleteForm',
+          '_title' => 'Delete earlier revision',
+        ])
+        ->setRequirement('_permission', 'delete all heartbeat stream revisions')
+        ->setOption('_admin_route', TRUE);
+
+      return $route;
+    }
+  }
+
+  /**
+   * Gets the settings form route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getSettingsFormRoute(EntityTypeInterface $entity_type) {
+    if (!$entity_type->getBundleEntityType()) {
+      $route = new Route("/admin/structure/{$entity_type->id()}/settings");
+      $route
+        ->setDefaults([
+          '_form' => 'Drupal\heartbeat8\Form\HeartbeatStreamSettingsForm',
+          '_title' => "{$entity_type->getLabel()} settings",
         ])
         ->setRequirement('_permission', $entity_type->getAdminPermission())
         ->setOption('_admin_route', TRUE);

+ 59 - 0
src/HeartbeatStreamHtmlRouteProvider.php.bak

@@ -0,0 +1,59 @@
+<?php
+
+namespace Drupal\heartbeat8;
+
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\Routing\AdminHtmlRouteProvider;
+use Symfony\Component\Routing\Route;
+
+/**
+ * Provides routes for Heartbeat stream entities.
+ *
+ * @see Drupal\Core\Entity\Routing\AdminHtmlRouteProvider
+ * @see Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider
+ */
+class HeartbeatStreamHtmlRouteProvider extends AdminHtmlRouteProvider {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getRoutes(EntityTypeInterface $entity_type) {
+    $collection = parent::getRoutes($entity_type);
+
+    $entity_type_id = $entity_type->id();
+
+    if ($collection_route = $this->getCollectionRoute($entity_type)) {
+      $collection->add("entity.{$entity_type_id}.collection", $collection_route);
+    }
+
+    return $collection;
+  }
+
+  /**
+   * Gets the collection route.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   *
+   * @return \Symfony\Component\Routing\Route|null
+   *   The generated route, if available.
+   */
+  protected function getCollectionRoute(EntityTypeInterface $entity_type) {
+    if ($entity_type->hasLinkTemplate('collection') && $entity_type->hasListBuilderClass()) {
+      $entity_type_id = $entity_type->id();
+      $route = new Route($entity_type->getLinkTemplate('collection'));
+      $route
+        ->setDefaults([
+          '_entity_list' => $entity_type_id,
+          // Make sure this is not a TranslatableMarkup object as the
+          // TitleResolver translates this string again.
+          '_title' => (string) $entity_type->getLabel(),
+        ])
+        ->setRequirement('_permission', $entity_type->getAdminPermission())
+        ->setOption('_admin_route', TRUE);
+
+      return $route;
+    }
+  }
+
+}

+ 20 - 7
src/HeartbeatStreamListBuilder.php

@@ -2,20 +2,26 @@
 
 namespace Drupal\heartbeat8;
 
-use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityListBuilder;
+use Drupal\Core\Routing\LinkGeneratorTrait;
+use Drupal\Core\Url;
 
 /**
- * Provides a listing of Heartbeat stream entities.
+ * Defines a class to build a listing of Heartbeat stream entities.
+ *
+ * @ingroup heartbeat8
  */
-class HeartbeatStreamListBuilder extends ConfigEntityListBuilder {
+class HeartbeatStreamListBuilder extends EntityListBuilder {
+
+  use LinkGeneratorTrait;
 
   /**
    * {@inheritdoc}
    */
   public function buildHeader() {
-    $header['label'] = $this->t('Heartbeat stream');
-    $header['id'] = $this->t('Machine name');
+    $header['id'] = $this->t('Heartbeat stream ID');
+    $header['name'] = $this->t('Name');
     return $header + parent::buildHeader();
   }
 
@@ -23,9 +29,16 @@ class HeartbeatStreamListBuilder extends ConfigEntityListBuilder {
    * {@inheritdoc}
    */
   public function buildRow(EntityInterface $entity) {
-    $row['label'] = $entity->label();
+    /* @var $entity \Drupal\heartbeat8\Entity\HeartbeatStream */
     $row['id'] = $entity->id();
-    // You probably want a few more properties here...
+    $row['name'] = $this->l(
+      $entity->label(),
+      new Url(
+        'entity.heartbeat_stream.edit_form', array(
+          'heartbeat_stream' => $entity->id(),
+        )
+      )
+    );
     return $row + parent::buildRow($entity);
   }
 

+ 32 - 0
src/HeartbeatStreamListBuilder.php.bak

@@ -0,0 +1,32 @@
+<?php
+
+namespace Drupal\heartbeat8;
+
+use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
+use Drupal\Core\Entity\EntityInterface;
+
+/**
+ * Provides a listing of Heartbeat stream entities.
+ */
+class HeartbeatStreamListBuilder extends ConfigEntityListBuilder {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildHeader() {
+    $header['label'] = $this->t('Heartbeat stream');
+    $header['id'] = $this->t('Machine name');
+    return $header + parent::buildHeader();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildRow(EntityInterface $entity) {
+    $row['label'] = $entity->label();
+    $row['id'] = $entity->id();
+    // You probably want a few more properties here...
+    return $row + parent::buildRow($entity);
+  }
+
+}

+ 58 - 0
src/HeartbeatStreamStorage.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\heartbeat8;
+
+use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\heartbeat8\Entity\HeartbeatStreamInterface;
+
+/**
+ * Defines the storage handler class for Heartbeat stream entities.
+ *
+ * This extends the base storage class, adding required special handling for
+ * Heartbeat stream entities.
+ *
+ * @ingroup heartbeat8
+ */
+class HeartbeatStreamStorage extends SqlContentEntityStorage implements HeartbeatStreamStorageInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function revisionIds(HeartbeatStreamInterface $entity) {
+    return $this->database->query(
+      'SELECT vid FROM {heartbeat_stream_revision} WHERE id=:id ORDER BY vid',
+      array(':id' => $entity->id())
+    )->fetchCol();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function userRevisionIds(AccountInterface $account) {
+    return $this->database->query(
+      'SELECT vid FROM {heartbeat_stream_field_revision} WHERE uid = :uid ORDER BY vid',
+      array(':uid' => $account->id())
+    )->fetchCol();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function countDefaultLanguageRevisions(HeartbeatStreamInterface $entity) {
+    return $this->database->query('SELECT COUNT(*) FROM {heartbeat_stream_field_revision} WHERE id = :id AND default_langcode = 1', array(':id' => $entity->id()))
+      ->fetchField();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clearRevisionsLanguage(LanguageInterface $language) {
+    return $this->database->update('heartbeat_stream_revision')
+      ->fields(array('langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED))
+      ->condition('langcode', $language->getId())
+      ->execute();
+  }
+
+}

+ 61 - 0
src/HeartbeatStreamStorageInterface.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace Drupal\heartbeat8;
+
+use Drupal\Core\Entity\ContentEntityStorageInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Language\LanguageInterface;
+use Drupal\heartbeat8\Entity\HeartbeatStreamInterface;
+
+/**
+ * Defines the storage handler class for Heartbeat stream entities.
+ *
+ * This extends the base storage class, adding required special handling for
+ * Heartbeat stream entities.
+ *
+ * @ingroup heartbeat8
+ */
+interface HeartbeatStreamStorageInterface extends ContentEntityStorageInterface {
+
+  /**
+   * Gets a list of Heartbeat stream revision IDs for a specific Heartbeat stream.
+   *
+   * @param \Drupal\heartbeat8\Entity\HeartbeatStreamInterface $entity
+   *   The Heartbeat stream entity.
+   *
+   * @return int[]
+   *   Heartbeat stream revision IDs (in ascending order).
+   */
+  public function revisionIds(HeartbeatStreamInterface $entity);
+
+  /**
+   * Gets a list of revision IDs having a given user as Heartbeat stream author.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   The user entity.
+   *
+   * @return int[]
+   *   Heartbeat stream revision IDs (in ascending order).
+   */
+  public function userRevisionIds(AccountInterface $account);
+
+  /**
+   * Counts the number of revisions in the default language.
+   *
+   * @param \Drupal\heartbeat8\Entity\HeartbeatStreamInterface $entity
+   *   The Heartbeat stream entity.
+   *
+   * @return int
+   *   The number of revisions in the default language.
+   */
+  public function countDefaultLanguageRevisions(HeartbeatStreamInterface $entity);
+
+  /**
+   * Unsets the language for all Heartbeat stream with the given language.
+   *
+   * @param \Drupal\Core\Language\LanguageInterface $language
+   *   The language object.
+   */
+  public function clearRevisionsLanguage(LanguageInterface $language);
+
+}

+ 22 - 0
templates/heartbeat_stream.html.twig

@@ -0,0 +1,22 @@
+{#
+/**
+ * @file heartbeat_stream.html.twig
+ * Default theme implementation to present Heartbeat stream data.
+ *
+ * This template is used when viewing Heartbeat stream pages.
+ *
+ *
+ * Available variables:
+ * - content: A list of content items. Use 'content' to print all content, or
+ * - attributes: HTML attributes for the container element.
+ *
+ * @see template_preprocess_heartbeat_stream()
+ *
+ * @ingroup themeable
+ */
+#}
+<div{{ attributes.addClass('heartbeat_stream') }}>
+  {% if content %}
+    {{- content -}}
+  {% endif %}
+</div>