diff --git a/core/modules/help/config/install/help.help.help_module.yml b/core/modules/help/config/install/help.help.help_module.yml
new file mode 100644
index 0000000..54ef0d2
--- /dev/null
+++ b/core/modules/help/config/install/help.help.help_module.yml
@@ -0,0 +1,10 @@
+langcode: en
+status: true
+dependencies: { }
+id: help_module
+label: 'Help Module'
+body:
+ value: "
About
\r\n\r\nThe Help module provides Help reference pages to guide you through the use and configuration of modules. It is a starting point for Drupal.org online documentation pages that contain more extensive and up-to-date information, are annotated with user-contributed comments, and serve as the definitive reference point for all Drupal documentation. For more information, see the online documentation for the Help module.
\r\n\r\nUses
\r\n\r\n\r\n\t- Providing a help reference
\r\n\t- The Help module displays both static module-provided help and configured help topics on the main Help page.
\r\n\t- Configuring help topics
\r\n\t- You can add, edit, delete, and translate configured help topics on the Help topics administration page. The help topics that are listed in the Module help section of the main Help page cannot be edited or deleted.
\r\n
\r\n"
+ format: full_html
+top_level: true
+related: { }
diff --git a/core/modules/help/config/schema/help.schema.yml b/core/modules/help/config/schema/help.schema.yml
new file mode 100644
index 0000000..4924d83
--- /dev/null
+++ b/core/modules/help/config/schema/help.schema.yml
@@ -0,0 +1,28 @@
+help.help.*:
+ type: config_entity
+ label: 'Help topic'
+ mapping:
+ id:
+ type: string
+ label: 'Machine-readable name'
+ label:
+ type: label
+ label: 'Topic title'
+ body:
+ label: 'Body'
+ type: mapping
+ mapping:
+ value:
+ type: text
+ label: 'Body text'
+ format:
+ type: string
+ label: 'Body format'
+ top_level:
+ type: boolean
+ label: 'Display on help topic list'
+ related:
+ type: sequence
+ label: 'Related topics'
+ sequence:
+ - type: string
diff --git a/core/modules/help/help.links.action.yml b/core/modules/help/help.links.action.yml
new file mode 100644
index 0000000..de27a52
--- /dev/null
+++ b/core/modules/help/help.links.action.yml
@@ -0,0 +1,5 @@
+entity.help.add_form:
+ route_name: entity.help.add_form
+ title: 'Add new help topic'
+ appears_on:
+ - help.topic_admin
diff --git a/core/modules/help/help.links.menu.yml b/core/modules/help/help.links.menu.yml
index aa85add..aa9c469 100644
--- a/core/modules/help/help.links.menu.yml
+++ b/core/modules/help/help.links.menu.yml
@@ -4,3 +4,9 @@ help.main:
route_name: help.main
weight: 9
parent: system.admin
+
+help.topic_admin:
+ title: Help topics
+ description: Add, delete, and edit help topics.
+ route_name: help.topic_admin
+ parent: system.admin_config_development
diff --git a/core/modules/help/help.module b/core/modules/help/help.module
index 3539660..81dfdad 100644
--- a/core/modules/help/help.module
+++ b/core/modules/help/help.module
@@ -26,17 +26,6 @@ function help_help($route_name, RouteMatchInterface $route_match) {
$output .= '';
$output .= '' . t('For more information, refer to the subjects listed in the Help Topics section or to the online documentation and support pages at drupal.org.', array('!docs' => 'https://drupal.org/documentation', '!support' => 'https://drupal.org/support', '!drupal' => 'https://drupal.org')) . '
';
return $output;
-
- case 'help.page.help':
- $output = '';
- $output .= '' . t('About') . '
';
- $output .= '' . t('The Help module provides Help reference pages to guide you through the use and configuration of modules. It is a starting point for Drupal.org online documentation pages that contain more extensive and up-to-date information, are annotated with user-contributed comments, and serve as the definitive reference point for all Drupal documentation. For more information, see the online documentation for the Help module.', array('!help' => 'https://drupal.org/documentation/modules/help/', '!handbook' => 'https://drupal.org/documentation', '!help-page' => \Drupal::url('help.main'))) . '
';
- $output .= '' . t('Uses') . '
';
- $output .= '';
- $output .= '- ' . t('Providing a help reference') . '
';
- $output .= '- ' . t('The Help module displays explanations for using each module listed on the main Help reference page.', array('!help' => \Drupal::url('help.main'))) . '
';
- $output .= '
';
- return $output;
}
}
diff --git a/core/modules/help/help.permissions.yml b/core/modules/help/help.permissions.yml
new file mode 100644
index 0000000..c1bc85a
--- /dev/null
+++ b/core/modules/help/help.permissions.yml
@@ -0,0 +1,3 @@
+administer help topics:
+ title: 'Administer help topics'
+ description: 'Create, edit, and delete help topics.'
diff --git a/core/modules/help/help.routing.yml b/core/modules/help/help.routing.yml
index a393eb8..4bc804b 100644
--- a/core/modules/help/help.routing.yml
+++ b/core/modules/help/help.routing.yml
@@ -13,3 +13,44 @@ help.page:
_title: 'Help'
requirements:
_permission: 'access administration pages'
+
+help.topic_view:
+ path: 'admin/help_topic/{id}'
+ defaults:
+ _content: '\Drupal\help\Controller\HelpController::helpEntityView'
+ _title: 'Help'
+ requirements:
+ _permission: 'access administration pages'
+
+help.topic_admin:
+ path: 'admin/config/development/help'
+ defaults:
+ _entity_list: 'help'
+ _title: 'Help topics'
+ requirements:
+ _permission: 'administer help topics'
+
+entity.help.add_form:
+ path: 'admin/config/development/help/add'
+ defaults:
+ _entity_form: 'help.add'
+ _title: 'Add help topic'
+ requirements:
+ _entity_create_access: 'help'
+
+entity.help.edit_form:
+ path: 'admin/config/development/help/{help}/edit'
+ defaults:
+ _entity_form: 'help.edit'
+ _title: 'Edit help topic'
+ requirements:
+ _entity_access: 'help.edit'
+
+entity.help.delete_form:
+ path: 'admin/config/development/help/{help}/delete'
+ defaults:
+ _entity_form: 'help.delete'
+ _title: 'Delete help topic'
+ requirements:
+ _entity_access: 'help.delete'
+
diff --git a/core/modules/help/src/Controller/HelpController.php b/core/modules/help/src/Controller/HelpController.php
index 80ae8f4..6bf9625 100644
--- a/core/modules/help/src/Controller/HelpController.php
+++ b/core/modules/help/src/Controller/HelpController.php
@@ -7,12 +7,15 @@
namespace Drupal\help\Controller;
+use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
+use Drupal\Component\Utility\String;
+use Drupal\help\Entity\Help;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
-use Drupal\Component\Utility\String;
/**
* Controller routines for help routes.
@@ -27,13 +30,33 @@ class HelpController extends ControllerBase {
protected $routeMatch;
/**
+ * The config factory.
+ *
+ * @var \Drupal\Core\Config\ConfigFactoryInterface
+ */
+ protected $configFactory;
+
+ /**
+ * The help entity storage.
+ *
+ * @var \Drupal\Core\Entity\EntityStorageInterface
+ */
+ protected $helpStorage;
+
+ /**
* Creates a new HelpController.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
+ * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+ * The config factory.
+ * @param \Drupal\Core\Entity\EntityStorageInterface $help_storage
+ * The entity storage for help topic entities.
*/
- public function __construct(RouteMatchInterface $route_match) {
+ public function __construct(RouteMatchInterface $route_match, ConfigFactoryInterface $config_factory, EntityStorageInterface $help_storage) {
$this->routeMatch = $route_match;
+ $this->configFactory = $config_factory;
+ $this->helpStorage = $help_storage;
}
/**
@@ -41,50 +64,71 @@ public function __construct(RouteMatchInterface $route_match) {
*/
public static function create(ContainerInterface $container) {
return new static(
- $container->get('current_route_match')
+ $container->get('current_route_match'),
+ $container->get('config.factory'),
+ $container->get('entity.manager')->getStorage('help')
);
}
/**
- * Prints a page listing a glossary of Drupal terminology.
+ * Prints a page listing help topics.
*
- * @return string
- * An HTML string representing the contents of help page.
+ * @return array
+ * Render array for the help topics list.
*/
public function helpMain() {
$output = array(
'#attached' => array(
'css' => array(drupal_get_path('module', 'help') . '/css/help.module.css'),
),
- '#markup' => '' . $this->t('Help topics') . '
' . $this->t('Help is available on the following items:') . '
' . $this->helpLinksAsList(),
+ '#markup' => '' . $this->t('Module help') . '
' . $this->t('Help pages are available for the following modules:') . '
' . $this->moduleHelpLinksAsList() .
+ '' . $this->t('Configured topics') . '
' . $this->t('Additional help topics configured on your site:') . '
' . $this->helpTopicsList(),
);
return $output;
}
/**
- * Provides a formatted list of available help topics.
+ * Provides a formatted list of available module help topics from hook_help().
*
* @return string
* A string containing the formatted list.
*/
- protected function helpLinksAsList() {
+ protected function moduleHelpLinksAsList() {
$module_info = system_rebuild_module_data();
$modules = array();
foreach ($this->moduleHandler()->getImplementations('help') as $module) {
if ($this->moduleHandler()->invoke($module, 'help', array("help.page.$module", $this->routeMatch))) {
- $modules[$module] = $module_info[$module]->info['name'];
+ $modules[$module] = array(
+ 'title' => $module_info[$module]->info['name'],
+ 'url' => new Url('help.page', array('name' => $module)),
+ );
}
}
asort($modules);
+ return $this->fourColumnList($modules);
+ }
+
+ /**
+ * Makes a four-column list of links.
+ *
+ * @param array $links
+ * Array whose elements are arrays with:
+ * - title: Link title.
+ * - url: URL object.
+ *
+ * @return string
+ * Markup for a four-column UL list of links.
+ */
+ protected function fourColumnList($links) {
// Output pretty four-column list.
- $count = count($modules);
+ $count = count($links);
$break = ceil($count / 4);
$output = '';
$i = 0;
- foreach ($modules as $module => $name) {
- $output .= '- ' . $this->l($name, new Url('help.page', array('name' => $module))) . '
';
+ foreach ($links as $info) {
+ $output .= '- ' . $this->l($info['title'], $info['url']) . '
';
if (($i + 1) % $break == 0 && ($i + 1) != $count) {
$output .= '
';
}
@@ -96,13 +140,36 @@ protected function helpLinksAsList() {
}
/**
- * Prints a page listing general help for a module.
+ * Provides a formatted list of configured help topics.
+ *
+ * @return string
+ * A string containing the formatted list.
+ */
+ protected function helpTopicsList() {
+ $entities = $this->helpStorage->loadMultiple();
+ uasort($entities, array('Drupal\help\Entity\Help', 'sort'));
+
+ $topics = array();
+ foreach ($entities as $entity) {
+ if ($entity->get('top_level')) {
+ $topics[] = array(
+ 'title' => $entity->label(),
+ 'url' => new Url('help.topic_view', array('id' => $entity->id())),
+ );
+ }
+ }
+
+ return $this->fourColumnList($topics);
+ }
+
+ /**
+ * Prints a help page from a module's hook_help().
*
* @param string $name
* A module name to display a help page for.
*
* @return array
- * A render array as expected by drupal_render().
+ * A render array for the help page.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
@@ -145,4 +212,66 @@ public function helpPage($name) {
}
}
+ /**
+ * Renders a help topic page from a configured help entity.
+ *
+ * @param string $id
+ * The ID of the help entity to display.
+ *
+ * @return array
+ * A render array for the help topic page.
+ *
+ * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
+ */
+ public function helpEntityView($id) {
+ $config = $this->configFactory->get('help.help.' . $id);
+
+ $build = array();
+
+ $build['#title'] = String::checkPlain($config->get('label'));
+
+ // When you get a config object, if it doesn't find the config it
+ // returns a new empty config object. So check and see if we found a
+ // topic title. If not, this is a page not found.
+ if (!$build['#title']) {
+ throw new NotFoundHttpException();
+ }
+
+ $body = $config->get('body');
+ $build['body'] = array(
+ '#type' => 'processed_text',
+ '#text' => $body['value'],
+ '#format' => $body['format'],
+ );
+
+ $related = $config->get('related');
+ $links = array();
+ foreach($related as $other_id) {
+ if ($other_id != $id) {
+ $topic = $this->configFactory->get('help.help.' . $other_id);
+ // This could be an empty new config object. Only display topics
+ // with titles, and silently omit others.
+ if ($title = $topic->get('label')) {
+ $links[] = array(
+ 'title' => $title,
+ ) + Url::fromRoute('help.topic_view', array('id' => $other_id))->toArray();
+ }
+ }
+ }
+
+ if (count($links)) {
+ $build['related'] = array(
+ '#theme' => 'links',
+ '#heading' => array(
+ 'text' => $this->t('Related topics'),
+ 'level' => 'h3',
+ ),
+ '#links' => $links,
+ );
+ }
+
+ // @todo Also make a list of topics that list this one as "related"?
+
+ return $build;
+ }
}
diff --git a/core/modules/help/src/Entity/Help.php b/core/modules/help/src/Entity/Help.php
new file mode 100644
index 0000000..dc5be2d
--- /dev/null
+++ b/core/modules/help/src/Entity/Help.php
@@ -0,0 +1,94 @@
+t('Are you sure you want to delete the topic %name?', array('%name' => $this->entity->label()));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfirmText() {
+ return t('Delete');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ return new Url('help.topic_admin');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $this->entity->delete();
+ drupal_set_message(t('Deleted help topic %name.', array('%name' => $this->entity->label())));
+
+ $form_state->setRedirectUrl($this->getCancelUrl());
+ }
+}
diff --git a/core/modules/help/src/Form/HelpForm.php b/core/modules/help/src/Form/HelpForm.php
new file mode 100644
index 0000000..dc4a3b3
--- /dev/null
+++ b/core/modules/help/src/Form/HelpForm.php
@@ -0,0 +1,153 @@
+helpStorage = $help_storage;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('entity.manager')->getStorage('help')
+ );
+ }
+
+ /**
+ * Checks for an existing help topic.
+ *
+ * @param string $entity_id
+ * The entity ID.
+ *
+ * @return bool
+ * TRUE if this topic already exists, FALSE otherwise.
+ */
+ public function exists($entity_id) {
+ return (bool) $this->helpStorage
+ ->getQuery()
+ ->condition('id', $entity_id)
+ ->execute();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function form(array $form, FormStateInterface $form_state) {
+ $form['label'] = array(
+ '#type' => 'textfield',
+ '#title' => $this->t('Title'),
+ '#maxlength' => 100,
+ '#default_value' => $this->entity->label(),
+ '#required' => TRUE,
+ );
+
+ $form['id'] = array(
+ '#type' => 'machine_name',
+ '#default_value' => $this->entity->id(),
+ '#machine_name' => array(
+ 'exists' => array($this, 'exists'),
+ 'error' => $this->t('The machine-readable name must be unique, and can only contain lowercase letters, numbers, and underscores.'),
+ ),
+ );
+
+ $form['top_level'] = array(
+ '#type' => 'checkbox',
+ '#default_value' => $this->entity->get('top_level'),
+ '#title' => $this->t('Top-level topic'),
+ '#description' => $this->t('Check box if this topic should be displayed on the Topics list'),
+ );
+
+ $body = $this->entity->get('body');
+
+ $form['body'] = array(
+ '#type' => 'text_format',
+ '#title' => $this->t('Body'),
+ '#default_value' => $body['value'],
+ '#format' => $body['format'],
+ );
+
+ $form['related'] = array(
+ '#title' => $this->t('Related topics'),
+ '#description' => $this->t('Comma-separated list of machine names of related topics.'),
+ '#type' => 'textarea',
+ '#default_value' => implode(',', $this->entity->get('related')),
+ );
+
+ $form['#entity_builders'][] = array($this, 'copyRelatedFieldToEntity');
+
+ return parent::form($form, $form_state);
+ }
+
+ /**
+ * Copies the related topics field value to the entity property.
+ *
+ * This is added to $form['#entity_builders'] in the form builder method.
+ *
+ * @param string $type
+ * Type of entity.
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * Entity to copy property values to.
+ * @param array $form
+ * Form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * Form state.
+ */
+ protected function copyRelatedFieldToEntity($type, EntityInterface $entity, array &$form, FormStateInterface &$form_state) {
+ $related = explode(',', $form_state->getValue('related'));
+ $tosave = array();
+ foreach ($related as $item) {
+ $item = trim($item);
+ if ($item) {
+ $tosave[] = $item;
+ }
+ }
+
+ $entity->set('related', $tosave);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(array $form, FormStateInterface $form_state) {
+ $form_state->setRedirect('help.topic_admin');
+
+ $status = $this->entity->save();
+ if ($status == SAVED_UPDATED) {
+ drupal_set_message(t('Help topic updated.'));
+ }
+ else {
+ drupal_set_message(t('Help topic added.'));
+ }
+ }
+}
diff --git a/core/modules/help/src/HelpAccessControlHandler.php b/core/modules/help/src/HelpAccessControlHandler.php
new file mode 100644
index 0000000..43043dc
--- /dev/null
+++ b/core/modules/help/src/HelpAccessControlHandler.php
@@ -0,0 +1,33 @@
+t('Title');
+ $header['id'] = $this->t('Machine name');
+
+ return $header + parent::buildHeader();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildRow(EntityInterface $entity) {
+ $row = array();
+
+ $row['label'] = $entity->label();
+ $row['id'] = $entity->id();
+
+ return $row + parent::buildRow($entity);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDefaultOperations(EntityInterface $entity) {
+ $operations = parent::getDefaultOperations($entity);
+
+ $url = Url::fromRoute('help.topic_view', array('id' => $entity->id()));
+ if ($entity->access('view')) {
+ $operations['view'] = array(
+ 'title' => $this->t('View'),
+ 'weight' => 0,
+ ) + $url->toArray();
+ }
+
+ return $operations;
+ }
+
+}
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index f721626..af3c52e 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -883,10 +883,13 @@ function hook_system_info_alter(array &$info, \Drupal\Core\Extension\Extension $
* developers should usually be provided via function header comments in the
* code, or in special API example files.
*
+ * Module overviews and other topic pages can also be provided as configurable
+ * help topics, using the \Drupal\help\Entity\Help config entity, outside of
+ * this hook. See the Help class documentation header for details.
+ *
* The page-specific help information provided by this hook appears as a system
- * help block on that page. The module overview help information is displayed
- * by the Help module. It can be accessed from the page at admin/help or from
- * the Extend page.
+ * help block on that page. The module overview help and configurable help
+ * topics are displayed by the Help module; topics are listed on admin/help.
*
* For detailed usage examples of:
* - Module overview help, see content_translation_help(). Module overview