Issue #2936995 by viappidu, mcaddz, acbramley, slydevil, yash.rode, sokru, quietone, Sandeep_k, smustgrave, amateescu, AaronMcHale, lauriii, longwave: Add taxonomy term revision UI

merge-requests/5076/merge
Dave Long 2024-02-09 18:01:26 +00:00
parent bf4e65cf04
commit 4a0bdc34a4
No known key found for this signature in database
GPG Key ID: ED52AE211E142771
24 changed files with 554 additions and 2 deletions

View File

@ -8,3 +8,4 @@ name: Forums
vid: forums
description: 'Forum navigation vocabulary'
weight: -10
new_revision: false

View File

@ -88,6 +88,7 @@ class VocabularyTest extends ConfigEntityResourceTestBase {
'status' => TRUE,
'dependencies' => [],
'name' => 'Llama',
'new_revision' => FALSE,
'description' => NULL,
'weight' => 0,
'drupal_internal__vid' => 'llama',

View File

@ -5,3 +5,4 @@ name: Track changes import term
vid: track_changes_import_term
description: ''
weight: 0
new_revision: false

View File

@ -5,3 +5,4 @@ name: Tags
vid: tags
description: 'Use tags to group articles on similar topics into categories.'
weight: 0
new_revision: false

View File

@ -40,6 +40,9 @@ taxonomy.vocabulary.*:
weight:
type: integer
label: 'Weight'
new_revision:
type: boolean
label: 'Whether a new revision should be created by default'
field.formatter.settings.entity_reference_rss_category:
type: mapping

View File

@ -32,7 +32,12 @@ use Drupal\user\StatusItem;
* "views_data" = "Drupal\taxonomy\TermViewsData",
* "form" = {
* "default" = "Drupal\taxonomy\TermForm",
* "delete" = "Drupal\taxonomy\Form\TermDeleteForm"
* "delete" = "Drupal\taxonomy\Form\TermDeleteForm",
* "revision-delete" = \Drupal\Core\Entity\Form\RevisionDeleteForm::class,
* "revision-revert" = \Drupal\Core\Entity\Form\RevisionRevertForm::class,
* },
* "route_provider" = {
* "revision" = \Drupal\Core\Entity\Routing\RevisionHtmlRouteProvider::class,
* },
* "translation" = "Drupal\taxonomy\TermTranslationHandler"
* },
@ -40,6 +45,7 @@ use Drupal\user\StatusItem;
* data_table = "taxonomy_term_field_data",
* revision_table = "taxonomy_term_revision",
* revision_data_table = "taxonomy_term_field_revision",
* show_revision_ui = TRUE,
* translatable = TRUE,
* entity_keys = {
* "id" = "tid",
@ -63,6 +69,10 @@ use Drupal\user\StatusItem;
* "delete-form" = "/taxonomy/term/{taxonomy_term}/delete",
* "edit-form" = "/taxonomy/term/{taxonomy_term}/edit",
* "create" = "/taxonomy/term",
* "revision" = "/taxonomy/term/{taxonomy_term}/revision/{taxonomy_term_revision}/view",
* "revision-delete-form" = "/taxonomy/term/{taxonomy_term}/revision/{taxonomy_term_revision}/delete",
* "revision-revert-form" = "/taxonomy/term/{taxonomy_term}/revision/{taxonomy_term_revision}/revert",
* "version-history" = "/taxonomy/term/{taxonomy_term}/revisions",
* },
* permission_granularity = "bundle",
* collection_permission = "access taxonomy overview",

View File

@ -57,6 +57,7 @@ use Drupal\taxonomy\VocabularyInterface;
* "vid",
* "description",
* "weight",
* "new_revision",
* }
* )
*/
@ -104,6 +105,13 @@ class Vocabulary extends ConfigEntityBundleBase implements VocabularyInterface {
return $this->description;
}
/**
* The default revision setting for a vocabulary.
*
* @var bool
*/
protected $new_revision = FALSE;
/**
* {@inheritdoc}
*/
@ -161,4 +169,18 @@ class Vocabulary extends ConfigEntityBundleBase implements VocabularyInterface {
}
}
/**
* {@inheritdoc}
*/
public function setNewRevision($new_revision) {
$this->new_revision = $new_revision;
}
/**
* {@inheritdoc}
*/
public function shouldCreateNewRevision() {
return $this->new_revision;
}
}

View File

@ -69,6 +69,15 @@ class TaxonomyPermissions implements ContainerInjectionInterface {
"create terms in $id" => ['title' => $this->t('%vocabulary: Create terms', $args)],
"delete terms in $id" => ['title' => $this->t('%vocabulary: Delete terms', $args)],
"edit terms in $id" => ['title' => $this->t('%vocabulary: Edit terms', $args)],
"view term revisions in $id" => ['title' => $this->t('%vocabulary: View term revisions', $args)],
"revert term revisions in $id" => [
'title' => $this->t('%vocabulary: Revert term revisions', $args),
'description' => $this->t('To revert a revision you also need permission to edit the taxonomy term.'),
],
"delete term revisions in $id" => [
'title' => $this->t('%vocabulary: Delete term revisions', $args),
'description' => $this->t('To delete a revision you also need permission to delete the taxonomy term.'),
],
];
}

View File

@ -46,6 +46,25 @@ class TermAccessControlHandler extends EntityAccessControlHandler {
return AccessResult::neutral()->setReason("The following permissions are required: 'delete terms in {$entity->bundle()}' OR 'administer taxonomy'.");
case 'view revision':
case 'view all revisions':
if ($account->hasPermission("view term revisions in {$entity->bundle()}") || $account->hasPermission("view all taxonomy revisions")) {
return AccessResult::allowed()->cachePerPermissions();
}
return AccessResult::neutral()->setReason("The following permissions are required: 'view revisions in {$entity->bundle()}' OR 'view all taxonomy revisions'.");
case 'revert':
if (($account->hasPermission("revert term revisions in {$entity->bundle()}") && $account->hasPermission("edit terms in {$entity->bundle()}")) || $account->hasPermission("revert all taxonomy revisions")) {
return AccessResult::allowed()->cachePerPermissions();
}
return AccessResult::neutral()->setReason("The following permissions are required: 'revert term revisions in {$entity->bundle()}' OR 'revert all taxonomy revisions'.");
case 'delete revision':
if (($account->hasPermission("delete term revisions in {$entity->bundle()}") && $account->hasPermission("delete terms in {$entity->bundle()}")) || $account->hasPermission("delete all taxonomy revisions")) {
return AccessResult::allowed()->cachePerPermissions();
}
return AccessResult::neutral()->setReason("The following permissions are required: 'delete term revisions in {$entity->bundle()}' OR 'delete all taxonomy revisions'.");
default:
// No opinion.
return AccessResult::neutral()->cachePerPermissions();

View File

@ -76,6 +76,13 @@ class VocabularyForm extends BundleEntityFormBase {
'#default_value' => $vocabulary->getDescription(),
];
$form['revision'] = [
'#type' => 'checkbox',
'#title' => $this->t('Create new revision'),
'#default_value' => $vocabulary->shouldCreateNewRevision(),
'#description' => $this->t('Create a new revision by default for this vocabulary.'),
];
// $form['langcode'] is not wrapped in an
// if ($this->moduleHandler->moduleExists('language')) check because the
// language_select form element works also without the language module being
@ -117,6 +124,7 @@ class VocabularyForm extends BundleEntityFormBase {
*/
public function save(array $form, FormStateInterface $form_state) {
$vocabulary = $this->entity;
$vocabulary->setNewRevision($form_state->getValue(['revision']));
// Prevent leading and trailing spaces in vocabulary names.
$vocabulary->set('name', trim($vocabulary->label()));

View File

@ -3,11 +3,12 @@
namespace Drupal\taxonomy;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\RevisionableEntityBundleInterface;
/**
* Provides an interface defining a taxonomy vocabulary entity.
*/
interface VocabularyInterface extends ConfigEntityInterface {
interface VocabularyInterface extends ConfigEntityInterface, RevisionableEntityBundleInterface {
/**
* Denotes that no term in the vocabulary has a parent.
@ -32,4 +33,12 @@ interface VocabularyInterface extends ConfigEntityInterface {
*/
public function getDescription();
/**
* Sets whether a new revision should be created by default.
*
* @param bool $new_revision
* TRUE if a new revision should be created by default.
*/
public function setNewRevision($new_revision);
}

View File

@ -5,9 +5,33 @@
* Install, update and uninstall functions for the taxonomy module.
*/
use Drupal\Core\Entity\Form\RevisionDeleteForm;
use Drupal\Core\Entity\Form\RevisionRevertForm;
use Drupal\Core\Entity\Routing\RevisionHtmlRouteProvider;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Implements hook_update_last_removed().
*/
function taxonomy_update_last_removed() {
return 8702;
}
/**
* Update entity definition to handle revision routes.
*/
function taxonomy_update_10100(&$sandbox = NULL): TranslatableMarkup {
$entityDefinitionUpdateManager = \Drupal::entityDefinitionUpdateManager();
$definition = $entityDefinitionUpdateManager->getEntityType('taxonomy_term');
$routeProviders = $definition->get('route_provider');
$routeProviders['revision'] = RevisionHtmlRouteProvider::class;
$definition
->setFormClass('revision-delete', RevisionDeleteForm::class)
->setFormClass('revision-revert', RevisionRevertForm::class)
->set('route_provider', $routeProviders)
->setLinkTemplate('revision-delete-form', '/taxonomy/term/{taxonomy_term}/revision/{taxonomy_term}/delete')
->setLinkTemplate('revision-revert-form', '/taxonomy/term/{taxonomy_term}/revision/{taxonomy_term}/revert')
->setLinkTemplate('version-history', '/taxonomy/term/{taxonomy_term}/revisions');
$entityDefinitionUpdateManager->updateEntityType($definition);
return \t('Added revision routes to Taxonomy Term entity type.');
}

View File

@ -5,5 +5,14 @@ access taxonomy overview:
title: 'Access the taxonomy vocabulary overview page'
description: 'Get an overview of all taxonomy vocabularies.'
revert all taxonomy revisions:
title: 'Revert all term revisions'
delete all taxonomy revisions:
title: 'Delete all term revisions'
view all taxonomy revisions:
title: 'View all term revisions'
permission_callbacks:
- Drupal\taxonomy\TaxonomyPermissions::permissions

View File

@ -5,6 +5,8 @@
* Post update functions for Taxonomy.
*/
use Drupal\Core\Config\Entity\ConfigEntityUpdater;
/**
* Implements hook_removed_post_updates().
*/
@ -19,3 +21,13 @@ function taxonomy_removed_post_updates() {
'taxonomy_post_update_clear_views_argument_validator_plugins_cache' => '10.0.0',
];
}
/**
* Re-save Taxonomy configurations with new_revision config.
*/
function taxonomy_post_update_set_new_revision(&$sandbox = NULL) {
\Drupal::classResolver(ConfigEntityUpdater::class)
->update($sandbox, 'taxonomy_vocabulary', function () {
return TRUE;
});
}

View File

@ -55,6 +55,7 @@ abstract class VocabularyResourceTestBase extends ConfigEntityResourceTestBase {
'name' => 'Llama',
'description' => NULL,
'weight' => 0,
'new_revision' => FALSE,
];
}

View File

@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\taxonomy\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\taxonomy\Entity\Term;
use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait;
/**
* Taxonomy term revision delete form test.
*
* @group taxonomy
* @coversDefaultClass \Drupal\Core\Entity\Form\RevisionDeleteForm
*/
class TaxonomyRevisionDeleteTest extends BrowserTestBase {
use TaxonomyTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'taxonomy',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected $permissions = [
'view term revisions in test',
'delete all taxonomy revisions',
];
/**
* Vocabulary for testing.
*
* @var \Drupal\taxonomy\VocabularyInterface
*/
private $vocabulary;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->vocabulary = $this->createVocabulary(['vid' => 'test', 'name' => 'Test']);
}
/**
* Tests revision delete.
*/
public function testDeleteForm(): void {
$termName = $this->randomMachineName();
$entity = Term::create([
'vid' => $this->vocabulary->id(),
'name' => $termName,
]);
$entity->setRevisionCreationTime((new \DateTimeImmutable('11 January 2009 4pm'))->getTimestamp())
->setRevisionTranslationAffected(TRUE);
$entity->setNewRevision();
$entity->save();
$revisionId = $entity->getRevisionId();
$this->drupalLogin($this->drupalCreateUser($this->permissions));
// Cannot delete latest revision.
$this->drupalGet($entity->toUrl('revision-delete-form'));
$this->assertSession()->statusCodeEquals(403);
// Create a new latest revision.
$entity
->setRevisionCreationTime((new \DateTimeImmutable('11 January 2009 5pm'))->getTimestamp())
->setRevisionTranslationAffected(TRUE)
->setNewRevision();
$entity->save();
// Reload the entity.
$revision = \Drupal::entityTypeManager()->getStorage('taxonomy_term')
->loadRevision($revisionId);
$this->drupalGet($revision->toUrl('revision-delete-form'));
$this->assertSession()->pageTextContains('Are you sure you want to delete the revision from Sun, 01/11/2009 - 16:00?');
$this->assertSession()->buttonExists('Delete');
$this->assertSession()->linkExists('Cancel');
$countRevisions = static function (): int {
return (int) \Drupal::entityTypeManager()->getStorage('taxonomy_term')
->getQuery()
->accessCheck(FALSE)
->allRevisions()
->count()
->execute();
};
$count = $countRevisions();
$this->submitForm([], 'Delete');
$this->assertEquals($count - 1, $countRevisions());
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->addressEquals(sprintf('taxonomy/term/%s/revisions', $entity->id()));
$this->assertSession()->pageTextContains(sprintf('Revision from Sun, 01/11/2009 - 16:00 of Test %s has been deleted.', $termName));
$this->assertSession()->elementsCount('css', 'table tbody tr', 1);
}
}

View File

@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\taxonomy\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\taxonomy\Entity\Term;
use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait;
/**
* Taxonomy term revision form test.
*
* @group taxonomy
* @coversDefaultClass \Drupal\Core\Entity\Form\RevisionRevertForm
*/
class TaxonomyRevisionRevertTest extends BrowserTestBase {
use TaxonomyTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'taxonomy',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected $permissions = [
'view term revisions in test',
'revert all taxonomy revisions',
];
/**
* Vocabulary for testing.
*
* @var \Drupal\taxonomy\VocabularyInterface
*/
private $vocabulary;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->vocabulary = $this->createVocabulary(['vid' => 'test', 'name' => 'Test']);
}
/**
* Tests revision revert.
*/
public function testRevertForm(): void {
$termName = $this->randomMachineName();
$entity = Term::create([
'vid' => $this->vocabulary->id(),
'name' => $termName,
]);
$entity->setRevisionCreationTime((new \DateTimeImmutable('11 January 2009 4pm'))->getTimestamp())
->setRevisionTranslationAffected(TRUE);
$entity->setNewRevision();
$entity->save();
$revisionId = $entity->getRevisionId();
$this->drupalLogin($this->drupalCreateUser($this->permissions));
// Cannot revert latest revision.
$this->drupalGet($entity->toUrl('revision-revert-form'));
$this->assertSession()->statusCodeEquals(403);
// Create a new latest revision.
$entity
->setRevisionCreationTime((new \DateTimeImmutable('11 January 2009 5pm'))->getTimestamp())
->setRevisionTranslationAffected(TRUE)
->setNewRevision();
$entity->save();
// Reload the entity.
$revision = \Drupal::entityTypeManager()->getStorage('taxonomy_term')
->loadRevision($revisionId);
$this->drupalGet($revision->toUrl('revision-revert-form'));
$this->assertSession()->pageTextContains('Are you sure you want to revert to the revision from Sun, 01/11/2009 - 16:00?');
$this->assertSession()->buttonExists('Revert');
$this->assertSession()->linkExists('Cancel');
$countRevisions = static function (): int {
return (int) \Drupal::entityTypeManager()->getStorage('taxonomy_term')
->getQuery()
->accessCheck(FALSE)
->allRevisions()
->count()
->execute();
};
$count = $countRevisions();
$this->submitForm([], 'Revert');
$this->assertEquals($count + 1, $countRevisions());
$this->assertSession()->statusCodeEquals(200);
$this->assertSession()->addressEquals(sprintf('taxonomy/term/%s/revisions', $entity->id()));
$this->assertSession()->pageTextContains(sprintf('Test %s has been reverted to the revision from Sun, 01/11/2009 - 16:00.', $termName));
$this->assertSession()->elementsCount('css', 'table tbody tr', 3);
}
}

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\taxonomy\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait;
/**
* Tests the new_revision setting of taxonomy vocabularies.
*
* @group taxonomy
*/
class TaxonomyRevisionTest extends BrowserTestBase {
use TaxonomyTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'taxonomy',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests default revision settings on vocabularies.
*/
public function testVocabularyTermRevision() {
$assert = $this->assertSession();
$vocabulary1 = $this->createVocabulary(['new_revision' => TRUE]);
$vocabulary2 = $this->createVocabulary(['new_revision' => FALSE]);
$user = $this->createUser([
'administer taxonomy',
]);
$term1 = $this->createTerm($vocabulary1);
$term2 = $this->createTerm($vocabulary2);
// Create some revisions so revision checkbox is visible.
$term1 = $this->createTaxonomyTermRevision($term1);
$term2 = $this->createTaxonomyTermRevision($term2);
$this->drupalLogin($user);
$this->drupalGet($term1->toUrl('edit-form'));
$assert->statusCodeEquals(200);
$assert->checkboxChecked('Create new revision');
$this->drupalGet($term2->toUrl('edit-form'));
$assert->statusCodeEquals(200);
$assert->checkboxNotChecked('Create new revision');
}
}

View File

@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\taxonomy\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\taxonomy\Entity\Term;
use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait;
/**
* Taxonomy term version history test.
*
* @group taxonomy
* @coversDefaultClass \Drupal\Core\Entity\Controller\VersionHistoryController
*/
class TaxonomyRevisionVersionHistoryTest extends BrowserTestBase {
use TaxonomyTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'taxonomy',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected $permissions = [
'view term revisions in test',
'revert all taxonomy revisions',
'delete all taxonomy revisions',
];
/**
* Vocabulary for testing.
*
* @var \Drupal\taxonomy\VocabularyInterface
*/
private $vocabulary;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->vocabulary = $this->createVocabulary(['vid' => 'test', 'name' => 'Test']);
}
/**
* Tests version history page.
*/
public function testVersionHistory(): void {
$entity = Term::create([
'vid' => $this->vocabulary->id(),
'name' => 'Test taxonomy term',
]);
$entity
->setDescription('Description 1')
->setRevisionCreationTime((new \DateTimeImmutable('1st June 2020 7am'))->getTimestamp())
->setRevisionLogMessage('first revision log')
->setRevisionUser($this->drupalCreateUser(name: 'first author'))
->setNewRevision();
$entity->save();
$entity
->setDescription('Description 2')
->setRevisionCreationTime((new \DateTimeImmutable('2nd June 2020 8am'))->getTimestamp())
->setRevisionLogMessage('second revision log')
->setRevisionUser($this->drupalCreateUser(name: 'second author'))
->setNewRevision();
$entity->save();
$entity
->setDescription('Description 3')
->setRevisionCreationTime((new \DateTimeImmutable('3rd June 2020 9am'))->getTimestamp())
->setRevisionLogMessage('third revision log')
->setRevisionUser($this->drupalCreateUser(name: 'third author'))
->setNewRevision();
$entity->save();
$this->drupalLogin($this->drupalCreateUser($this->permissions));
$this->drupalGet($entity->toUrl('version-history'));
$this->assertSession()->elementsCount('css', 'table tbody tr', 3);
// Order is newest to oldest revision by creation order.
$row1 = $this->assertSession()->elementExists('css', 'table tbody tr:nth-child(1)');
// Latest revision does not have revert or delete revision operation.
$this->assertSession()->elementNotExists('named', ['link', 'Revert'], $row1);
$this->assertSession()->elementNotExists('named', ['link', 'Delete'], $row1);
$this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(1)', 'Current revision');
$this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(1)', 'third revision log');
$this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(1)', '06/03/2020 - 09:00 by third author');
$row2 = $this->assertSession()->elementExists('css', 'table tbody tr:nth-child(2)');
$this->assertSession()->elementExists('named', ['link', 'Revert'], $row2);
$this->assertSession()->elementExists('named', ['link', 'Delete'], $row2);
$this->assertSession()->elementTextNotContains('css', 'table tbody tr:nth-child(2)', 'Current revision');
$this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(2)', 'second revision log');
$this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(2)', '06/02/2020 - 08:00 by second author');
$row3 = $this->assertSession()->elementExists('css', 'table tbody tr:nth-child(3)');
$this->assertSession()->elementExists('named', ['link', 'Revert'], $row3);
$this->assertSession()->elementExists('named', ['link', 'Delete'], $row3);
$this->assertSession()->elementTextNotContains('css', 'table tbody tr:nth-child(2)', 'Current revision');
$this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(3)', 'first revision log');
$this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(3)', '06/01/2020 - 07:00 by first author');
}
}

View File

@ -7,6 +7,7 @@ namespace Drupal\Tests\taxonomy\Traits;
use Drupal\Core\Language\LanguageInterface;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\TermInterface;
use Drupal\taxonomy\VocabularyInterface;
/**
@ -63,4 +64,20 @@ trait TaxonomyTestTrait {
return $term;
}
/**
* Creates a new revision for a given taxonomy term.
*
* @param \Drupal\taxonomy\TermInterface $term
* A taxonomy term object.
*
* @return \Drupal\taxonomy\TermInterface
* The new taxonomy term object.
*/
protected function createTaxonomyTermRevision(TermInterface $term) {
$term->set('name', $this->randomMachineName());
$term->setNewRevision();
$term->save();
return $term;
}
}

View File

@ -5,3 +5,4 @@ name: 'Recipe category'
vid: recipe_category
description: 'Use this taxonomy to group recipes of the same type together.'
weight: 0
new_revision: false

View File

@ -5,3 +5,4 @@ name: Tags
vid: tags
description: 'Use tags to group articles on similar topics into categories.'
weight: 0
new_revision: false

View File

@ -5,3 +5,4 @@ name: Tags
vid: tags
description: 'Use tags to group articles on similar topics into categories.'
weight: 0
new_revision: false

View File

@ -5,3 +5,4 @@ name: Tags
vid: tags
description: 'Use tags to group articles on similar topics into categories.'
weight: 0
new_revision: false